VRChatのワールドにGooglePhotosと連携するフォトフレームを追加する

# 何ができるの？
VRChatのワールドに置いたフォトフレームに、Google Photosで指定したアルバム内の画像を表示できるようになります。

仕組みとしてはこちら（https://qiita.com/nyakome306/items/bab82e741dd66053b374 ）を基に、VRCPanoramaでGoogle Drive上の画像を参照・表示し、Google Apps Scriptで定期的にGoogle Photosの画像を取得、Driveの画像を更新する形となります。



この記事関係なくGoogleの「バックアップと同期」をPCにインストールしておくと
撮ったSSが自動でGoogle PhotosにアップロードされてスマホからSSを管理できて捗るのでお勧め。

この記事では、一定時間で表示されるSSが切り替わるフォトフレームの実装について紹介します。

# Google Apps Script側
一部箇所の説明を省いてますので、こちらも参照しながら設定してください
https://qiita.com/nyakome306/items/bab82e741dd66053b374
### スクリプトを新規作成
以下のURLにアクセスし、左上の新規スクリプトをクリックするとエディタが開きます
https://script.google.com
### Google Cloud Platform(GCP)プロジェクトと連結
以下のURLにアクセスし、左上の▼をクリックして新しいプロジェクトを作成します。
https://console.cloud.google.com
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176380/d8c0c87e-a194-1b18-92a5-5637de27b6a6.png)

プロジェクトのダッシュボードを表示し、左側にあるプロジェクトIDを控えておきます。
先ほど作成したスクリプトのエディタ画面よりリソース→Cloud Platformプロジェクトを選択、プロジェクトIDを入力して”プロジェクトを設定”をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176380/d3cdd546-c0e8-7627-74b2-303fe0e94c14.png)

### Photos Library APIキーを取得
GCPのプロジェクトのダッシュボードの上側の検索窓にPhotos Library APIと入力して検索し、画像の通りにクリックしていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176380/586fecde-66c1-ad32-2d1d-c8aea6ad16c8.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176380/44594246-52f9-cfd5-2091-f14768031a7d.png)
名前はそのままでも大丈夫です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176380/9aded11a-9d77-0e18-2827-7f21b907a8a7.png)
表示されたクライアントID、クライアントシークレットを控えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176380/2ebdbae2-1e3a-db81-5232-7b56c30d7ade.png)



### コード本体
以下コードを新規作成したスクリプトに貼り付けてください。
またクライアントIDとクライアントシークレットを上で入手したものに書き換え、保存ボタン💾をクリックしてください。
表示する写真の枚数はphotoNumで指定できます。

表示するアルバムの選択は、photoalbum()を実行すると表示→ログにアルバム名とアルバムIDが出力されるため、表示したいアルバムのIDをコピーし、スクリプトのalbumidを書き換えてください。

```javascript:qiita.js
var clientid = "クライアントID";
var clientsecret = "クライアントシークレット";

//表示枚数
var photoNum = 10;

//表示するアルバムID
var albumid = "表示したいアルバムのID";


var tokenurl = "https://accounts.google.com/o/oauth2/token"
var authurl = "https://accounts.google.com/o/oauth2/auth"
var scope = "https://www.googleapis.com/auth/photoslibrary"
 

//アクセストークンURLを含んだHTMLを返す関数
function authpage(){
  var service = checkOAuth();
  var authorizationUrl = service.getAuthorizationUrl();
  Logger.log(authorizationUrl);
}
 
//認証チェック
function checkOAuth() {
  return OAuth2.createService("PhotosAPI")
    .setAuthorizationBaseUrl(authurl)
    .setTokenUrl(tokenurl)
    .setClientId(clientid)
    .setClientSecret(clientsecret)
    .setCallbackFunction("authCallback")　//認証を受けたら受け取る関数を指定する
    .setPropertyStore(PropertiesService.getScriptProperties())  //スクリプトプロパティに保存する
    .setScope(scope)
    .setParam('login_hint', Session.getActiveUser().getEmail())
    .setParam('access_type', 'offline')
    .setParam('approval_prompt', 'force');
}
 
//認証コールバック
function authCallback(request) {
  var service = checkOAuth();
  Logger.log(request);
  var isAuthorized = service.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput("認証に成功しました。ページを閉じてください。");
  } else {
    return HtmlService.createHtmlOutput("認証に失敗しました。");
  }
}
 
//ログアウト
function reset() {
  checkOAuth().reset();
}
//アルバム一覧を取得する
function photoalbum(){
  var service = checkOAuth();
  
  var accessToken = service.getAccessToken();
  
  var url = "https://photoslibrary.googleapis.com/v1/albums";
  var response = UrlFetchApp.fetch(url, {
    method: 'GET',
    headers: {
      Authorization: 'Bearer ' + accessToken
    },
    contentType: "application/json",
    muteHttpExceptions: true
  });
  //一覧データを取得する
  var result = JSON.parse(response.getContentText())["albums"];
  for(i=0;i<result.length;i++){
    Logger.log(result[i]["title"]+","+result[i]["coverPhotoMediaItemId"]);
  }
}



//アルバム内の画像一覧取得
function albumPhotolist(albumId){
  var service = checkOAuth();
 
  var accessToken = service.getAccessToken();
 
  //アルバムIDの指定など
  var pageSize = photoNum + 10;
  var payload = {
      "pageSize":pageSize.toString(),
      "albumId": albumId,
  };
 
  
  var url = "https://photoslibrary.googleapis.com/v1/mediaItems:search";
  var response = UrlFetchApp.fetch(url, {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + accessToken
    },
    contentType: "application/json",
    payload : JSON.stringify(payload),
    muteHttpExceptions: true
  });
  
  //画像Urlを取得
  var result = JSON.parse(response.getContentText())["mediaItems"];
  var photolist = result.filter(function(v) {return v.mimeType == "image/png" || v.mimeType == "image/jpeg"});
  for(i=0;i<photolist.length;i++){
    Logger.log(photolist[i].baseUrl);
  }
  return photolist;
}

//初期化
function photoFilesInit(){
  
  var albumFolders=DriveApp.getFoldersByName("VRCPhotoAlbum");
  var albumFolder;
  if(!albumFolders.hasNext()){
    albumFolder = DriveApp.createFolder("VRCPhotoAlbum");
  }else{
    albumFolder = albumFolders.next();
  }
  setProp("folderId",albumFolder.getId());
  var strImURL = "Image URL list";
  for(i=0;i<photoNum;i++){
    var photo;
    var photos = DriveApp.getFilesByName("photo"+i+".png");
    if(!photos.hasNext()){
      photo = albumFolder.createFile("photo"+i+".png", "", MimeType.PNG)
    }else{
      photo = photos.next();
    }
    var access = photo.getSharingAccess()    
    //公開範囲の設定
    if (access != DriveApp.Access.ANYONE) photo.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);
    strImURL += "\nhttp://drive.google.com/uc?export=view&id="+photo.getId();
    setProp("photo"+i+"Id",photo.getId());
    
  }
  Logger.log(strImURL);
}

//定期実行関数
function updatePhotoFiles(){

  //albumIdに指定したアルバムを表示する
　var photoUrls = albumPhotolist();
  for(i=0;i<photoNum;i++){
    var blob = UrlFetchApp.fetch(photoUrls[i]["baseUrl"]+"=d").getBlob();
    updateFile("photo"+i+".png",getProp("photo"+i+"Id"),blob);
    
  }
}

function setProp(key,val){
  PropertiesService.getScriptProperties().setProperty(key,val);
}
function getProp(key){
  var value = PropertiesService.getScriptProperties().getProperty(key);
  return value;
}

function updateFile(title,id,blob) {
  var body = {
    title: title,
    mimeType: 'image/png'
  };
  file = Drive.Files.update(body,id, blob);
}




```

### API有効化
Drive APIを用いるため、リソース→Googleの拡張サービスより選択して有効化してください。
GASエディタ上の関数の選択よりauthpageを選択して実行▶ボタンを押してください。
表示→ログを開き、表示されているURLにアクセスしてください。
Googleアカウントのログイン画面が出るのでログインし、詳細→photos library(安全でないページに移動）→許可とクリックしてください。
### 同期ファイル生成、動作テスト
GASエディタ上の関数の選択よりphotoFilesInitを選択して実行▶ボタンを押してください。VRCPanoramaより参照されるファイルがGoogleDrive上に作成されます。（VRCPhotoAlbumフォルダ）
またupdatePhotoFilesを実行するとVRCPhotoAlbumフォルダ内の画像が指定したアルバムのものに更新されます。
### 定期実行
updatePhotoFiles()を編集→現在のプロジェクトのトリガーからトリガーを作成して定期実行するようにしてください。


# VRChat側
### パッケージの導入
以下の.unitypackageを導入してphotoFrame.prefabをワールドに置いてください。
https://drive.google.com/open?id=1rQ5jNQfXfhINcihF7A1M3TAev2JUORy_

### URLの登録
GASエディタ上の関数の選択よりphotoFilesInitを選択して実行▶ボタンを押してからログを開くと、VRCPanoramaより参照される画像ファイルのURLが一覧で表示されます。
![コメント 2020-02-15 153832.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176380/784572a2-aebd-9b1c-964f-9a7af54d4fbb.jpeg)


設置したphotoFrame→screen内のVRCPanoramaコンポーネントのsizeを上のphotoNumで指定した値（初期値10）にし、追加されたフォームにURLを順に入力してください。
![コメント 2020-02-15 152850.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176380/53c51f8d-2d0d-71c8-fa3a-2f76367dc40a.jpeg)
Unityの再生ボタンを押して、screenにSSが表示されれば正しく導入されています。