2019年12月5日

Google Apps Script 操作 Google Photo API 上傳圖片實作心得

Google Apps Script 操作 Google Photo API 上傳圖片實作心得

Wayne Fu 0 A+
google-apps-script-photo-api-upload.jpg-Google Apps Script 操作 Google Photo API 上傳圖片實作心得自從「Google 關閉 Picasa API」之後,比較適合拿來存放圖片的免費空間變成「Google 相簿」。不過Google 為了不想被當成免費的外連圖床,Google Photo API 有不少限制,可參考這篇新聞「Google相片庫API正式上線,讓開發者在自家應用加入相簿功能」,裡面有提到:

Google特別提醒了開發者,在呼叫照片內容列表之後,應用程式應該儲存媒體檔案的ID,而非回傳的檔案本身,因為媒體檔案內容可能會有改變,並且在一定時間之後,回應的內容包括URL會過期

意思就是 API 取得的圖片外連沒有用處,一段時間後就會失效,這件事在「讓 Google Photo 實現相簿畫廊效果」也有提到。

所以 Google Photo API 主要的實質用途,在於上傳、搜尋圖片,至少可以把其他地方的圖片搬過來存放,不需要付費且嚴格來說沒有容量限制

但必須說 Google Photo API 很不好操作,之前處理「Gmail API」時已確立是最困難的 API 不會被超越,而此次使用 Google Photo API 也被整得灰頭土臉,榮登第二名的地位。本篇將說明在 Google Apps Script(簡稱 GAS)環境操作 API,如何上傳圖片到指定相簿的流程。



一、完整流程


一開始可先參考官網文件,瞭解 API 做些什麼事、如何開始:



都是英文可能很吃力,大致說明整個流程要做哪些事:

  • 取得 OAuth 驗證:
    • 啟用 API、取得 Client ID、密鑰
    • 申請永久 token,將來才不用每次重新驗證
  • 建立相簿:
    • 這一步非常重要,也是遇到的第一個大坑
    • 所有在「Google 相簿」官網上建立的相簿都無法用 API 上傳圖片
    • 只有用 API 建立的相簿才能上傳圖片
  • 上傳圖片:
    • 這是第二個坑,Google Photo 很不乾脆,不知為何要分兩個動作,上傳圖片後只能拿到一個 token
    • 接著要拿 token 在相簿裡面建立圖片物件,才能完成上傳
    • 容易掉坑的地方在於,不管上傳什麼都會拿到 token,即使圖片格式不對這一步都不會報錯。要等到最後一步「建立圖片物件」失敗時,讓你找不出究竟整個過程哪個動作錯誤,才導致無法成功建立物件。
    • 也許官方背後的意涵是,只要搞得越複雜,Google Photo 被拿來濫用的機率就越低
  • 建立圖片物件:最後一個坑,也是搞最久的一個地方,後面範例程式碼再說明



二、取得 OAuth 2.0 金鑰


OAuth 驗證的流程很長,包括「啟用 API、取得 Client ID、密鑰(Client Secret)、永久金鑰(refresh token)」,但取得 OAuth 並非本篇重點,為避免篇幅太長這部分將會簡單帶過,請參考以下參考資料,重點在於想辦法取得可永久使用的 refresh token:


如果是前端工程師的話,我相信有辦法可以搞定 OAuth、refresh token。真的沒辦法的話,有此需求的讀者請再留言反應,視需求程度再來寫 Google API 處理 Oauth 的通用流程。



三、建立相簿


前面說過了,必須使用 API 建立的相簿才能上傳圖片,所以首先請學習如何操作 API 建立相簿。參考官方文件:


此頁面有語法範例,也提醒前面走 OAuth 驗證時 scope 要至少有 photoslibrary 才有寫入的權限。

非常棒的是,這個頁面就有控制台可以操作測試 API,往下捲就會看到:

google-apps-script-photo-api-upload-1.jpg-Google Apps Script 操作 Google Photo API 上傳圖片實作心得

  • A:在這裡設定好相簿標題就好
  • B:按下 EXECUTE 執行 API
  • C:這裡可看到 API 範例語法,將來照抄很方便
  • D:執行成功的話,建立相簿後紅色底線這裡會產生「相簿 ID」,將來上傳圖片需要用到此 ID
  • 可注意最下方紅框,isWriteable: true 代表這個相簿可寫入,才能上傳圖片

但是別被騙了,這裡是一個大坑,此處產生的「相簿 ID」一點作用都沒有,根本無法上傳圖片,原因在於這個相簿不是由我們的 APP 所產生,而是由控制台產生,所以我們沒有寫入的權限

所以之後得乖乖自己寫 code 來建立相簿,這樣產生的相簿 ID 才能用於上傳圖片。不過補充說明,把上圖「Google OAuth 2.0」、「API Key」都取消勾選,打開「Show standard parameters」,填入我們自己的 access_token,這樣就是由我們自己 APP 所建立的相簿,可上傳圖片。



四、上傳圖片


參考官方文件:



這裡詳細說明前面提到的「兩階段上傳」流程,以及語法範例、注意事項。大概整理一下要點:

  • 一次最多可上傳 50 圖,一張圖片最大 50MB,一個相簿最多裝 20000 張圖
  • 上傳的圖片格式必須為「raw bytes」
  • 依照 API 格式上傳完畢後,會返回一個字串「upload-token」,要記住這個字串
  • 依照 API 第二階段的格式,需要設定「相簿 ID」、輸入「upload-token」,有幸成功的話,這張圖片就會出現在相簿裡了
  • API 第二階段有個參數「albumPosition」可設定上傳圖片的位置,例如強制擺在相簿最前面,但我怎麼做都是出現在最後面,所以我認為這參數是裝飾用的。



五、建立圖片物件


官方很貼心的提供了控制台可操作,下面這個頁面捲到底就會看到:


但其實這功能只做了一半,因為只能跑第二階段的功能,也就是你要先自己寫 code 上傳完圖片,取得「upload-token」後,才能來這裡測試建立圖片物件的功能,挖勒...

總之官網的文件寫的很詳細,但沒有完整的實例,控制台測試功能也不完整。想要真正測試圖片上傳功能只能真的寫出 code 來才有辦法,那麼就直接來看範例程式碼吧。



六、GAS 範例程式碼


因為是在 GAS 環境操作,如果 GAS 不夠熟的話,要跳的坑就更多了,所以先看最大的坑怎麼解再看程式碼。

1. GAS 圖片的 raw bytes

從別處圖床搬到 Google Photo,可用 GAS 的指令 UrlFetchApp.fetch 爬圖片網址。爬回來後要如何轉換格式是一個大問題,究竟要丟什麼格式 Google Photo API 才吃只能不斷測試:

  • UrlFetchApp.fetch(imgUrl).getContentText() → 這是文字資料,不可以
  • UrlFetchApp.fetch(imgUrl).getContent() → 官方說明書說可返回 raw binary content → 實測 ok
  • UrlFetchApp.fetch(imgUrl).getBlob().getBytes() → 先取 blob 物件再轉 byte 格式 → 實測 ok


2. GAS 的 payload 格式

一般來說送出 post 的 request body,需要將物件用 JSON.stringify() 轉成字串。但實際使用 GAS 的 UrlFetchApp.fetch 送出 post 時,卻跟 Google Photo API 一直犯衝。

為了排錯,使用官方提供的同樣範例語法,在控制台操作可成功上傳圖片,特地改用前端的 jQuery Ajax 送出也都沒問題,但偏偏 GAS 送出就會報錯。

怎麼找到解法的過程、或原理就先略過了,總之 GAS 的 payload 格式不能用 JSON.stringify() 把 request body 轉換成字串,要直接手動作出 request body 的字串格式,才能被 Google Photo API 吃進去,來跳過這最後一個坑。


3. 範例程式碼

var photo_client_id = "xxxxxxxxxxxxxxxxxxxxx", // 填入自己的 Client ID
photo_client_secret = "xxxxxxxxxxxxxxxxxxxxx", // 填入自己的 Client Secret
photo_refresh_token = "xxxxxxxxxxxxxxxxxxxxx", // 填入自己的 refresh_token
albumId = "xxxxxxxxxxxxxxxxxxxxx", // 填入自己的相簿 ID
albumTitle = "WFU BLOG 測試", // 填入相簿描述
fileName = "WFUBLOG.jpg", // 填入圖片檔名
access_token = getAccessToken();

function uploadPhoto() {
var src = "https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizW3s3HghEoobUR5juPl6QpOV-zZR5MQDsHGRlCmAcgGUX7jql6Q19E4Lsd8cBRQeUum9Ltx5luiSMFm1A6s3TzgtH3syX2aDjALB5IaClMtIEd56978kbz60PdGJGjelw-Rk9q2SpP5yG/s200/wfublog-logo-8abeb7.png", // 這裡是上傳的圖片網址
uploadToken = getUploadToken(src),
uploadUrl = "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate",
data = '{"albumId":"' + albumId + '","newMediaItems": [{"description": "test","simpleMediaItem": {"fileName":"' + fileName + '","uploadToken":"' + uploadToken + '"}}], "albumPosition":{"position": "FIRST_IN_ALBUM"}}', // 若用 JSON.stringify(data) → google photo api 吃不到,自行轉成字串最保險
options = {
method: "post",
headers: {
"Authorization": "Bearer " + access_token,
"Content-Type": "application/json"
},
payload: data,
muteHttpExceptions: true
},
response = UrlFetchApp.fetch(uploadUrl, options);
Logger.log(response);
}

function getUploadToken(src) {
var rawData = getPhotoRaw(src),
options = {
"method": "post",
"headers": {
"Authorization": "Bearer " + access_token,
"Content-type": "application/octet-stream",
"X-Goog-Upload-File-Name": fileName,
"X-Goog-Upload-Protocol": "raw"
},
"payload": rawData
},
uploadRawUrl = "https://photoslibrary.googleapis.com/v1/uploads",
uploadToken = UrlFetchApp.fetch(uploadRawUrl, options);
return uploadToken;
}

function getPhotoRaw(src) {
var response = UrlFetchApp.fetch(src);
return response.getContent(); // 圖片轉成 raw binary 格式
}

function createAlbum() {
var album = '{album: {title:"' + albumTitle + '"}}', // 若用 JSON.stringify(data) → google photo api 吃不到,自行轉成字串最保險
url = "https://photoslibrary.googleapis.com/v1/albums",
options = {
"method": "post",
"headers": {
"Authorization": "Bearer " + access_token,
"Content-Type": "application/json"
},
"payload": JSON.stringify(album)
},
response = UrlFetchApp.fetch(url, options);
Logger.log(JSON.stringify(response)); // 取得相簿 ID
}

function getAccessToken() {
var options = {
method: "post"
},
fetchUrl = "https://www.googleapis.com/oauth2/v3/token?client_id=" + photo_client_id + "&client_secret=" + photo_client_secret + "&refresh_token=" + photo_refresh_token + "&grant_type=refresh_token",
response = UrlFetchApp.fetch(fetchUrl, options),
results = JSON.parse(response.getContentText()),
access_token = results.access_token;
return access_token;
}

所有重點已用註解標示,以下依流程重點說明:
  • 先填入自己的 photo_client_id、photo_client_secret、photo_refresh_token,以及 albumTitle、fileName
  • 接著執行 createAlbum() 建立相簿,記下返回的相簿 ID,填入 albumId
  • 在 uploadPhoto 裡面可替換要上傳的圖片網址,接著執行 uploadPhoto() 就可上傳圖片到指定相簿


更多 Google Apps Script 相關技巧:


更多 Google Photo 相關文章:
0 0
如這篇文章對你有幫助,歡迎「分享」到 FB、「追蹤」粉絲團、「訂閱」最新文章

沒有留言:

張貼留言注意事項:

◎ 勾選「通知我」可收到後續回覆的mail!
◎ 請在相關文章留言,與文章無關的主題可至「Blogger 社團」提問。
◎ 請避免使用 Safari 瀏覽器,否則無法登入 Google 帳號留言(只能匿名留言)!
◎ 提問若無法提供足夠的資訊供判斷,可能會被無視。建議先參考這篇「Blogger 提問技巧及注意事項」。
◎ CSS 相關問題非免費諮詢,建議使用「Chrome 開發人員工具」尋找答案。
◎ 手機版相關問題請參考「Blogger 行動版範本的特質」→「三、行動版範本不一定能執行網頁版工具」;或參考「Blogger 行動版範本修改技巧 」,或本站 Blogger 行動版標籤相關文章。
◎ 非官方範本問題、或貴站為商業網站,請參考「Blogger 免費諮詢 + 付費諮詢
◎ 若是使用官方 RWD 範本,請參考「Blogger 推出全新自適應 RWD 官方範本及佈景主題」→ 不建議對範本進行修改!
◎ 若留言要輸入語法,"<"、">"這兩個符號請用其他符號代替,否則語法會消失!
◎ 為了過濾垃圾留言,所有留言不會即時發佈,請稍待片刻。
◎ 本站「已關閉自刪留言功能」。

TOP