【Pythyon】Google Photos API を実行するとUnknownApiNameOrVersionのエラー
google-api-python-client
に関しての内容です。
一年前に実装したGoogle Photosから写真を取得するプログラムをそのまま放置してしまっており、久しぶりに別環境で動かそうと思ったらエラーが出てしまい動かなくなってしまっておりました。
正しいやり方ではないのかもしれませんが取り急ぎ解決出来たのでメモとして残しておきます。
(Qiita等でも「間違った情報の記事をアップすると全体の質が下がる。初心者は記事を投稿するな」という意見がありますがこちらは個人用の備忘録ですのであしからず。。)
エラーメッセージ
Traceback (most recent call last): File "*******/Develop/google-photos/google_photos.py", line 177, in <module> main() File "******/google-photos/google_photos.py", line 156, in main service = discovery.build( File "*******/.pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/_helpers.py", line 134, in positional_wrapper return wrapped(*args, **kwargs) File "*******/.pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/discovery.py", line 273, in build content = _retrieve_discovery_doc( File "*******/.pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/discovery.py", line 387, in _retrieve_discovery_doc raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version)) googleapiclient.errors.UnknownApiNameOrVersion: name: photoslibrary version: v1
上記のようなエラーが出てしまい、APIが存在しないと言われてしまっております。
取り敢えず、エラーが出ているところを見てみます。
当該ソースコード
ライブラリのパスはお使いの環境によって変わると思いますが、おおよそ以下のような箇所でエラーが出ているようです。
# .pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/_helpers.py def positional_decorator(wrapped): @functools.wraps(wrapped) def positional_wrapper(*args, **kwargs): if len(args) > max_positional_args: plural_s = "" if max_positional_args != 1: plural_s = "s" message = ( "{function}() takes at most {args_max} positional " "argument{plural} ({args_given} given)".format( function=wrapped.__name__, args_max=max_positional_args, args_given=len(args), plural=plural_s, ) ) if positional_parameters_enforcement == POSITIONAL_EXCEPTION: raise TypeError(message) elif positional_parameters_enforcement == POSITIONAL_WARNING: logger.warning(message) return wrapped(*args, **kwargs) return positional_wrapper if isinstance(max_positional_args, six.integer_types): return positional_decorator else: args, _, _, defaults = inspect.getargspec(max_positional_args) return positional(len(args) - len(defaults))(max_positional_args)
# .pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/discovery.py", line 273, in build params = {"api": serviceName, "apiVersion": version} if http is None: discovery_http = build_http() else: discovery_http = http service = None for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version): requested_url = uritemplate.expand(discovery_url, params) try: content = _retrieve_discovery_doc( requested_url, discovery_http, cache_discovery, serviceName, version, cache, developerKey, num_retries=num_retries, static_discovery=static_discovery, ) service = build_from_document( content, base=discovery_url, http=http, developerKey=developerKey, model=model, requestBuilder=requestBuilder, credentials=credentials, client_options=client_options, adc_cert_path=adc_cert_path, adc_key_path=adc_key_path, ) break # exit if a service was created except HttpError as e: if e.resp.status == http_client.NOT_FOUND: continue else: raise e # If discovery_http was created by this function, we are done with it # and can safely close it if http is None: discovery_http.close() if service is None: raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version)) else: return service
# .pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/discovery.py", line 387, in _retrieve_discovery_doc if cache_discovery: if cache is None: cache = discovery_cache.autodetect() if cache: content = cache.get(url) if content: return content # When `static_discovery=True`, use static discovery artifacts included # with the library if static_discovery: content = discovery_cache.get_static_doc(serviceName, version) if content: return content else: raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
どうやらGoogle APIに接続するためのService
を生成しようと、discovery.py
の中のbuild
メソッドを実行し、その中で更に、discovery_cache/__init__ .py
の_retrieve_discovery_doc
を呼び出しているが、content
がnull
のためraise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
しているようです。
ではなぜcontent
がnull
なのかというと、本来discovery_cache.get_static_doc(serviceName, version)
で取得できるはずなのですが、取得出来ていないようです。
そのメソッドを追ってみると以下のような実装になっておりました。
# .pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/discovery_cache/__init__ .py def get_static_doc(serviceName, version): """Retrieves the discovery document from the directory defined in DISCOVERY_DOC_DIR corresponding to the serviceName and version provided. Args: serviceName: string, name of the service. version: string, the version of the service. Returns: A string containing the contents of the JSON discovery document, otherwise None if the JSON discovery document was not found. """ content = None doc_name = "{}.{}.json".format(serviceName, version) try: with open(os.path.join(DISCOVERY_DOC_DIR, doc_name), 'r') as f: content = f.read() except FileNotFoundError: # File does not exist. Nothing to do here. pass return content
DISCOVERY_DOC_DIR
配下のdoc_name = "{}.{}.json".format(serviceName, version)
ファイル(今回はphotoslibrary.v1.json
)を開きに行っているようですが、ファイルが見つからないようです。
当該ディレクトリを見てみると、以下のように様々なAPI向けのJSONファイルが配置してあるのですが、確かにphotoslibrary.v1.json
が無いようです。
google-api-python-client
のリポジトリを見てみるとこのdocument
ディレクトリにstatic filesが配置されそこからservice
オブジェクトを生成する方式になったのは昨年2020年の11月のようなので、そこから上記のようなエラーが出るようになってしまっているものと思われます。
他の方法で取得するやり方も不明なので、取り敢えずphotoslibrary.v1.json
を無理くりで作っちゃうことにしました。
他のサービスのjson(gmail.v1.json
)などを見てみると、oauth用のためのscopeやエンドポイントなどそのサービスのAPIに関する情報がすべてまとめてあるJSONファイルだったので、その情報を集めていたとこと、GoogleCloudPlatformのリポジトリにほぼ求めていたまんまのjsonがありました。
中身をざーっと見た感じでも、フォーマットも合っており必要な情報も揃ってそうだったので、乱暴にもそれをそのままリネームして当該ディレクトリに配置してみたところうまく動くようになりました。
↓こんな感じのjsonです。
// *******/.pyenv/versions/3.9.0/envs/google-photos/lib/python3.9/site-packages/googleapiclient/discovery_cache/documents/photoslibrary.v1.json { "auth": { "oauth2": { "scopes": { "https://www.googleapis.com/auth/drive.photos.readonly": { "description": "View the photos, videos and albums in your Google Photos" }, "https://www.googleapis.com/auth/photoslibrary": { "description": "View and manage your Google Photos library" }, "https://www.googleapis.com/auth/photoslibrary.appendonly": { "description": "Add to your Google Photos library" }, "https://www.googleapis.com/auth/photoslibrary.readonly": { "description": "View your Google Photos library" }, "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata": { "description": "Manage photos added by this app" }, "https://www.googleapis.com/auth/photoslibrary.sharing": { "description": "Manage and add to shared albums on your behalf" } } } }, "basePath": "", "baseUrl": "https://photoslibrary.googleapis.com/", "batchPath": "batch", "canonicalName": "Photos Library", "description": "Manage photos, videos, and albums in Google Photos\n", "discoveryVersion": "v1", "documentationLink": "https://developers.google.com/photos/", ~中略~~ "servicePath": "", "title": "Photos Library API", "version": "v1", "version_module": true }
その後も色々と調査してみたのですが、結局正式にはどのようにgoogle photos用のサービスオブジェクトを生成すればよいかわからなかったのですが、go言語のAPIクライアントの方にもgoogle photosに関する定義ファイルは存在していないようなので、何か意図があってのことなのかもしれません。
取り敢えず今回はこれで動くようになったのでしばらくはこのまま運用しようと思います。
【Golang】Google API 複数scopeのクレデンシャルを取得
先日1年ほど前に書いたGoのソースを眺めていたら、Google APIのクレデンシャル取得のところをどうしてこういう実装にしたのか忘れてしまい、戸惑ったので備忘として残しておきます。
Gmail API Go Quickstart に掲載されている実装の
// If modifying these scopes, delete your previously saved token.json.
config, err := google.ConfigFromJSON(b, gmail.GmailReadonlyScope)
の箇所が、ここではGmailのReadOnlyのScopeでconfigを取得していますが、これが複数サービス(たとえはGoogle Calender、Google Tasks)への権限も含めて取得したいとなった場合、このConfigFromJSON
は使えず、またscopeを複数配列で渡せるようなメソッドもなかった(見つけられなかっただけかもしれません。。) ので、自分で実装を変更してみた、という内容です。
方法
↑の実装ではConfigFromJSON
で取得するはずだった、Credentials構造体のscopeの箇所だけ自分で代入した、というだけです。
type Credentials struct { Installed struct { ClientId string `json:"client_id"` ProjectId string `json:"project_id"` AuthUrl string `json:"auth_url"` TokenUrl string `json:"token_url"` AuthProviderX509CertUrl string `json:"auth_provider_x509_cert_url"` ClientSecret string `json:"client_secret"` RdirectUris []string `json:"redirect_uris"` } `json:"installed"` } func getConfig() *oauth2.Config { b, err := ioutil.ReadFile("credentials.json") if err != nil { log.Fatalf("Unable to read client secret file: %v", err) } var cred Credentials json.Unmarshal(b, &cred) // tasks.TasksReadonlyScope → tasks.TasksScope scopes := []string{gmail.GmailReadonlyScope, tasks.TasksScope, calendar.CalendarReadonlyScope} return &oauth2.Config{ ClientID: cred.Installed.ClientId, ClientSecret: cred.Installed.ClientSecret, Endpoint: google.Endpoint, Scopes: scopes, RedirectURL: cred.Installed.RdirectUris[0], } }
それ以外のToken取得などの処理はチュートリアルのままでOKです。
全体としては以下のような実装になります。
type Credentials struct { Installed struct { ClientId string `json:"client_id"` ProjectId string `json:"project_id"` AuthUrl string `json:"auth_url"` TokenUrl string `json:"token_url"` AuthProviderX509CertUrl string `json:"auth_provider_x509_cert_url"` ClientSecret string `json:"client_secret"` RdirectUris []string `json:"redirect_uris"` } `json:"installed"` } // Retrieve a token, saves the token, then returns the generated client. func getClient(config *oauth2.Config) *http.Client { // The file token.json stores the user's access and refresh tokens, and is // created automatically when the authorization flow completes for the first // time. tokFile := "token.json" tok, err := tokenFromFile(tokFile) if err != nil { tok = getTokenFromWeb(config) saveToken(tokFile, tok) } return config.Client(context.Background(), tok) } // Request a token from the web, then returns the retrieved token. func getTokenFromWeb(config *oauth2.Config) *oauth2.Token { authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) fmt.Printf("Go to the following link in your browser then type the "+ "authorization code: \n%v\n", authURL) var authCode string if _, err := fmt.Scan(&authCode); err != nil { log.Fatalf("Unable to read authorization code: %v", err) } tok, err := config.Exchange(context.TODO(), authCode) if err != nil { log.Fatalf("Unable to retrieve token from web: %v", err) } return tok } // Retrieves a token from a local file. func tokenFromFile(file string) (*oauth2.Token, error) { f, err := os.Open(file) if err != nil { return nil, err } defer f.Close() tok := &oauth2.Token{} err = json.NewDecoder(f).Decode(tok) return tok, err } // Saves a token to a file path. func saveToken(path string, token *oauth2.Token) { fmt.Printf("Saving credential file to: %s\n", path) f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { log.Fatalf("Unable to cache oauth token: %v", err) } defer f.Close() json.NewEncoder(f).Encode(token) } func getConfig() *oauth2.Config { b, err := ioutil.ReadFile("credentials.json") if err != nil { log.Fatalf("Unable to read client secret file: %v", err) } var cred Credentials json.Unmarshal(b, &cred) // tasks.TasksReadonlyScope → tasks.TasksScope scopes := []string{gmail.GmailReadonlyScope, tasks.TasksScope, calendar.CalendarReadonlyScope} return &oauth2.Config{ ClientID: cred.Installed.ClientId, ClientSecret: cred.Installed.ClientSecret, Endpoint: google.Endpoint, Scopes: scopes, RedirectURL: cred.Installed.RdirectUris[0], } } func main() { config := getConfig() client := getClient(config) srv, err := gmail.New(client) // // gmail取得処理 // srv, err := calendar.New(client) // // calendar取得処理 // srv, err :=tasks.New(client) // //task取得処理 // }
こんな感じで一つのトークン、Configで複数サービスへの接続ができるようになります。
もちろん一つのクレデンシャルに権限を与えすぎるのもリスクはありますが自分で管理するアプリケーションの中で接続サービスごとにトークンを分けるのも大変なので、自分はこうしてしまっています。
【Golang】Gmail API メール本文のデコード
以前できなくて悩んでいた所が最近再度見てみたらサクッと解決できたのでメモ。
結論
base64
パッケージのStdEncoding
ではなくURLEncoding
を使えばOK。
以前はStdEncoding
を使って、うまくできない、途中で途切れてしまう〜と悩んでいました。
以下の感じで取れます。
message, err := srv.Users.Messages.Get(user, m.Id).Format("full").Do() message, err := srv.Users.Messages.Get(user, m.Id).Format("full").Do() if err != nil { log.Fatalf("Unable to retrieve messages: %v", err) } body := message.Payload.Body data := body.Data decodedMessage, _ := b64.URLEncoding.DecodeString(data)
GmailAPIの仕様
については今回は割愛しますが、自分は主にusers.messages.list
メソッドにクエリーを投げてmessageId
を取得し、取得したmessageId
をusers.messages.list
へのリクエストに投げてメール本文を取得、という感じでつかってます。
取得したメールのオブジェクトの構造は以下URLの通りなので、この通りbody.data
までを辿って上記の通りデコードすれば本文が取得出来ます。
スティード400を復活 〜プラグ交換編〜
これまでにキャブの洗浄とサスペンション交換、オイル、エレメント交換を実施しました。
コロナ禍でなかなか地元に帰ることができず時間が立ってしまいましたが、先日久しぶりに作業できたのでまとめておきます。
オイル交換してからすでに一年が経過してしまっているので結局乗り出す前には再度交換しないといけませんが。。。
作業する順序を少し考えないと。。
前回エンジンがまたかからなくなってしまっておりプラグがダメになっている可能性があったので、今回交換してみました。
プラグの場所
スティードにはVツインエンジンの前後それぞれ左右に一つづつ、計4つのプラグがあります。
見ての通り、左前と右後ろの位置のプラグはなかなかエグい位置にプラグが刺さっておりかなり作業しづらいので、少し工夫が必要です。
右前と左後ろの位置
この位置は簡単です。
こんな感じでプラグカバーを外せばプラグがむき出しになるので、プラグレンチで外して交換するだけです。
適合するプラグは Webike とかで検索してみるとわかります。
今回はこれを購入しました。
また、プラグの対辺寸法は18mmなのでプラグレンチはこちらを購入しました。
純正の車載工具があるとプラグ交換がとてもやりやすいらしいのですが、自分は持っていなかったのでこうした社外品で頑張りました。
スティードのプラグは深い位置にあるのでこういった長めのものがあると回しやすいです。
でも左前と右後ろはそれでもやりづらいですが。。
このプラグをプラグレンチで外すと簡単に取れます。
新しいものと比較すると汚れが一目瞭然。。。
中に埃が入らないよう素早く新しいものを取り付けます。
プラグカバーをもとに戻したら完了です。
左後ろの位置も比較的やりやすい場所にあるので同様に外して新しいものを取り付ければ簡単に交換できます。
左前と右後ろの位置
右後ろのプラグカバーは、繋がっているケーブルがプラスチックの留め具で固定されているので、それを外してから外す感じになります。
マイナスドライバーなどで簡単に外れます。
左前と右後ろの位置かなり作業がしずらいところにあるので、プラグレンチだけでは回せません。
このようにプラグレンチを挿しても穴にすっぽり収まってしまい長さが足りず回すことができません。
そこで今回は六角レンチをジョイントして回すことにしました。
使っているプラグレンチの片側が16mmのため、16mmの六角レンチを購入しました。
これをプラグレンチに挿せば狭い場所でも回すことができます。
これでレンチで回すことはできるようになるのですが、穴が深いので緩めたあとの回収と取り付けも気をつけないと大変です。
緩めたあとは↓のようにラジオペンチを突っ込んで挟めれば取れます。
(素人なのでわからないのですがこういうのを回収できるような磁石がついた工具とかあるもんなんですかね・・・?)
ただ、ここでラジペンでしっかり挟めていない状態でプラグが外れてしまうと下のように空洞?になっているところにプラグが落ちてしまい取るのが大変になってしまいます。
なので、プラグレンチで緩めきったあとは下に紙を挟み込んでおいて、落ちないようにしておいてからプラグレンチを引き抜き、プラグ回収をしたほうがやりやすいです。
(写真の場所がそれぞれ違うところを写したものになってますが)
こうするとうまくプラグが回収できます。
プラグを取り付ける際も、穴が深いので、プラグを取り付けてからレンチで締めるということができないので、プラグをレンチにはめた状態で取り付けるとうまくできます。
こうして4箇所プラグ交換をすれば完了です。
終わりに
今回始めてプラグ交換を実施してみました。
かなり難しい位置にあるプラグなので苦労しましたが、ちょうどよい道具が見つけられたのでなんとか交換できました。
肝心なエンジンがかかるかどうかの確認はできていないのでまたの機会に確認しようと思います。
また、このときスプロケ、チェーン交換もやってみた(結局できなかった。。)のでそちらも後日まとめたいと思います。
Golang + Gin + Heroku でLineBotを作ってみた
はじめに
コロナ禍大変になってきてますね。
最近かなり暖かくもなってきたので、誰にも迷惑をかけず感染リスクも侵さない娯楽としてバイクにめちゃくちゃ乗りたくなってきたのですが、
残念ながらまだかつての愛車も修理が終わっておらず、新たに購入する資金力も無いため、家族共々自宅に籠もっております。
ふと思い立ってGoを勉強してみたいと思い、勉強し始めました。特に理由はありません。
基本的な文法を学ぶ必要がありますが、モチベーション維持のためにはやはり何か作りながらのほうがいい。
しかし特に作りたいものも無いしどうしたものか。。
現在、妻へ連絡する色々を自動化するためにLineBotを使っているのですが、
バックエンドは昔PHPで実装したものをそのまま継続的に使用しており(学生時代PHPしか書けなかったので。。。)
最近個人的にPHPを使うことも少なくなってきたので、Goで書き直そうとやってみました。
取り合えずSDKとGoのWebフレームワークであるGinを使ってオウム返答するところまでを動かします。
Goの環境をDockerで構築
Flask-AskでAlexaのカスタムスキルを作成 - 雑食へいちゃんの思い出達
この記事でやったことと同じです。
まずはこんなalpineベースのgolangイメージをベースにDockerfileを作成します。
FROM golang:1.14.2-alpine3.11 WORKDIR /go RUN apk update && \ apk add vim git
以下コマンドでコンテナの中に入ります。
$ docker build . -t go-env $ docker run -it -v $PWD:/go -p 8080:8080 go-env /bin/ash
コンテナに入ったらGinをインストールします。
$ go get github.com/gin-gonic/gin
まずは、サンプルのmain.go
を書いて動作を確認します。
package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.GET("/hello", func(c *gin.Context) { c.String(http.StatusOK, "Hello World!!") }) router.Run(":8080") }
$ go run main.go
ブラウザからlocalhost:8080/hello
にアクセスして「Hello World!!」と表示されれば成功です。
次はこれをHerokuにデプロイしていきます。
Dockerのコンテナに作った環境をherokuにデプロイしていくので、
Dockerfileを以下の通り書き換えます。
FROM golang:1.14.2-alpine3.11 WORKDIR /go ADD . /go RUN apk update && \ apk add git vim CMD go run main.go
また、main.go
で立ち上げるポートも8080ではなく、環境変数を使用するように変更します。
package main import ( "os" "net/http" "github.com/gin-gonic/gin" ) func main() { bot, err := linebot.New( os.Getenv("LINEBOT_CHANNEL_SECRET"), os.Getenv("LINEBOT_CHANNEL_TOKEN"), ) if err != nil { log.Fatal(err) } router := gin.Default() router.GET("/hello", func(c *gin.Context) { c.String(http.StatusOK, "Hello World!!") }) router.Run(":" + os.Getenv("PORT")) // ここを環境変数を参照するように変更 }
ローカル環境のDockerコンテナ内でGinサーバを立ち上げる際は、以下コマンドで環境変数を追加する必要があります。
$ export PORT=8080
続いてHerokuにデプロイしていきます。
デプロイの方法は先程同様↓の記事を参照ください。
Flask-AskでAlexaのカスタムスキルを作成 - 雑食へいちゃんの思い出達
HerokuのURLにアクセスし、localhostと同様の表示が出れば成功です。
オウム返しするLineBotを作成する
Line Messaging APIにはGolang用のSDKが提供されているので、それを使用します。
$ go get github.com/line/line-bot-sdk-go/linebot
main.go
を以下のように修正します。
基本すべて公式に掲載されているサンプルですが、Ginに合わせて一部修正しています。
line-bot-sdk-go/server.go at master · line/line-bot-sdk-go · GitHub
package main import ( "os" "fmt" "log" "net/http" "github.com/gin-gonic/gin" "github.com/line/line-bot-sdk-go/linebot" ) func main() { bot, err := linebot.New( os.Getenv("LINEBOT_CHANNEL_SECRET"), os.Getenv("LINEBOT_CHANNEL_TOKEN"), ) if err != nil { log.Fatal(err) } router := gin.Default() router.GET("/hello", func(c *gin.Context) { c.String(http.StatusOK, "Hello World!!") }) router.POST("/callback", func(c *gin.Context) { events, err := bot.ParseRequest(c.Request) if err != nil { if err == linebot.ErrInvalidSignature { c.Writer.WriteHeader(400) } else { c.Writer.WriteHeader(500) } return } for _, event := range events { if event.Type == linebot.EventTypeMessage { switch message := event.Message.(type) { case *linebot.TextMessage: if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(message.Text)).Do(); err != nil { log.Print(err) } case *linebot.StickerMessage: replyMessage := fmt.Sprintf( "sticker id is %s, stickerResourceType is %s", message.StickerID, message.StickerResourceType) if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(replyMessage)).Do(); err != nil { log.Print(err) } } } } }) router.Run(":" + os.Getenv("PORT")) }
これをHerokuにデプロイし、LINEBOT_CHANNEL_SECRET
とLINEBOT_CHANNEL_TOKEN
を環境変数として設定し、Line Developersサイトでcallback URLにHerokuのサイトURLを設定すれば完了です。
終わり
とりあえず動くところまではできたので、文法についても勉強しながら、現行で動いている機能を移植していきたいと思います。
【Python】Google Photos API でアルバムの画像取得
はじめに
我が家ではこどもたちの写真を撮影月ごとにGoogle Photosの共有アルバムにまとめて両親に共有しています。
これらの写真をAPIで取得して、フォトフレームなど使えるといいなーと思っていましたが、以前はGoogle Photos のAPIは整備されておらず、Picasa APIのみでした。
(Picasa自体も既にサービス終了しており、APIもあまりメンテナンスされている雰囲気でもなくいつか終わりそうな気がしたので、こっちを使うことは避けてました。)
気が付くと2018年にGoogle Photos APIがリリースされていたので、これを使っての写真取得を試してみたいと思います。
最終的には、年月を指定すると、その月の共有アルバムに含められている写真を取得する、というプログラムにしたいと思います。
公式リファレンスはこちらです
Credentialを取得
まず初めに、APIを使用するのに必要なCredentialを取得します。
Google Photos APを使うにはOAuthでのユーザー認証が必要になります。
OAuthの認証にはPythonのクライアントライブラリが提供されているのでそちらを使います。
上記の公式にも下記の記述がある通りGoogle Photos APIでは、Service Account方式は使えないようです。
Note that the Library API does not support service accounts; to use this API, users must be signed in to a valid Google Account.
なので、OAuth 2.0 for Web Server Applicationsか、OAuth 2.0 for Installed Applicationsのどちらかをつかうのですが、今回はWeb Serverを立てるわけではないので、Installed Applicationsタイプで実装しました。
client_secretの取得
まずはOAuthの認証に必要なclient secretを取得します。 Google API Consoleにアクセス。
左上の「APIとサービス」より「認証情報」を選択
「+認証情報を作成」をクリック
これでダウンロードよりclient_secret.json
をダウンロードします。
credentialの取得
ダウンロードしたclient_secret.json
を読み取り、credentialを取得します。
# google_photos.py import google.oauth2.credentials import google_auth_oauthlib.flow SCOPES = ['https://www.googleapis.com/auth/photoslibrary'] API_SERVICE_NAME = 'photoslibrary' API_VERSION = 'v1' CLIENT_SECRET_FILE = 'client_secret.json' flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE,scopes=SCOPES) credentials = flow.run_console()
上記プログラムを実行すると、
$ python google_photos.py Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={$cliens_id}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fphotoslibrary&state={$state}&prompt=consent&access_type=offline Enter the authorization code:
のように、URLが出力されるので、上記URLをブラウザに入力すると以下の画面になるので、Googleアカウントでログインしてください。
ログイン後は、以下のように認可コードが払い出されるので、コピーしてターミナルに貼り付けます。
上記でCredentialは取得出来るのですが、その中にはtokenやRefresh tokenも含まれており、その後はRefresh tokenを使用してCredentialを取得することが出来るようになるため、毎回ログインする必要がなくなります。
しかし、現在のgoogle_auth_oauthlib
まだcredentialの保存をサポートしていないので、credentialをそのままjsonのファイルで保存して、今後はRefresh Tokenを使用してcredentialを取得するようにします。
google-auth-oauthlib does not currently have support for credentials storage. It may be added in the future. See oauth2client deprecation for more details.
credentialの中にはdatetimeオブジェクトが入っているので、↓のコードを頂戴してcredentialをシリアライズして保存しておきます。
def support_datetime_default(o): if isinstance(o, datetime): return o.isoformat() raise TypeError(repr(o) + " is not JSON serializable") with open(CREDENTIAL_FILE, mode='w') as f_credential_w: f_credential_w.write(json.dumps(vars(credentials), default=support_datetime_default, sort_keys=True))
こうすることで、一度credentialを取得したあとは、ログインをし直す必要なく、Refresh Tokenでcredentialの取得が出来るようになります。
if os.path.exists(CREDENTIAL_FILE): with open(CREDENTIAL_FILE) as f_credential_r: credentials_json = json.loads(f_credential_r.read()) credentials = google.oauth2.credentials.Credentials( credentials_json['token'], refresh_token=credentials_json['_refresh_token'], token_uri=credentials_json['_token_uri'], client_id=credentials_json['_client_id'], client_secret=credentials_json['_client_secret'] )
credentialを取得後は以下のようにServiceを取得します。
service = build( API_SERVICE_NAME, API_VERSION, credentials=credentials )
今後このserviceで各種APIを叩いていきます。
これまでのコードの全体像は以下のようになります。
# google_photos.py import google.oauth2.credentials import google_auth_oauthlib.flow import json,os from datetime import datetime,date from googleapiclient.discovery import build SCOPES = ['https://www.googleapis.com/auth/photoslibrary'] API_SERVICE_NAME = 'photoslibrary' API_VERSION = 'v1' CLIENT_SECRET_FILE = 'client_secret.json' CREDENTIAL_FILE = 'credential.json' def support_datetime_default(o): if isinstance(o, datetime): return o.isoformat() raise TypeError(repr(o) + " is not JSON serializable") def getCredentials(): if os.path.exists(CREDENTIAL_FILE): with open(CREDENTIAL_FILE) as f_credential_r: credentials_json = json.loads(f_credential_r.read()) credentials = google.oauth2.credentials.Credentials( credentials_json['token'], refresh_token=credentials_json['_refresh_token'], token_uri=credentials_json['_token_uri'], client_id=credentials_json['_client_id'], client_secret=credentials_json['_client_secret'] ) else: flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE,scopes=SCOPES) credentials = flow.run_console() with open(CREDENTIAL_FILE, mode='w') as f_credential_w: f_credential_w.write(json.dumps(vars(credentials), default=support_datetime_default, sort_keys=True)) return credentials def main(): credentials = getCredentials() service = build( API_SERVICE_NAME, API_VERSION, credentials=credentials ) if __name__ == "__main__": main()
各アルバム内の写真を取得
共有アルバムIDの取得
アルバムIDは以下のように取得できます。
sharedAlbums = service.sharedAlbums().list(pageSize=20,pageToken=nextPageToken).execute()
我が家では前述の通り、子供たちの写真を共有アルバムにて両親に共有しています。
その際に子供の写真については、「yyyymm01〜」の命名規則でアルバム名をつけているので、
該当するアルバム名を取得します。
nextPageToken = '' albums = {} while True: sharedAlbums = service.sharedAlbums().list(pageSize=20,pageToken=nextPageToken).execute() for sharedAlbum in sharedAlbums['sharedAlbums']: if 'title' in sharedAlbum: m = re.search(r'^\d\d\d\d\d\d01', sharedAlbum['title']) if m: month = m.group() if not (month in albums): albums[m.group()] = [] albums[m.group()].append(sharedAlbum['id']) if 'nextPageToken' in sharedAlbums: nextPageToken = sharedAlbums['nextPageToken'] else: break
その際に子供の写真については、「yyyymm01〜」の命名規則でアルバム名をつけているので、
この運用を始めてからは基本的に一月に1共有アルバムになったのですが、
この運用ルールで実施できていなかった頃に、一月の間に複数アルバムを作ってしまっているので、
以下のように複数取得出来るようにしています。
if not (month in albums): albums[m.group()] = [] albums[m.group()].append(sharedAlbum['id'])
また最終的には、年月を指定すると、その月の写真をとってくる、というプログラムにしたいので、毎回共有アルバムのIDを取得するのは面倒なので、
予め子供の写真を登録している共有アルバムのリストを取得してファイルに保存しておき、
写真を取得するときは、指定した年月の共有アルバムIDリストのファイルから読み込む、という動きにしました。
MONTH_ALBUMS_FILE = 'month_albums.json' def updateAlbumList(service): nextPageToken = '' albums = {} while True: sharedAlbums = service.sharedAlbums().list(pageSize=20,pageToken=nextPageToken).execute() for sharedAlbum in sharedAlbums['sharedAlbums']: if 'title' in sharedAlbum: m = re.search(r'^\d\d\d\d\d\d01', sharedAlbum['title']) if m: month = m.group() if not (month in albums): albums[m.group()] = [] monthAlbums[m.group()].append(sharedAlbum['id']) if 'nextPageToken' in sharedAlbums: nextPageToken = sharedAlbums['nextPageToken'] else: break with open(MONTH_ALBUMS_FILE, mode='w') as f_month_albums_w: f_month_albums_w.write(json.dumps(albums, default=support_datetime_default, sort_keys=True)) def getAlbumIds(months): albumIds = [] with open(MONTH_ALBUMS_FILE) as f_month_album_r: monthAlbumJson = json.loads(f_month_album_r.read()) for month in months: monthAlbumIds = monthAlbumJson[month] for monthAlbumId in monthAlbumIds: albumIds.append(monthAlbumId) return albumIds
共有アルバムのIDを取得してMONTH_ALBUMS_FILE
というファイルに保存しておくupdateAlbumList
という関数と、
年月をyyyymm01
形式で渡すと共有アルバムのIDを返却するgetAlbumIds
という関数とで分けました。
共有アルバム内の写真を取得
いよいよ写真の取得ですが、写真の取得は
mediaItems.searchというIFで取得ができます。 リクエストbodyのJSONの中にアルバムIDを含めてリクエストすると、そのアルバム内の写真を取得できます。
以下ように取得できます。
body = { 'albumId' : albumId, 'pageSize' : 50, } mediaItems = service.mediaItems().search(body=body).execute()
アルバムIDの配列を渡されたら、そのアルバムID内の写真を取得するような関数にしました。
def getPhotos(service, albumIds): photos = [] for albumId in albumIds: nextPageTokenMediaItems = '' while True: photo = {} body = { 'albumId' : albumId, 'pageSize' : 50, 'pageToken' : nextPageTokenMediaItems } mediaItems = service.mediaItems().search(body=body).execute() # print(mediaItems) for mediaItem in mediaItems['mediaItems']: photo['id'],photo['url'] = mediaItem['id'],mediaItem['baseUrl'] photos.append(photo) if 'nextPageToken' in mediaItems: nextPageTokenMediaItems = mediaItems['nextPageToken'] else: break print(photos)
最終的な全体の実装は以下の通りです。
from googleapiclient.discovery import build import google.oauth2.credentials import google_auth_oauthlib.flow import json,os,re from datetime import datetime,date SCOPES = ['https://www.googleapis.com/auth/photoslibrary'] API_SERVICE_NAME = 'photoslibrary' API_VERSION = 'v1' CLIENT_SECRET_FILE = 'client_secret.json' REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' CREDENTIAL_FILE = 'credential.json' MONTH_ALBUMS_FILE = 'month_albums.json' def support_datetime_default(o): if isinstance(o, datetime): return o.isoformat() raise TypeError(repr(o) + " is not JSON serializable") def getCredentials(): if os.path.exists(CREDENTIAL_FILE): with open(CREDENTIAL_FILE) as f_credential_r: credentials_json = json.loads(f_credential_r.read()) credentials = google.oauth2.credentials.Credentials( credentials_json['token'], refresh_token=credentials_json['_refresh_token'], token_uri=credentials_json['_token_uri'], client_id=credentials_json['_client_id'], client_secret=credentials_json['_client_secret'] ) else: flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE,scopes=SCOPES) credentials = flow.run_console() with open(CREDENTIAL_FILE, mode='w') as f_credential_w: f_credential_w.write(json.dumps(vars(credentials), default=support_datetime_default, sort_keys=True)) return credentials def updateAlbumList(service): nextPageToken = '' albums = {} while True: sharedAlbums = service.sharedAlbums().list(pageSize=20,pageToken=nextPageToken).execute() for sharedAlbum in sharedAlbums['sharedAlbums']: if 'title' in sharedAlbum: m = re.search(r'^\d\d\d\d\d\d01', sharedAlbum['title']) if m: month = m.group() if not (month in albums): albums[m.group()] = [] albums[m.group()].append(sharedAlbum['id']) if 'nextPageToken' in sharedAlbums: nextPageToken = sharedAlbums['nextPageToken'] else: break with open(MONTH_ALBUMS_FILE, mode='w') as f_month_albums_w: f_month_albums_w.write(json.dumps(albums, default=support_datetime_default, sort_keys=True)) def getPhotos(service, albumIds): photos = [] for albumId in albumIds: nextPageTokenMediaItems = '' while True: photo = {} body = { 'albumId' : albumId, 'pageSize' : 50, 'pageToken' : nextPageTokenMediaItems } mediaItems = service.mediaItems().search(body=body).execute() # print(mediaItems) for mediaItem in mediaItems['mediaItems']: photo['id'],photo['url'] = mediaItem['id'],mediaItem['baseUrl'] photos.append(photo) if 'nextPageToken' in mediaItems: nextPageTokenMediaItems = mediaItems['nextPageToken'] else: break print(photos) def getAlbumIds(months): albumIds = [] with open(MONTH_ALBUMS_FILE) as f_month_album_r: monthAlbumJson = json.loads(f_month_album_r.read()) for month in months: monthAlbumIds = monthAlbumJson[month] for monthAlbumId in monthAlbumIds: albumIds.append(monthAlbumId) return albumIds def main(): credentials = getCredentials() service = build( API_SERVICE_NAME, API_VERSION, credentials=credentials ) updateAlbumList(service) # ここは必要に応じて実施するように後々処理を分けるつもり months = ['20190101','20190201'] albumIds = getAlbumIds(months) getPhotos(service, albumIds) if __name__ == "__main__": main()
まとめ
Pythonを使って、Google Photos APIを使ってみました。
共有アルバムから写真は取得出来るようになったので、今度はAlexaと連携して、声で指示した写真を取得出来るようにしたいと思います。
2021年3月15日追記
一年経ったぐらいに久しぶりにこのコードを動かしてみたらエラーが出るようになってしまったので修正しました。
スティード400を復活 〜サスペンション&オイルフィルター交換編〜
昨年の夏に実家に眠っているスティード400 のキャブ洗浄を行いました。
kittagon.hateblo.jp
今年のGWにはオイルエレメントの交換と、サスペンションの交換をしたのでまとめます。
手探りでやったのであまり写真に残せませんでしたが。。
サスペンションの交換
僕のスティード400はリジットバーをつけているので、今回はそれを交換したいと思います。
リジットバーを付けていると車体が下がってカッコいいんですが、なにせショック吸収が皆無なのでお尻が結構辛いです。
高校生の頃はそれで良かったのですが、今の歳になると絶対にキツい、、、
なので純正とまでは行きませんが、最低限のショック吸収があるサスペンションに交換したいと思います。
今回はヤフオクで購入した、デイトナのサスペンションを装着します。
前回と同様に、シートとサイドカバーを外しておきます。
とりあえずリジットバーが固定されている、シート下側のボルトを緩めてしまいます。(↓の赤丸箇所)
ここのボルトを引き抜くためには、後輪が動ける状態でないと外せないので、この辺りをジャッキアップします。
他のブログ等見てみると、ボルトにテンションを掛けておいて、スルッと外れる高さまでうまいこと丁度にジャッキアップすると書いてありますが、僕は微調整も面倒だったので、がっつりジャッキアップした後、後輪を手で持ち上げながらボルトを引き抜きました。
どうせ後から付けるサスペンションは高さが違うのでここで微調整してもつけるときはまた高さ調整しなくてはなりませんからね。
左右それぞれこれぐらいの高さまで上げてしまいました。
リジットバーが付いているので、リアのタイヤも浮いた状態になります。写真を撮るのを忘れましたが)
ボルトを外しておいて、スイングアームの↓の赤丸の辺りを持ち上げると、ボルトにかかるテンションが緩むので、持ち上げた状態でボルトを引き抜くと簡単に取れます。
こうしてシート側のボルトが無事外せたので、今度はスイングアーム側に固定されているボルトを緩めて外せばOKです。
若干ステーが邪魔ですが、シートの下からボルトを回して外せます。
次はいよいよサスペンションを取り付けていきます。
外すときはタイヤの横から手を突っ込んで外せたので意識する必要はなかったのですが、取り付けるときはシート下のステーを外す必要があります。
最初からステーを外してしまうほうが、リジットバーを外す際も楽です。僕は外すのが面倒臭かかったので横着してステーを外さないままリジットバーを外したのですが、結局交換のサスを取り付ける際に外す必要があったので、最初から外しておけばよかったと思いました。
↓これです。
これが少し面倒臭くて、シート下からは取れず、左右のサイドカバー下にあるボルトを外さないと取れません。
↓左側
↓右側。ステーの表にイグニッションコイルがついているので一旦外す必要があります。
↓イグニッションコイルを外した下に見えるボルトを外します。
こんな感じでシート下でサスペンションを固定しているところが見えるようになるので、あとは先程リジットバーを取り外したのと逆の手順でサスペンションを取り付ければOKです。
取り付けるときは、スイングアーム側を固定するのは簡単ですが、シート下を固定するためにはジャッキの高さを微調整してボルトの穴がちょうど良い位置にしないといけません。
これがかなり面倒臭いです。。
無事取り付けることができました。
リジットバーをつけていたときと比べるとだいぶ車高が高くなりました。
これなら乗りやすそうですね。
オイル、オイルフィルターの交換
去年せっかくキャブを洗浄したので、オイルも交換しようと思いますが、オイルフィルターも前回いつ交換したんだっけ?という感じだったので、今回の機に交換しようと思います。
オイルフィルターについてはあまり知識が無いのですが、とりあえず型をみてスティードに適合しそうなものの中から以下を選定しました。
また、オイルフィルターを外すためにはオイルフィルターレンチが必要になります。
バイク屋さん曰く、オイルフィルターレンチのサイズは、スズキ車とホンダ・カワサキ・ヤマハで違うそうです。
ホンダ車用のこちらを購入し使用しました。
まずは、オイル交換です。こちらは簡単ですね。
エンジンの下にあるボルトを外してオイルを出します。
市販の廃油吸収剤を買ってきて下に敷いておきます。
敷いた状態で↑のボルトを外すとオイルが出てきます。
次にオイルフィルターを交換します。
スティードのオイルフィルターはここ↓にあります。
これを先程のオイルフィルターレンチで回して外します。
このオイルフィルターを外す際も、外したあとオイルが出てくるので、廃油吸収剤を下に置いておいたほうが良いです。
写真を取り忘れてしまいましたが、オイルフィルターを交換したら、オイルを適量入れて作業終了です。
終わりに
サスペンションの交換は、高校の時リジットバーに交換した際も、詳しい先輩にやっていただいたのですが、とても大変そうだった印象だったので、とりあえず自力でやりきれてよかったです。
ちゃんと安定させてジャッキアップできれば特に難しい部分はありませんでした。
オイルも交換したし改めてエンジンをかけてみようとしたらかからなくなってました。
色々調べた結果プラグが悪くなっている可能性があるので、次回時間があるときはプラグの交換をしたいと思います。