FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

Googleフォトの画像を並び替えたい

この記事はFLINTERS10周年記念ブログリレーの35日目です。

こんにちは、最近はデータエンジニアをやっている湯上です。
FLINTERSではそれなりの古株ではありますが、この会社で働いているうちにも様々な出来事があり今や気付けば二児の親です。

さて、我が家では誕生日などの子どもの節目にフォトスタジオでプロに写真を撮影してもらうようにしています。 最近は写真をデータでいただけるサービスが多く、貰った写真はGoogleフォトにアップロードして管理するようにしています。 この写真データなのですが、Exifに撮影日時が記録されておらずファイル作成日が時系列順になっていないことがあります。 Googleフォトの画像は日付順に表示されるため、これらのデータをそのままアップロードすると順番がバラバラになってしまいます。

ファイル名は連番になっているので名前順に並び替えられればよいのですが、Googleフォトではその機能がないため並び替えるにはファイルの撮影日時を編集していかなければなりません。 何十枚もある写真データに対して手作業で編集するのは辛すぎるため、スクリプトでまとめて編集してしまいたいものです。

このような時、Photos Library APIを利用して一気に並び替えられないかというのがこの記事の趣旨です。

結論から言ってしまうと、Photos Library APIを利用して撮影日時を変更することはできません

OAuth2.0設定

他のGoogle APIと同じくOauth認証設定を行います。

CloudConsoleでプロジェクトを作成し、「APIとサービス」 > 「ライブラリ」でPhotos Library APIを有効に。

「APIとサービス」 > 「認証情報」でOAuthクライアントIDを作成します。
ここで作成したクライアントIDとクライアントシークレットは後ほど利用します。

最後にOAuth同意画面を作成します。 必要なスコープは
/auth/photoslibrary.appendonly
/auth/photoslibrary.readonly
/auth/photoslibrary.edit.appcreateddata
です。

アクセストークンの取得

ブラウザでこれを開きます。

https://accounts.google.com/o/oauth2/v2/auth?client_id=[クライアントID]&redirect_uri=http://localhost:8080&scope=https://www.googleapis.com/auth/photoslibrary.appendonly%20https://www.googleapis.com/auth/photoslibrary.readonly%20https://www.googleapis.com/auth/photoslibrary.edit.appcreateddata&access_type=offline&response_type=code

リクエストしたこの2つのスコープを許可して続行します。 遷移後のURLに含まれるcodeパラメーターがAuthorization Codeです。 これを使ってアクセストークンを取得します。

curl --data "code=[Authorization Code]" --data "client_id=[クライアントID]" --data "client_secret=[クライアントシークレット]" --data "http://localhost:8080" --data "grant_type=authorization_code" --data "access_type=offline" https://www.googleapis.com/oauth2/v4/token

# レスポンスの"access_token"を取得

APIリクエスト

APIリクエストさせるものは何でもいいのですが、今回はpythonで書きました。requestsを利用しています。
対象のデータ一覧を取得しましょう。

def get_photo_list() -> dict :
    url = "https://photoslibrary.googleapis.com/v1/mediaItems:search"
    response = requests.post(
        url=url,
        headers={
            "Authorization": f"Bearer {access_token}",
        },
        data=json.dumps({
            "albumId": album_id,
            "pageSize": 100,
    }))
    return response.json()["mediaItems"]

今回はアップロードした画像を同じアルバムに入れているため検索条件にalbumIdを指定しています。 album_idはアルバム一覧から探します。

def get_album_list() -> dict:
    url = "https://photoslibrary.googleapis.com/v1/albums"
    response = requests.get(
        url=url,
        headers= {
            "Authorization": f"Bearer {access_token}",
        },
    )
    return response.json()

取得できた写真データをファイル名でソート(sorted(photo_list, key=lambda x: x["filename"]))した後、更新します。
この時patch APIを使って更新できると思っていたのですが、APIを叩くとエラーになります。

{
  "error": {
    "code": 400,
    "message": "Invalid media item ID.",
    "status": "INVALID_ARGUMENT"
  }
}

おそらく、リファレンスにあるこの(↓)制限のためと思われます。

メディア アイテムは、API を使用してデベロッパーが作成したものであり、ユーザーが所有している必要があります。

さらに、このAPIで更新可能な情報はdescriptionのみです。

しかしここで諦めるわけにはいきません。

もう一度写真データをアップロード

API経由でファイルをアップロードした場合、メディアアイテムを作成する必要があります。 この時に撮影時間を指定すればうまくいくはずです。

公式ガイドに従って試しに1件アップロードしてみます。

def upload_photo(file_name: str, media_item: dict) -> str:
    with open(file_name, "rb") as f:
        file_data = f.read()
        url = "https://photoslibrary.googleapis.com/v1/uploads"
        response = requests.post(
            url=url,
            headers= {
                "Authorization": f"Bearer {access_token}",
                "Content-type": "application/octet-stream",
                "X-Goog-Upload-File-Name": file_name,
                "X-Goog-Upload-Protocol": "raw",
            },
            data=file_data,
        )
        upload_token = response.text
        print(upload_token)
        url = "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate"
        response = requests.post(
            url=url,
            headers= {
                "Authorization": f"Bearer {access_token}",
            },
            data=json.dumps({
                "albumId": album_id,
                "newMediaItems": [
                    {
                        "description": "test",
                        "simpleMediaItem": {
                            "uploadToken": upload_token,
                            "mediaMetadata": media_item["mediaMetadata"],
                        },
                    },
                ],
            }),
        )
        return response.text

レスポンス。

{
  "error": {
    "code": 400,
    "message": "Invalid JSON payload received. Unknown name \"mediaMetadata\" at 'new_media_items[0].simple_media_item': Cannot find field.",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.BadRequest",
        "fieldViolations": [
          {
            "field": "new_media_items[0].simple_media_item",
            "description": "Invalid JSON payload received. Unknown name \"mediaMetadata\" at 'new_media_items[0].simple_media_item': Cannot find field."
          }
        ]
      }
    ]
  }
}

mediaMetadataは明示的に設定できないようです。

だめじゃん

もはや万策尽きたのでローカルでメタデータを更新してからアップロードします。
pythonでExifを書き換えるためにexifライブラリを利用しています。

import datetime
import json

from exif import Image, DATETIME_STR_FORMAT
import requests

access_token = "アクセストークン"

def rewrite_creation_time(file_name: str, new_time: datetime) -> str:
    new_file_name = f"new_{file_name}"
    with open(file_name, "rb") as f:
        img = Image(f)
        img.datetime = new_time.strftime(DATETIME_STR_FORMAT)
        img.datetime_digitized = new_time.strftime(DATETIME_STR_FORMAT)
        img.datetime_original = new_time.strftime(DATETIME_STR_FORMAT)
        with open(new_file_name, "wb") as new_image_file:
            new_image_file.write(img.get_file())
    return new_file_name

def upload_photo(file_name: str):
    with open(file_name, "rb") as f:
        file_data = f.read()
        url = "https://photoslibrary.googleapis.com/v1/uploads"
        response = requests.post(
            url=url,
            headers= {
                "Authorization": f"Bearer {access_token}",
                "Content-type": "application/octet-stream",
                "X-Goog-Upload-File-Name": file_name,
                "X-Goog-Upload-Protocol": "raw",
            },
            data=file_data,
        )
        upload_token = response.text
        url = "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate"
        response = requests.post(
            url=url,
            headers= {
                "Authorization": f"Bearer {access_token}",
            },
            data=json.dumps({
                "newMediaItems": [
                    {
                        "simpleMediaItem": {
                            "uploadToken": upload_token,
                        },
                    },
                ],
            }),
        )
        return response.text

def increment_timestamp(timestamp: datetime) -> datetime:
    timestamp += datetime.timedelta(minutes=1)
    return timestamp


if __name__ == "__main__":
    photo_list = get_photos()
    timestamp_for_first_photo = datetime.datetime(
        year=2023,
        month=10,
        day=11,
        hour=0,
        minute=0,
    )
    timestamp = timestamp_for_first_photo
    for photo in photo_list:
        new_file_name = rewrite_creation_time(photo, timestamp)
        upload_photo(new_file_name)
        timestamp = increment_timestamp(timestamp)

これはさすがにうまくいきます。

おわりに

先日、80枚にも及ぶ写真ファイルの撮影日時を1枚ずつ編集しながらこの記事の作成を決意しました。
Photos Library APIでお手軽に編集可能と思っていたのですが残念な結果になってしまいました。
クラウド上のメタデータを編集可能なAPIがリリースされることを期待して待つことにします。