バーコード蔵書管理アプリに手を加えてみた

[AppSheetと20行のGoogle Apps Scriptで作るバーコード蔵書管理アプリ](https://qiita.com/takatama/items/fa141cb13ccc7324f887)
が大変すばらしかったので、自分でも実装してみましたが、

この記事で使用している  [OpenBD API](https://openbd.jp/#first)では対応していない書籍がいくつか見られたので、
見つからなかった場合他のAPIより取得という機能を追加してみました。

:::note info
スプレットシートのフォーマットは[元記事](https://qiita.com/takatama/items/fa141cb13ccc7324f887)のをそのまま使用しています。
:::

:::note warn
他のAPIではPubDateの表示形式が異なっていたため、そのままで設定すると一部表示形式が異なって表示されてしまうので、
PubDateの列全体の表示形式をスプレットシート上であらかじめ**yyyy/mm/dd**形式に変更しておく必要があります。
:::

# GoogleAppsScriptについて
OpenBDのAPIでヒットしなかったまたはヒットしたけど著者が空欄だったら楽天のAPIを、
楽天のAPIでヒットしなかったらGoogleのAPIを検索し、
GoogleのAPIでもヒットしなかったらIDとCreateAtの列だけ追記します。
※一部書籍について、OpenDBのAPIでは書籍の画像がヒットしなかったため、画像がヒットしない場合は画像だけGoogleのAPIのものを使用しています。
→画像はAmazonのものに差し替えました。
※念のため楽天のAPIについても同様に画像はヒットしなければGoogleのものを使用するコードを記載しています。
※楽天のPubDateは日にちまでの記載がなかったり(**例：2016年03月**)、**2016年03月04日頃**とかの曖昧な記載が多かったので、Googleのものに変更しています。
※楽天のアプリIDは[**こちら**](https://webservice.rakuten.co.jp/app/create)より登録することで取得できます（要楽天ID、URLは適当なものでOK）。
※スプレッドシート上にISBN直打ちでも動作するように**ID**と**CreateAt**は変更しています。
※ISBNの重複は避けたいので、新規登録時に重複していた場合はエラーメッセージを表示させ、該当ISBNの行を削除しています。

以下コード

:::note warn
GASは始めたばかりなので、コード見ずらい部分あるかと思いますが、ご容赦ください。
:::

```javascript

/* 2022-04-22 楽天で画像見つからなかったときにGoogleのAPIを取ってこなかったバグ修正、
              Googleでも画像が見つからなかった時の処理追加 
              重複の際のデーター削除時、
　　　　　　　 削除列の一つ下のセルがアクティブな状態で止まっていたのを、
　　　　　　　 削除列のセルでアクティブになるよう変更
　　　　　　　 それに伴い、スプレットシートIDの記述削除 */
/* 2022-05-03 著作者情報がない書籍の場合エラーになっていたのを修正 */
/* 2022-05-03 漫画において巻数が表示されなかったため表示されるよう変更(OpenBDのみ対応) */
/* 2022-06-10 最終の画像としてAmazonを採用、OpenBD以外でCreateAtが正しく表示されないバグ修正、
　　　　　　　 古い書籍でISBNが10桁のものしかない場合にも対応 */
/* 2023-05-25 googleにも画像がないかつ著者がいない場合エラーとなっていたのを修正 */
/* 2024-04-30 googleでの書影が正しく表示されなくなってしまったため、googleの書影の場合は全てAmazonの書影とするよう変更(対象個所は元に戻せるようにコメントアウトにて修正) */
/* 2024-08-30 ISBNが10桁の場合の対応の実装漏れにつき修正 */
/* 2024-09-27 ISBNが10桁の場合の正しく実行できないバグ修正 */

const rakuten_appid= /*楽天のアプリIDを記載*/

function fetchBookSummary_openbd(isbn) {
  const url = 'https://api.openbd.jp/v1/get?isbn=' + isbn;
  const res = UrlFetchApp.fetch(url,{muteHttpExceptions: true});
  const json = JSON.parse(res.getContentText());
  if (!json[0]) return null;
  return json[0];
}

function fetchBookSummary_rakuten(isbn) {
  const url = 'https://app.rakuten.co.jp/services/api/BooksBook/Search/20170404?format=json&isbn=' + isbn+ '&applicationId=' + rakuten_appid ;
  const res = UrlFetchApp.fetch(url,{muteHttpExceptions: true});
  const json = JSON.parse(res.getContentText());
  if(json["totalItems"] == 0) return null;
  if(json["Items"].length == 0) return null;
  return json["Items"][0];
}

function fetchBookSummary_google(isbn) {
  const url = 'https://www.googleapis.com/books/v1/volumes?q=isbn:' + isbn + "&country=JP";
  const res = UrlFetchApp.fetch(url,{muteHttpExceptions: true});
  const json = JSON.parse(res.getContentText());
  if(json["totalItems"] == 0) return null;
  if(json["items"].length == 0) return null;
  return json["items"][0];
}

function getDate(strDate) {
  return new Date(strDate.slice(0, 4), strDate.slice(4, 6), strDate.slice(6, 8));
}

function getNowDate(){
  let d = new Date();
  return Utilities.formatDate(d, "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss");
}

const toISBN10 = (isbn13) => {
  // 1. 先頭３文字と末尾１文字を除く
  const src = isbn13.toString().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}`;
}

const toISBN13 = (isbn10) => {
  // 1. 先頭に`978`を足して、末尾の1桁を除く
  const src = `978${isbn10.toString().slice(0, 9)}`;

  // 2. 先頭の桁から順に1、3、1、3…を掛けて合計する
  const sum = src.split('').map(s => parseInt(s))
      .reduce((p, c, i) => p + ((i % 2 === 0) ? c : c * 3));

  // 3. 合計を10で割った余りを10から引く（※引き算の結果が10の時は0とする）
  const rem = 10 - sum % 10;
  const checkdigit = rem === 10 ? 0 : rem;

  // 1.の末尾に3.の値を添えて出来上がり
  return `${src}${checkdigit}`;
}

function onChangeSheet(e) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.getDataRange().getValues().forEach((row, i) => {
    let isbn = row[1], title = row[2];
    if (isbn && title) return; //すでに登録済みの場合は飛ばす
    let isbn_list = sheet.getRange(2,2,sheet.getLastRow()-1).getValues();
    let count=isbn_list.length;
    let cnt=0;
    if(isbn.length==10){
      isbn=toISBN13(isbn);
    }
    for(let j=0;j<count;j++){
        if(isbn == isbn_list[j][0]){
          cnt++;
        }
    }
    if(cnt>1){
      Browser.msgBox("エラー!!","ISBNが重複しています。", Browser.Buttons.OK);
      const lrow = sheet.getLastRow();
      sheet.deleteRow(lrow);
      sheet.getRange(lrow,2).activate();
      return;
    }
    var now = getNowDate();
    //OpenBD
    let bookinfo_openbd = fetchBookSummary_openbd(isbn);
    if(bookinfo_openbd){
      let bookinfo_openbd_sm = bookinfo_openbd.summary;
      
      let bookinfo_openbd_sb=bookinfo_openbd.onix.DescriptiveDetail.TitleDetail.TitleElement["PartNumber"];
      if(bookinfo_openbd_sb){
        bookinfo_openbd_sm.title = bookinfo_openbd_sm.title + " " + bookinfo_openbd_sb;
      }
      if(bookinfo_openbd_sm ){
        //著者がいる場合
        if(bookinfo_openbd_sm.author){
          if(bookinfo_openbd_sm.cover){
            sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_openbd_sm.title, bookinfo_openbd_sm.publisher, getDate(bookinfo_openbd_sm.pubdate), bookinfo_openbd_sm.cover, bookinfo_openbd_sm.author, now]]);
          }else{
            //画像なかったらGoogleの画像を取得
            /*let bookinfo_google=fetchBookSummary_google(isbn);
            if(bookinfo_google["volumeInfo"]["imageLinks"]){
              sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_openbd_sm.title, bookinfo_openbd_sm.publisher, getDate(bookinfo_openbd_sm.pubdate), bookinfo_google["volumeInfo"]["imageLinks"].thumbnail, bookinfo_openbd_sm.author, now]]);
            }else{*/
              //Googleにも画像がない
              sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_openbd_sm.title, bookinfo_openbd_sm.publisher, getDate(bookinfo_openbd_sm.pubdate), "https://images-na.ssl-images-amazon.com/images/P/"+toISBN10(isbn)+".09.LZZZZZZZ.jpg", bookinfo_openbd_sm.author, now]]);
            //}
          }
        }else{
          //著者がいない
          let bookinfo_google=fetchBookSummary_google(isbn);
          if(bookinfo_openbd_sm.cover){
            sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_openbd_sm.title, bookinfo_openbd_sm.publisher, getDate(bookinfo_openbd_sm.pubdate), bookinfo_openbd_sm.cover, bookinfo_google["volumeInfo"]["authors"].join(","), now]]);
          }else{
            
            /*if(bookinfo_google["volumeInfo"]["imageLinks"]){
              sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_openbd_sm.title, bookinfo_openbd_sm.publisher, getDate(bookinfo_openbd_sm.pubdate), bookinfo_google["volumeInfo"]["imageLinks"].thumbnail, bookinfo_google["volumeInfo"]["authors"].join(","), now]]);
            }else{*/
              //Googleにも画像がない
              sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_openbd_sm.title, bookinfo_openbd_sm.publisher, getDate(bookinfo_openbd_sm.pubdate), "https://images-na.ssl-images-amazon.com/images/P/"+toISBN10(isbn)+".09.LZZZZZZZ.jpg", bookinfo_google["volumeInfo"]["authors"].join(","), now]]);
            //}
          }
        }
      }
    }else{
      //楽天
      let bookinfo_rakuten = fetchBookSummary_rakuten(isbn);
      if(bookinfo_rakuten){
        bookinfo_rakuten = bookinfo_rakuten["Item"];
        let bookinfo_google=fetchBookSummary_google(isbn);
        let publishedDate
        if(bookinfo_google){
           publishedDate=bookinfo_google["volumeInfo"].publishedDate
        }else{
           publishedDate=bookinfo_rakuten.salesDate
        }
        if(bookinfo_rakuten.largeImageUrl){
          sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_rakuten.title, bookinfo_rakuten.publisherName, publishedDate, bookinfo_rakuten.largeImageUrl, bookinfo_rakuten.author, now]]);
        }else{
          //画像なかったらGoogleの画像を取得
          /*if(bookinfo_google["volumeInfo"]["imageLinks"]){
          sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_rakuten.title, bookinfo_rakuten.publisherName, publishedDate, bookinfo_google["volumeInfo"]["imageLinks"].thumbnail, bookinfo_rakuten.author, now]]);
          }else{*/
            //Googleにも画像がない
            sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_rakuten.title, bookinfo_rakuten.publisherName, publishedDate, "https://images-na.ssl-images-amazon.com/images/P/"+toISBN10(isbn)+".09.LZZZZZZZ.jpg", bookinfo_rakuten.author, now]]); 
          //}
        }
      }else{
        //Google
        let bookinfo_google=fetchBookSummary_google(isbn);
        if(bookinfo_google){
          /*if(bookinfo_google["volumeInfo"]["imageLinks"]){
            sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_google["volumeInfo"].title, (bookinfo_google["volumeInfo"].publisher)?bookinfo_google["volumeInfo"].publisher:"", bookinfo_google["volumeInfo"].publishedDate, bookinfo_google["volumeInfo"]["imageLinks"].thumbnail,  bookinfo_google["volumeInfo"]["authors"].join(","), now]]); 
          }else{*/
            //Googleにも画像がない
            //著者がいる
            if(bookinfo_google["volumeInfo"]["authors"]){
            sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_google["volumeInfo"].title, (bookinfo_google["volumeInfo"].publisher)?bookinfo_google["volumeInfo"].publisher:"", bookinfo_google["volumeInfo"].publishedDate, "https://images-na.ssl-images-amazon.com/images/P/"+toISBN10(isbn)+".09.LZZZZZZZ.jpg",  bookinfo_google["volumeInfo"]["authors"].join(","), now]]); 
          }else{
            //著者がいない
            sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, bookinfo_google["volumeInfo"].title, (bookinfo_google["volumeInfo"].publisher)?bookinfo_google["volumeInfo"].publisher:"", bookinfo_google["volumeInfo"].publishedDate, "https://images-na.ssl-images-amazon.com/images/P/"+toISBN10(isbn)+".09.LZZZZZZZ.jpg",  "", now]]); 
          }
          //}
        }else{
          //どこもヒットしない
          sheet.getRange(i + 1, 1, 1, row.length).setValues([[i, isbn, "", "", "", "", "", now]]);
        }
      }
    }
  });
}
```

# AppSheetについて
個人的にですが、IDの所は[元記事](https://qiita.com/takatama/items/fa141cb13ccc7324f887)では **UNIQUEID()** を指定していましたが、スプレットシート上でもわかりやすく見れるように **[_RowNumber]** に変更しています。
※それに伴い、TypeもNumberに変更しています。
ISBNの重複を避けるためにISBNのKEYにチェックを入れています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/855481/c0292d74-2f96-67c1-25d8-469fa4c9db41.png)

# 最後に
あとは[元記事](https://qiita.com/takatama/items/fa141cb13ccc7324f887)と同じように動作検証していけば完了となります。

[元記事](https://qiita.com/takatama/items/fa141cb13ccc7324f887)をまねていざ作ってみたはいいもののOpen BDでは意外とヒットしない
ことがあったため他のAPIも使えばいいんじゃないかという考えのもと作成してみました。
GASやJSONの勉強にもなり面白かったです。

書籍も色々あり、APIに登録されている形式も様々ですので、
現状のコードでは書籍によっては正しく登録されない可能性があります。
そのような場合、報告いただければ修正対応を行わせていただきますので、
コメント欄にてご連絡ください。
※対応にはお時間かかることもございますことご了承ください。

# 追記(2022/4/25)
すべてにおいて画像がない場合は、
Amazonの商品ページに載っている画像のアドレスを、
スプレッドシートにコピペすることで、
アプリ側にも画像が表示されるようになります。
※AmazonのAPIは色々と制約があるようなので、
 今回は採用見送っています。

# 追記(2022/6/10)
AmazonのAPIは制約があり採用を見送っていたのですが、
なんと、[画像だけであればISBNさえわかってしまえば取得可能](https://qiita.com/nyoroko/items/452a8db449862f31ab2b)
ということがわかりましたので、画像が見つからない場合、
最終的にAmazonを参照するようにしました。
なお、上記リンクにありますように、
Amazonを利用する際のISBNは10桁でなければならないので、
[こちら](https://qiita.com/iz-j/items/edbdf29065777f9518af)を流用させていただき、
ISBNの変換を行っています。
また、古い書籍の場合、ISBNが10桁のものしか記載がない場合がありますので、
それに対応するために、[逆バージョンの10桁→13桁](https://qiita.com/iz-j/items/27b9656ebed1a4516ee1)を流用しています。

# 追記(2022/10/19)

デフォルトの設定だと、アプリに表示されるのがタイトルとISBNのみで見づらかったため、
App→Primary Viewより下記画像の通りに設定し、タイトル、出版日、著者をアプリ上で表示するよう変更しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/855481/f906966b-9496-c3db-e47c-e3af0d26ad20.png)

# 追記(2023/9/25)

openDBのAPIの提供終了が発表されていました。
https://openbd.jp/news/20230725.html
一応従来通りに使用はできるようですが、収録範囲、収録内容に大幅な変更が見られ、
特に書影については収録範囲が大幅に低下するとのことでした。
現行ですでに作成済みの場合、OpenDBの書影が見えなくなっているかと思われますので、
スプレットシートのタイトルだけを削除してもらうと、
再度自動でデータ取得を行うので、こちらにて修正可能です。

:::note info
OpenDBの部分のコードを削除しなくても再度実行にて問題なく取得できましたので、
あえて削除していません。
気になる方は自分で削除してください。
:::