PythonでGoogleフォトの特定のアルバムの画像をEagleにまとめてアップロードしてみた

ここ最近、[Eagle](https://jp.eagle.cool/) を使用して画像・ナレッジ管理すると便利だと聞き、試したところ噂通り便利だったため一括で移行を進めることにしました。

普段、画像はとりあえずGoogleフォトに保管していたので、まとめて欲しいものだけEagleにアップロードしたいと思ったときに今回の方法を実施しました。

# Eagleとは

Eagleとは、台湾のogdesign.incという会社が開発したクリエイター向けの画像管理ツールです。

筆者はクリエイターではなくエンジニアですが、

- 管理できるものがPNGやJPEGなどの画像ファイルに留まらずPDF、ウェブページ、GIFなど多岐に渡る
- ローカルで動作しますが、Googleドライブなどのクラウドストレージの紐づけられる
- Chrome拡張機能が提供されているためブラウザの画像を直接Eagleに保存できる
- Eagle APIを使用してAPIベースで操作も可能

な点に惹かれました。

# 事前準備

事前準備として以下を実施します。

- Python実行環境の用意
- Eagle API Tokenの取得
- Google Cloud の OAuthクライアントの認証情報の取得
- 必要なパッケージのインストール

## Python実行環境の用意

今回はPythonを使って処理を行うためご自身のPC環境にPythonの実行環境を用意します。

各OS毎に手順に沿ってインストールをしてください。

https://prog-8.com/docs/python-env-win

https://prog-8.com/docs/python-env

## ~~Eagle API Tokenの取得~~

~~Eagle APIを使ってアップロードをするにはAPI Tokenを使用し認証する必要があります。~~

~~API TokenはEagleのソフトの左上にあるハンバーガーメニューを選択後、「環境設定 > 開発者」を見るとAPI Tokenを参照できます。~~

![2024-05-24_00h55_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1046178/e1aec864-35fc-09aa-0db2-e85d8c3d2324.png)

## Google Cloud の OAuthクライアントの認証情報の取得

GoogleフォトからPythonを使用しアルバムの情報を取得するために、
Google Photo APIを使用します。

Google Photo APIを使用するための認証情報は、
Google Cloudのコンソールから取得する必要があります。

下記ブログの「Google Cloud Platform の設定」にて分かりやすく説明されているためご参照ください。
Google Cloudを使ったことがある方は既存のプロジェクトを使用していただいても大丈夫です。

https://qiita.com/Erytheia/items/4aa0fc0599db1233c333#google-cloud-platform-%E3%81%AE%E8%A8%AD%E5%AE%9A

:::note warn
取得した`.json`の拡張子のファイルは必ず`client_secrets.json`の名前に変更してください。
:::

## 必要なパッケージのインストール

Python環境を用意できたら必要なパッケージをインストールします。

Windowsであればコマンドプロンプト、MacであればTerminalを開いて下記コマンドを実行してください。

```shell
pip install google-api-python-client google-auth-oauthlib EagleWrapper
```

# 実行してみる

事前準備はできたので実際にPythonのファイルを作成します。

index.pyなどファイル名はなんでも大丈夫なので、下記コードを書いたファイルを用意してください。
※コードの説明はあとがきで記載します。

~~2点~~ 1点、環境に応じて修正が必要なため書き換えてください。

- albumName
    - Googleフォトのアルバム名を記載してください
    - `ALBUM_NAME`を正しいものに置き換えてください
- ~~EAGLE_API_KEY~~
    - ~~事前に取得したEagleのAPI Tokenを記載してください~~
    - ~~`YOUR_EAGLE_API_KEY`を正しいものに置き換えてください~~

```python:index.py
from googleapiclient.discovery import build
import google.oauth2.credentials
import google_auth_oauthlib.flow
import json, os
from eaglewrapper import Eagle

albumName = "ALBUM_NAME" # <- アルバム名を入力
#EAGLE_API_KEY = 'YOUR_EAGLE_API_KEY' # <- Eagle API Tokenを入力

SCOPES = ['https://www.googleapis.com/auth/photoslibrary']
API_SERVICE_NAME = 'photoslibrary'
API_VERSION = 'v1'
CLIENT_SECRET_FILE = 'client_secrets.json'
CREDENTIAL_FILE = 'credential.json'
#EAGLE_API_ENDPOINT = 'http://localhost:41595/api/item/addFromURL'

eagle = Eagle()

# Credentialを取得
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_local_server()
        with open(CREDENTIAL_FILE, mode='w') as f_credential_w:
            f_credential_w.write(credentials.to_json())
    return credentials

# アルバムから画像を取得
def getPhotosFromAlbum(service, albumId):
    photos = []
    nextPageToken = ''
    while True:
        body = {
            'albumId': albumId,
            'pageSize': 100,
            'pageToken': nextPageToken
        }
        response = service.mediaItems().search(body=body).execute()
        items = response.get('mediaItems', [])
        for item in items:
            photos.append([item['baseUrl'],item['filename']])
        nextPageToken = response.get('nextPageToken', None)
        if not nextPageToken:
            break
    return photos

# アルバムIDを取得
def findAlbumByName(service, albumName):
    nextPageToken = ''
    while True:
        response = service.albums().list(pageSize=50, pageToken=nextPageToken).execute()
        albums = response.get('albums', [])
        for album in albums:
            if album['title'] == albumName:
                return album['id']
        nextPageToken = response.get('nextPageToken', None)
        if not nextPageToken:
            break
    return None

# Eagleに画像をアップロード
def upload_to_eagle(image_url, filename):
    response = eagle.add_from_url(image_url, filename)
    return response

def main():
    credentials = getCredentials()
    service = build(API_SERVICE_NAME, API_VERSION, credentials=credentials,static_discovery=False)
    albumId = findAlbumByName(service, albumName)
    if albumId:
        print(f"Found album '{albumName}' with ID: {albumId}")
        photos = getPhotosFromAlbum(service, albumId)
        i=0
        # 各画像をEagleにアップロード
        for photo in photos:
            print(photo[1]+"をアップロード")
            image_url = photo[0]
            filename = photo[1]
            upload_to_eagle(image_url, filename)
            i += 1
    else:
        print(f"Album '{albumName}' not found.")

if __name__ == "__main__":
    main()
```

:::note warn
コードが記述されたファイル（index.py）と同じフォルダに`client_secrets.json`を配置してください
:::

用意ができたら最後実行して完了です。

```shell
python index.py
```

初回のみブラウザが起動し、認証が求められるためそのまま認証を進めるとコードの処理が実行されます。

# コード解説

## モジュール・変数の指定

ここでは使用するモジュールと変数を指定しています。

EagleのAPIを使いやすいようPython Wrapperがあるため今回はこちらを活用します。
開発者さんに感謝！

https://pypi.org/project/EagleWrapper/

```python
from googleapiclient.discovery import build
import google.oauth2.credentials
import google_auth_oauthlib.flow
import json, os
from eaglewrapper import Eagle

albumName = "ALBUM_NAME" # <- アルバム名を入力
EAGLE_API_KEY = 'YOUR_EAGLE_API_KEY' # <- Eagle API Tokenを入力

SCOPES = ['https://www.googleapis.com/auth/photoslibrary']
API_SERVICE_NAME = 'photoslibrary'
API_VERSION = 'v1'
CLIENT_SECRET_FILE = 'client_secrets.json'
CREDENTIAL_FILE = 'credential.json'
EAGLE_API_ENDPOINT = 'http://localhost:41595/api/item/addFromURL'

eagle = Eagle()
```

## Credentialの取得

こちらの関数では、毎回`client_secrets.json`を使用してログインを行わなくても良いように、
初回は`client_secrets.json`を使って認証を行い`credential.json`を生成し、次回以降は`credential.json`を用いて認証するロジックとなっています。
[こちら](https://qiita.com/tokkun5552/items/38b4d678ccc3e2f796fa)のブログを参考にさせていただきました。

```python
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_local_server()
        with open(CREDENTIAL_FILE, mode='w') as f_credential_w:
            f_credential_w.write(credentials.to_json())
    return credentials
```

## アルバムから画像を取得

こちらの関数では、Googleフォトのアルバムから画像を取得するものになります。
1度の処理でまとめて取得するのではなく`nextPageToken`があれば無くなるまで取得し続ける処理となっています。
画像を取得と表現していますが、厳密にはアップロード用に使用するURLを取得しているためローカル環境に一時的に保存するような処理は走っていません。

```python
def getPhotosFromAlbum(service, albumId):
    photos = []
    nextPageToken = ''
    while True:
        body = {
            'albumId': albumId,
            'pageSize': 100,
            'pageToken': nextPageToken
        }
        response = service.mediaItems().search(body=body).execute()
        items = response.get('mediaItems', [])
        for item in items:
            photos.append([item['baseUrl'],item['filename']])
        nextPageToken = response.get('nextPageToken', None)
        if not nextPageToken:
            break
    return photos
```

## アルバムIDを取得

こちらの関数はGoogleフォトのアルバム名からアルバムIDを取得する処理になります。
アルバム一覧を取得し一致するものを検索します。

```python
def findAlbumByName(service, albumName):
    nextPageToken = ''
    while True:
        response = service.albums().list(pageSize=50, pageToken=nextPageToken).execute()
        albums = response.get('albums', [])
        for album in albums:
            if album['title'] == albumName:
                return album['id']
        nextPageToken = response.get('nextPageToken', None)
        if not nextPageToken:
            break
    return None
```

## Eagleに画像をアップロード

こちらの関数は、EagleにAPIを使用して取得した画像をアップロードしています。

```python
def upload_to_eagle(image_url, filename):
    response = eagle.add_from_url(image_url, filename)
    return response
```

## main関数

最後はmain関数となり、実際に処理のフローを記述しています。
ファイル名とアップロード用URLを含んだ2次元リストを作成することでそれぞれ同時に指定してアップロードが可能となります。

```python
def main():
    credentials = getCredentials()
    service = build(API_SERVICE_NAME, API_VERSION, credentials=credentials,static_discovery=False)
    albumId = findAlbumByName(service, albumName)
    if albumId:
        print(f"Found album '{albumName}' with ID: {albumId}")
        photos = getPhotosFromAlbum(service, albumId)
        i=0
        # 各画像をEagleにアップロード
        for photo in photos:
            print(photo[1]+"をアップロード")
            image_url = photo[0]
            filename = photo[1]
            upload_to_eagle(image_url, filename)
            i += 1
    else:
        print(f"Album '{albumName}' not found.")

if __name__ == "__main__":
    main()
```

# 参考

- [Eagle API](https://api.eagle.cool/)

# おわりに

今回、GoogleフォトからEagleへの移行にチャレンジしてみたのでせっかくだと思いブログにしました。

Eagleを使用することでプレゼン資料に使う素材や画像をタグ付けすることもできますし、PowerPointやPDFに対応しているので自分のものに限らず他人のプレゼン資料などを事前にタグ付けしておくことでタグベースの検索も可能です。

Eagleを使うことでアイデアを気軽に管理できるようになったのでぜひおすすめのツールです。

今まで、Googleフォト使ってたしアップロードしなおすのめんどくさいからEagle使わなくてもいいかな…と思っていた方はぜひ参考にしていただければと思います！

# 付記

Eagle API Keyの取得は不要だったためスキップいただいて大丈夫です。