小卷的胡言亂語

隨心。隨興。隨喜。隨緣



使用c#連接google photos api實作

谷歌在2018年I/O大會宣布開放了photos api給開發者使用,終於!
非常需要這支api的傻露研究了一下~。

1、前言


傻露一直是picasa的使用者,
由於google在2016年宣布不再更新picasa,建議使用者使用自家的google photos,
並在2018年3月關閉picasa軟體上傳至google photos的通道後,
使用google提供的picasa api無法取得完整的google photos資料,
這讓傻露一直相當頭痛,終於在今年5月看到google釋出photos api的消息,
就來花點時間嘗試看看如何取得自己帳戶內的照片資料啦。

2、事前準備


開發者請先至google替你的應用程式申請clientid、clientsecret。
申請網址

傻露使用之開發工具為VS2013、以MVC+C#實作讀取的部分。

3、實作


1.新增mvc專案
2.nuget下載JSON.NET套件
3.加入命名空間

using Newtonsoft.Json;  
using System.Net;  
using System.Text;  
using System.IO;  

4.定義向google申請到的clientid、clientsecret,以及在申請頁面中填入的callback url

public class HomeController : Controller
{
    private string strClientID = "{CLIENT_ID}";
    private string strClientSecret = "{CLIENT_SECRET}";    
    private string strRedirectUrl = "{CALLBACK_URL}";   
}

5.連結至google OAuth

public ActionResult Index()
{
    string Url = "https://accounts.google.com/o/oauth2/auth?scope={0}&redirect_uri={1}&response_type={2}&client_id={3}&state={4}";  
    
    //scope:欲存取的scope,這裡只作讀取:所有相簿=分享+未分享
    string scope = "https://www.googleapis.com/auth/photoslibrary.readonly";  
    string redirect_uri = strRedirectUrl;  
    string response_type = "code";  
    string state = "";      
    
    Response.Redirect(string.Format(Url, scope, redirect_uri, response_type, strClientID, state));  
    return null;  
}  

(2018.8月更新)
若使用refresh token,Url字串須加上access_type的參數,值為變數access_type。 更改以及加入代碼如下:

string Url = "https://accounts.google.com/o/oauth2/auth?scope={0}&redirect_uri={1}&response_type={2}&client_id={3}&state={4}&access_type={5}";  
string access_type="offline";   //default=online,若要在使用者離線時亦能存取,須將此值設定為offline。  
Response.Redirect(string.Format(Url, scope, redirect_uri, response_type, strClientID, state, access_type)); 

關於refresh token的用途,詳細可參考Google Identity Platform文件
文件內表示,發給的access token具有時效性,時效一過,重新向谷歌申請時又會向使用者要求重新作Oauth認證。
因此設計refresh token此一機制,在使用者離線時,使用者不需再作Oauth認證,透過refresh token重新取得access token並重新取得資料。

6.在使用者登入並同意存取相簿內資料後,api會回傳一個token data(json格式)。
因此在這裡先定義一個TokenData物件接取資料。

public class TokenData
{
    /// <summary>
    /// access_token
    /// </summary>
    public string access_token { get; set; }    

    /// <summary>
    /// refresh_token
    /// </summary>
    public string refresh_token { get; set; }

    /// <summary>
    /// expires_in
    /// </summary>
    public string expires_in { get; set; }

    /// <summary>
    /// token_type
    /// </summary>
    public string token_type { get; set; }
    }

7.接收token data

public ActionResult Callback(string Code)
{
    // 沒有接收到參數
    if (string.IsNullOrEmpty(Code))
        return Content("沒有收到 Code");

    string Url = "https://accounts.google.com/o/oauth2/token";
    string grant_type = "authorization_code";    
    string redirect_uri = strRedirectUrl;
    string data = "code={0}&client_id={1}&client_secret={2}&redirect_uri={3}&grant_type={4}";

    //get token
    HttpWebRequest request = HttpWebRequest.Create(Url) as HttpWebRequest;
    string result = null;
    request.Method = "POST";    // 方法
    request.KeepAlive = true; // 是否保持連線
    request.ContentType = "application/x-www-form-urlencoded";

    string param = string.Format(data, Code, strClientID, strClientSecret, redirect_uri, grant_type);
    byte[] bs = Encoding.ASCII.GetBytes(param);

    using (Stream reqStream = request.GetRequestStream())
    {
        reqStream.Write(bs, 0, bs.Length);
    }

    using (WebResponse response = request.GetResponse())
    {
        StreamReader sr = new StreamReader(response.GetResponseStream());
        result = sr.ReadToEnd();
        sr.Close();
    }

    TokenData tokenData = JsonConvert.DeserializeObject<TokenData>(result);
    Session["token"] = tokenData.access_token;  //只取access_token
    
    return RedirectToAction("CallAPI");
}

(2018.8月更新)
如果在上面第5.步驟中設定access_type=“offline”,於本步驟程式碼執行後,取得的tokenData內即會給予refresh_token之資料
若使用refresh token,更改及新增代碼如下:

string grant_type = "refresh_token"; 
string strRefreshToken = "{REFRESH_TOKEN}";  
string data = "client_id={0}&client_secret={1}&refresh_token={2}&grant_type={3}";  
string param = string.Format(data, strClientID, strClientSecret, strRefreshToken, grant_type);    

8.call api

public ActionResult CallAPI()
{
    //--------1.取得相簿清單-----------
    if (Session["token"] == null)
        return Content("請先取得授權!");

    string token = Session["token"] as string;
                          
    // 取得album的 API 網址               
    string Url = "https://photoslibrary.googleapis.com/v1/albums?access_token=" + token + "&pageSize=50";   //pageSize預設=20,上限=50,不加此參數即會只回傳20筆資料
            
    HttpWebRequest request = HttpWebRequest.Create(Url) as HttpWebRequest;
            
    string result = null;   //api回傳的結果
    request.Method = "GET";    // 方法
    request.KeepAlive = true; // 是否保持連線                       

    using (WebResponse response = request.GetResponse())
    {
        StreamReader sr = new StreamReader(response.GetResponseStream());
        result = sr.ReadToEnd();        

        sr.Close();
    }   

    Response.Write(result);
}

執行之result列印出來內容大致如下:

{
    "albums": [
        {
            "id": "AGj1epU-......",
            "title": "相簿1",
            "productUrl": "https://photos.google.com/lr/album/AGj1epU-......",
            "totalMediaItems": "96",
            "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/AJZSZux......"
        },
        {
            "id": "AGj1epUO......",
            "title": "相簿2",
            "productUrl": "https://photos.google.com/lr/album/AGj1epUO......",
            "totalMediaItems": "54",
            "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/AJZSZux......"
        },
        ...
        ],
    "nextPageToken": "AH_uQ438......"
}

如果有”nextPageToken”此欄位,就代表後面還有資料,要以此token加在api網址後方繼續查詢,可獲得下一組資料。
即把Url改為:

string nextPageToken = "{nextPageToken}";   //取自上方api回傳之資料  
Url = "https://photoslibrary.googleapis.com/v1/albums?access_token=" + token + "&pageToken=" + nextPageToken + "&pageSize=50";  

欲取得單一相簿內所有照片資料,需使用Post方式連接api:

public ActionResult CallAPI()
{
    //接續上方CallAPI()之內容
    string contentUrl = "https://photoslibrary.googleapis.com/v1/mediaItems:search?access_token=" + token + "&pageSize=100";    //pageSize:預設50,上限100
    string albumid = "AGj1epV5o......";     //單一相簿的id,即上方result內"albums"之"id"
    request = HttpWebRequest.Create(contentUrl) as HttpWebRequest;
    
    string resultContent = null;  //api回傳的結果
    request.Method = "POST";    // 方法
    request.KeepAlive = true; // 是否保持連線
    request.ContentType = "application/json; charset=utf-8";            
    request.Headers.Add("Authorization", "Bearer " + token);
               
    string requestBody = "{\"pageSize\": \"100\",\"albumId\": \""+albumid+"\"}";    //google要求的格式為{"pageSize":"100","albumId": "ALBUM_ID"}
    Stream ws = request.GetRequestStream();
    using (var streamWriter = new StreamWriter(ws, new UTF8Encoding(false)))
    {
        streamWriter.Write(requestBody);
        streamWriter.Flush();
        streamWriter.Close();
    }
    
    using (WebResponse response = request.GetResponse())
    {
        StreamReader sr = new StreamReader(response.GetResponseStream());
        resultContent = sr.ReadToEnd();    

        sr.Close();
    }

    Response.Write(resultContent);
}

執行之resultContent列印出來內容大致如下:

{
  "mediaItems": [
    {
      "id": "AGj1epUz......",
      "description": "description",
      "productUrl": "https://photos.google.com/lr/album/AGj1epUu......",
      "baseUrl": "https://lh3.googleusercontent.com/photos-library/AJZSZuyU......",
      "mimeType": "image/jpeg",
      "mediaMetadata": {
        "creationTime": "2014-09-06T13:53:36Z",
        "width": "800",
        "height": "450",
        "photo": {}
      }
    },
    ]
}  

與上面抓取所有相簿資料相同的,如果有”nextPageToken”此欄位,就代表後面還有資料,要獲得下一組資料就必須以此token加在api網址後方繼續查詢。

總結

以上程式碼示範了讀取相簿資料,其他還有新增、更新等等的部分,原則是差不多的,參考文件實作應該不會太難。

原先傻露是為了要以api取得google相簿資料以製作自己的相簿藝廊(photo album / gallery),
畢竟目前google相簿
1.相簿排序無法變更為自己喜愛的方式(按照相簿名稱排序)
2.只有單一相簿連結分享,無UI可看到所有公開的相簿

而在傻露實際try過google photos api後發現:
1.以token取得的相簿、照片資訊,包含id、baseurl(圖床),依不同token均不同
每次以不同access token取得的相簿、照片資訊,包含id、baseurl(圖床)均不同(2018.8月更新)
2.若是透由【google相簿】上傳的資料,上述1.取得的資料具有時效性,時效一過,沒有登入google帳戶該張圖片是會產生破圖,無法讀取的(時效大約1日內吧)

雖然不清楚google這樣做的目的是甚麼,不過傻露大概猜測了一下,
除了不鼓勵使用者把google的相簿當作圖床來使用、不希望使用者可以像picasa那樣容易取得相簿的json資料、也不希望開發者儲存使用者的資料吧,
所以雖然乍看之下可以透過google photos api取得自己的所有相簿資料,但就製作公開的gallery而言有困難,
所以也只好放棄google photos api這條管道了,
只好安慰自己,當作一個程式練習,也多了解google api如何運作囉。

另外感謝網友Allen提醒access token與refresh token之不同,傻露也據此調整了內文。
若有未盡詳細或錯誤之處,也歡迎各位指正,謝謝。^_^”

參考資料

Google Photos API
Google Identity Platform
【oAuth 2.0 實作系列】ASP.Net MVC 實作使用 oAuth 2.0 連接 Google API
Android アプリで Google Photos APIs を使ってみた!!