見出し画像

Notion及びPaperpileを用いた文献管理手法


はじめに

研究活動で最も重要なのが文献管理です。これまで私はMendeley,Zoteroを主な文献管理ソフトとして使用してきましたが,外出先からの文献へのアクセス及びマルチデバイス間でのメモ管理が困難という点において不満を抱いていました。先日Notionを用いた文献管理についての記事を拝読し,試しに導入して使用感を確認していました。

上の記事での方法は,Paperpileが生成したGoogle Drive上のPDFファイルのfile nameを元に,Google Apps Script (GAS)でNotion APIを通じて論文の情報をNotionに送るものです。この方法は一度スクリプトを動かせば常に自動同期が行われるという利点があるものの,以下の点において私が望むデータ構造の構築が困難でした。

  • NotionのプロパティがPaperpileのfile nameに依存する。

  • NotionからGoogle Driveへのファイルリンクが無い。

特に一点目が致命的で,私の分野の論文タイトルが非常に長い(およそ150文字)ことからfile nameによるプロパティ生成ではファイルの文字数制限により著者情報や論文誌等の情報を十分に送ることが出来ませんでした。

そこでこの記事では,Paperpileの有するメタデータをfile nameを介さずにNotionへ送り,Google Drive上のPDFファイルのリンクを置くことですべてのデバイスでNotionを経由して直接PDFファイルにアクセスできるようにする方法を解説します。

完成図

この方法の欠点は,PaperpileにAPIによるメタデータの出力が備わっていないため,メタデータのアップロードを手動で行う必要があることです。もしこの部分を自動化したい場合はSelenium等のブラウザ制御を外部から行う必要がありそうです。

データフロー

データフローを上図に示します。PaperpileからPDFの保存は自動で行われますが,メタデータはPaperpileのexporterを使って手動でGoogle Driveにダウンロードします。これらのデータはPaperpileのハッシュでGASによって管理され,Notionに自動的にデータが送られます。各デバイスからはNotionに置かれたリンクからドライブ上のPDFおよびwebのPDFに直接アクセス可能です。

実装方法

GASの設定及び実装コード

初めにGASでアプリケーションファイルを作成します。プロジェクト名は任意です (PaperNotionとしました)。

GASの設定画面でスクリプトプロパティを次のように設定します。

  1. FOLDER_ID:  Google Drive内PaperpileのPDFがあるフォルダID

  2. NOTION_DB_ID: NotionのデータベースID

  3. NOTION_TOKEN: NotionのAPI利用トークン

Google DriveのFOLDER_IDは該当フォルダのurl末尾です。このフォルダは共有設定から編集権限をリンクを知っている全員に与えておきます。また,NotionのデータベースID及びトークンに関しては以下の記事を参考に設定します。

続いて,コードエディタで以下のコードを記述します。

const PROPS = PropertiesService.getScriptProperties();
const FOLDER_ID = PROPS.getProperty('FOLDER_ID');
const DB_ID = PROPS.getProperty('NOTION_DB_ID');
const TOKEN = PROPS.getProperty('NOTION_TOKEN');
const HASH_LIST_FILE_NAME = 'hash_list.txt';

////////////////////// ドライブ上のHASHリスト管理関数 ////////////////////
function getOrCreateHashListFile() {
  const folder = DriveApp.getFolderById(FOLDER_ID);
  let fileIterator = folder.getFilesByName(HASH_LIST_FILE_NAME);

  if (fileIterator.hasNext()) {
    // ファイルが既に存在する場合はそのファイルを返す
    return fileIterator.next();
  } else {
    // ファイルが存在しない場合は新しいファイルを作成
    const file = DriveApp.createFile(HASH_LIST_FILE_NAME, '');
    file.moveTo(folder);
    return file;
  }
}


function getHashList() {
  const file = getOrCreateHashListFile();
  const hashListContent = file.getBlob().getDataAsString();
  const hashList = hashListContent.split('\n').filter(dup_sha1 => dup_sha1);
  return hashList;
}

function updateHashList(newHashs) {
  const file = getOrCreateHashListFile();
  let existingContent = file.getBlob().getDataAsString();
  // 既存の内容が空でなければ改行を追加
  existingContent = existingContent ? existingContent + '\n' : '';
  const newContent = existingContent + newHashs.join('\n');
  file.setContent(newContent.trim());
}
/////////////////////////////////////////////////////////////////////////

//////////////////////// JSON読み込み関数 ////////////////////////////////
function readLatestPaperpileExport() {
  const folder = DriveApp.getFolderById(FOLDER_ID);
  const filesIterator = folder.getFiles();
  let latestFile = null;
  let latestDate = null;

  while (filesIterator.hasNext()) {
    let file = filesIterator.next();
    let fileName = file.getName();
    // ファイル名が"Paperpile - [日付] JSON Export.txt"のパターンに一致するか確認
    if (fileName.startsWith("Paperpile - ") && fileName.endsWith(" JSON Export.txt")) {
      // 日付部分を抽出
      let fileDateStr = fileName.substring(11, fileName.length - " JSON Export.txt".length);
      let fileDate = new Date(fileDateStr);
      if (!latestDate || fileDate > latestDate) {
        latestDate = fileDate;
        latestFile = file;
      }
    }
  }

  if (latestFile) {
    const content = latestFile.getBlob().getDataAsString();
    //console.log(content);
    return content;
  } else {
    console.log('最新のファイルが見つかりませんでした。');
    return null;
  }
}

//////////////////////////////////////////////////////////////////////////

////////////////////////////////// Notion API ////////////////////////////
function send2Notion(jsonContent) {
  const jsonData = JSON.parse(jsonContent);
  const existingHashs = getHashList();
  const newHashs = [];
  const apiUrl = 'https://api.notion.com/v1/pages';

  // 各論文データに対してNotion APIを呼び出す
  jsonData.forEach(paper => {
    if(!existingHashs.includes(paper.dup_sha1)) {
      if (paper.attachments && paper.attachments.length > 0 && paper.attachments[0].gdrive_id) {
        const googleDriveId = paper.attachments[0].gdrive_id;
        const googleDriveUrl = `https://drive.google.com/file/d/${googleDriveId}/view`;
        const title = paper.title;
        const authors = paper.author.map(a => a.formatted);
        let authorList = [];
        if (authors.length > 0) {
          authorList.push({ "name": authors[0] }); // 最初の著者
          if (authors.length > 2) {
            authorList.push({ "name": authors[authors.length - 2] }); // 中間の著者
          }
            if (authors.length > 1) {
              authorList.push({ "name": authors[authors.length - 1] }); // 最後の著者
            }
        }
        const doi = paper.doi;
        const url = paper.url[0];
        let journal;
          if(paper.journal_checked == 1){
            journal = paper.journal
          }else if(paper.booktitle){
            journal = paper.booktitle
          }else(
            journal = 'Others'
          )
        const thumbnailURL = getThumbnailUrl(googleDriveId);
        let abstract;
          if(paper.abstract && paper.abstract.length > 0){
            abstract = paper.abstract;
          } else {
            abstract = null;
        }
        let year;
          if (paper.published && paper.published.year) {
            year = parseInt(paper.published.year, 10); 
          } else {
          year = null; // または適切なデフォルト値を設定
        }


        const payload = {
          parent: { database_id: DB_ID,
          },
          cover: { 
            "type": "external",
            "external": {
            "url": thumbnailURL
            }
          },

          properties: {// Notionデータベースのタイトルプロパティ名に合わせて修正
            "Title": {
              title: [{ text: { content: title } }]
            },
            "Author": {
              "multi_select": authorList
            },
            "Year": {
              number: year
            },
            "Journal": {
              "select": {
                "name": journal
              }
            },
            "File": {
              url: googleDriveUrl
            },
            "URL": {
              url: url
            },
            "Abstract": {
              "rich_text": [{ "text": { "content": abstract || "No abstract provided" } }]
            },
          }
        };
        
        const options = {
          method: 'post',
          headers: {
            'Authorization': `Bearer ${TOKEN}`,
            'Content-Type': 'application/json',
            'Notion-Version': '2021-08-16'
          },
          payload: JSON.stringify(payload)
        };

        try {
          const response = UrlFetchApp.fetch(apiUrl, options);
          Logger.log(response.getContentText());
          newHashs.push(paper.dup_sha1); // 成功したら新しいハッシュをリストに追加
          } catch (e) {
          Logger.log(`Error sending paper: ${paper.title}, Error: ${e}`);
          }

          Utilities.sleep(300); // 0.3秒ウェイト
      }else{
        Logger.log(`Skipping paper: ${paper.title} because it has no Google Drive ID.`);
        return; // Continue to next iteration in forEach loop
      }
    }
  });

  if (newHashs.length > 0) {
    updateHashList(newHashs); // DOIリストを更新
  }

}
////////////////////////////////////////////////////////////////////

///////////////// Google Drive上のファイルサムネイル取得 ////////////
function getThumbnailUrl(fileId, width=1600, authuser=0){
  return `https://lh3.googleusercontent.com/d/${fileId}=w${width}?authuser=${authuser}`;
}
////////////////////////////////////////////////////////////////////

function myFunction() {
  const content = readLatestPaperpileExport(); 
  if (content) {
    send2Notion(content); // JSON文字列を渡す
  } else {
    console.log('JSONデータを取得できませんでした。');
  }
}

Notionへのデータ送信はsend2Notion()内で行われます。データ転送にはNotionのPropertyとこの関数内のProperty設定が整合していることが必要です。そこで,NotionのPropertyを次のように設定します。

  1. Title - text

  2. Author - multi_select

  3. Year - number

  4. Journal - select

  5. File - url

  6. URL - url

  7. Abstract - rich-text

以上で設定は終わりです。Paperpileからエクスポート機能で出力したJsonファイルをGoogle Drive上のフォルダにアップロードします。

Paperpileのエクスポート画面

GASのコードを実行して,Notion側に反映されれば成功です。Notion上のFile PropertyはGoogle Drive上のPDFにリンクされており,直接コメントの編集が可能です。また,コード中Send2Notion()でJSONファイルの読み込みとNotionへの書き込みを行っているので,この部分を変更することで任意のメタデータのやり取りが可能です。

コード実行の結果が確認できれば,GASのトリガー機能を使って一定時間ごとにコード実行するように設定します。これで,JSONファイルをアップロードすればいつでもNotionにそのデータが反映されるようになります。

まとめ

NotionとPaperpileを用いた文献管理方法について紹介しました。外出先で論文を読むことが多かったため、メモやコメントを直接Google DriveのPDFに保存できるのは便利だと思います。IpadのペンシルでDriveのPDFに直接コメントを書き込むことができればより良いのですが…文献管理ソフトは痒いところに手が届かないので、自分で環境を整えることが必要ですね。

この記事が気に入ったらサポートをしてみませんか?