Google Photosからどうしても永続的な画像リンクを取得したい

# Picasa Webの時代

かつてGoogle PhotosはPicasa Webというサービスだった。そのころは画像の永続的なリンクを取得するのは極めてかんたんだった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/119ae099-6a57-6531-ef6b-929d7a18f914.png)

したがってブログの写真をPicasaに上げて、ここからリンクを取ってきて貼り付けるということはわりと普通に行われていた。

またPicasaという画像編集ソフトウェアとの密な連携もあり(というかそっちが先)、非常に多くのユーザーがいた。

# Google Photosに統合

いまはなきGoogle+との兼ね合いもあったのだろうが、Google Photosという新しいサービスに統合されることとなった。というより天下のGoogleもPicasa Webというレガシー化したシステムと比較的新しいGoogle Photosという２つのサービスを連携したまま維持することに限界を覚えたのだろう。じつのところこの前年には「Google+フォト」というGoogle Photosではないサービスも統合されている。

2016-02-13: [GoogleがついにPicasaの閉鎖へ、Google Photosへの移行ツールについては不明瞭な部分も | TechCrunch Japan](https://jp.techcrunch.com/2016/02/13/20160212google-is-finally-killing-picasa/)

Google Photosへの統合は極めて慎重に行われた。写真データは何もせずともGoogle Photosから閲覧できるようになったし、過去にブログに埋め込むためなどの理由で発行された埋め込みのURLも維持された。

しかしこのとき、写真をブログに埋め込むためのリンクを発行する機能は失われた。まあ無料静的画像アップローダとして使われるとマネタイズに困るのだろう。

# Picasa API

ではこの統合で永続的な画像リンクを取得する方法が失われたかといえばそうではなかった。Picasa Web時代のAPIは維持されていたのだ。WordpressのプラグインPhoto Express for Googleなんかはこれを利用していた。

しかしこれも2019年3月ついには廃止された。

[Picasa Web Albums Data API  |  Google Developers](http://web.archive.org/web/20190610053252/https://developers.google.com/picasa-web/)

# Google Picker API

ではPicasa API廃止で永続的な画像リンクを取得する方法が失われたかといえばそうではなかった。Google Picker APIだ。これによってpicasa webのころと同じURLが引き続き取得できた。はてなブログの[Google フォト貼り付け機能](https://staff.hatenablog.com/entry/2015/06/18/151000)はこれを利用していた。

しかし、ついにPicasa Web時代から受け継いできた画像URLの挙動が怪しくなった。

> https://staff.hatenablog.com/entry/2020/03/11/172942
> 2020年3月10日以降にGoogleフォト貼り付け機能を利用して、ブログに掲載した画像が一定時間後に非表示となる問題が発生しております。

一定時間後非表示になるというのはどうも単なる不具合だった様子があるが、その画像があるGoogleアカウントでログインしていないと一部の画像が見られない状態となったようだ。共有アルバムにある画像でかつ画像のURLを発行した時期とアルバムをその当時から指定して選んでいたかなどなどの要因があるようだ。

ref:

- [はてなブログで取得したGoogleフォトの画像URLが表示されない問題を解決 : トイレのうず／ブログ](https://1010uzu.com/blog/solved-issue-images-url-gotten-from-hatena-blog-not-displayed)
- [Googleフォト貼り付け機能の今後の対応につきまして - はてなブログ開発ブログ](https://staff.hatenablog.com/entry/2020/06/10/155523)

また、2021年3月末日を持ってPicker APIでのGoogle Photosからの画像リンク取得が終了すると主張しているブログがある。

> [【今月二度目】 Google Picker API がら取得した Google Photos の画像が利用できない、ヤァ!ヤァ!ヤァ! - A Day in the Life](https://secon.dev/entry/2020/03/29/132256/)
> 2020年4月7日追記
>
> メールで、2021年3月末日を持って、Picker API の Google Drive 以外の部分(Photos とか Maps)とかの終了のお知らせが来た。

ただこのブログが引用しているGoogle発表の一次情報にはそのような記載がみつけられていない。なんなら冒頭に現在の情勢(多分covid19感染拡大)を鑑み延期とある。WebArchiveも探したが、Googleの嫌がらせにより本文が過去にさかのぼって見れない。~~プレス情報をアーカイブから見れないように細工して公開するってどういう了見だ、Googleよ~~。
[Upcoming changes to the Google Drive API and Google Picker API | Google Cloud Blog](https://cloud.google.com/blog/products/application-development/upcoming-changes-to-the-google-drive-api-and-google-picker-api)

# 関連事項: Googleフォトからはてなフォトライフへ移行予約機能

Google側の仕様変更に対し、ブログ内のGoogle Photosから読み込んでいる画像をすべてはてなフォトライフに移して、リンクを置き換えるという機能の予約をはじめた。この方法を使った場合なんとはてなフォトライフの容量としてはカウントされない。
[Googleフォトからはてなフォトライフへ移行予約する - はてなブログ ヘルプ](https://help.hatenablog.com/entry/import-from-google-picker)

# 関連事項: Google Photos、容量無制限ではなくなる

画像アップロード時に画像の再圧縮を許容することで無制限に画像をアップロードできていたが、どうも終焉のときが来たらしい。まあそうなるだろうてことはなんとなくわかってた。下の記事を持ち出すまでもなく「無料」は永遠ではない。

[「無料」は永遠ではない？ 「Google フォト」が容量無制限の保存を終了する意味 | WIRED.jp](https://wired.jp/2020/11/12/google-photos-free-unlimited-storage-ends/)

# 現在も残るおそらく唯一の永続的な画像リンクを取得する方法

Piacasa Web時代からのURLはどうやら終わりの始まりといった様相を呈している。繰り返しになるが、Googleとしては静的画像ホスティングサイトとして使われたのでは、マネタイズが難しいということであろう。競合サービスもいつのまにか消えてなくなるか有料化している。

しかしながら、Google Cloud StorageやAmazon Photo、その他サービスや自前でのホスティングに移行するにせよ、すぐには身動きがとれない。

ほんとうにもう永続的な画像リンクを取得する方法は残っていないのだろうか。いや、残っている。

Google Photosで共有アルバムに追加されている写真を開いてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/53488383-71bb-c43b-6e10-eedfa06c415b.png)

そして右クリックをして「画像のURLをコピー」とすると以下のようなURLがとれる。

```
https://lh3.googleusercontent.com/pw/ACtC-3cJSKyhzh-KLcQnDgE567O_OakIcXFuLfvwUVJx9p_aze8qwBHdWhXF2JPRrdgfU-a2fJInRRHAPDphk2CqnpCq5BcTdEbJi6GUphjOuc467F1xRFhNfAem5TDPit3gcjQDxxAyrhUoDo8vh4MZKIDi=w1220-h915-no?authuser=0
```

若干いらない情報が含まれているので本当に必要な部分はここだ

```
https://lh3.googleusercontent.com/pw/ACtC-3cJSKyhzh-KLcQnDgE567O_OakIcXFuLfvwUVJx9p_aze8qwBHdWhXF2JPRrdgfU-a2fJInRRHAPDphk2CqnpCq5BcTdEbJi6GUphjOuc467F1xRFhNfAem5TDPit3gcjQDxxAyrhUoDo8vh4MZKIDi=w1220-h915
```

このURLは実は永続的である。ここ数年の間継続して閲覧できている。

## URL取得を自動化したい

こういうURLはAPIかなんかで取れたりしないものかと誰しもが考えるが残念ながらそうはなっていない。つまりウェブスクレイピングするしかない。

通常のアルバムは作成した本人しか見られないため、ログインしなければならず自動化が難し。しかし共有アルバムならばだれでもURLさえわかれば見ることができる。

そこで共有アルバムのURLから永続的な画像のURLをとることを考える。

まず共有アルバムのURLを普通にwgetしてみる。

```bash
$ wget https://photos.app.goo.gl/oJEMCo5g5eUptS2fA
```

テキストエディタで開いてみる。`AF_initDataCallback`で検索すると以下のような部分が見つかる。

```html
<script nonce="oMza4KyxCVu6ixq4ZVw9Og">AF_initDataCallback({key: 'ds:0', isError:  false , hash: '1', data:(中略)
, sideChannel: {}});</script>
```

HTMLのなかに埋め込まれて書かれているJavaScript部分に`AF_initDataCallback関数にオブジェクトを渡している箇所があり、このオブジェクトに目的のデータが含まれている。

このオブジェクトの構造はこの一年で2度も変化しており(2020/06/15と2020/08/25)、さらにはJavaScriptのコメント文が割り込んだりもして、単なる正規表現で抜き出すのは大変なので、オブジェクト部分をJSON5としてパースして`data`プロパティの配列を取得する。この要素は難読化ないしデータ量削減のためにさらに配列で書かれている。どうにか読み解く。

`data`プロパティの配列を`d`とすると

| 要素 | 内容 |
|-----|------|
| 0   | 不明 |
| 1   | アルバムに含まれる画像のうち500件分の情報 |
| 2   | next page tokenのようななにか |

のような構造になっている。

`d[1]`は配列になっていて、各要素は画像一枚一枚に対応する。`i`番目の`d[1]`の要素を`e`とすると、

| 要素 | 内容 |
|-----|------|
| 0   | UID Google Photos APIで見えるものとは一致しない |
| 1   | 詳細情報 |
| 2   | 画像の更新日時 |
| 5   | 画像をアルバムに追加した日時 |

他の要素はわからない。

`e[1]`もまた配列になっている。これを`detail`とすると、


| 要素 | 内容 |
|-----|------|
| 0   | 永続的な画像URL |
| 1   | 画像の幅 |
| 2   | 画像の高さ |

こうしてめでたくURLを手にすることができた

### next page tokenのようななにかについて

アルバムに含まれる画像が500以上のとき、500を超える情報は`d[1]`にはなく、`d[2]`で取れるnext page tokenのようななにかをつかってあらたにリクエストをなげないといけないらしい。

実際ブラウザでネットワーク通信を覗くとそういうリクエストが飛んでいる。

![req](https://user-images.githubusercontent.com/10869046/79943095-006e8480-84a3-11ea-8bf9-0160db55b6f5.png)
しかし`curl`で再現を試みたところなんかうまく行かなかった。多分クエリ文字列が大事なんだろうが、こいつらがどこから得られたのかがわからない。

わかったという人は
https://github.com/yumetodo/google-photos-album-image-url-fetch/issues/3
までご一報ください。

## スクレイピングライブラリを作った

というわけで上に書いたような作業を自動化するライブラリを作った。CIを毎日回すようにしているので、仕様変更があってもアラートが来るようにはなっている。npmで公開している。

[yumetodo/google-photos-album-image-url-fetch](https://github.com/yumetodo/google-photos-album-image-url-fetch)
[google-photos-album-image-url-fetch - npm](https://www.npmjs.com/package/google-photos-album-image-url-fetch)

肝心のパース部分は上に書いたことを忠実にやっている。なお先に述べたとおりこの構造は仕様変更が多いので、
https://github.com/yumetodo/google-photos-album-image-url-fetch/blob/master/src/impl.ts
を参照するようにしてほしい。

```typescript
import { request } from 'gaxios';
import { ImageInfo } from './imageInfo';
import { AbortSignal } from 'abort-controller';
import { parse } from 'json5';
export async function getSharedAlbumHtml(albumSharedurl: string, signal?: AbortSignal): Promise<string> {
  return request<string>({
    url: albumSharedurl,
    retryConfig: { retry: 4, retryDelay: 1000 },
    retry: true,
    signal: signal,
  }).then(r => r.data);
}
export function parsePhase1(input: string): string | null {
  const re = /<script nonce="[^"]+">AF_initDataCallback\((.+data\s*:\s*[^<]+})\s*\)\s*;\s*<\/script>/;
  const s = re.exec(input);
  if (null === s || s.length !== 2) {
    return null;
  }
  return s[1];
}
export function parsePhase2(input: string): unknown {
  try {
    return parse(input);
  } catch (_) {
    return null;
  }
}
export interface ContainData {
  data: unknown;
}
export const isContainData = (o: unknown): o is ContainData => typeof o === 'object' && o != null && 'data' in o;
const rawIsArray = Array.isArray;
const isArray = (a: unknown): a is unknown[] => rawIsArray(a);
export function parsePhase3(input: unknown): ImageInfo[] | null {
  if (!isContainData(input)) {
    return null;
  }
  const d = input.data;
  if (!isArray(d) || d.length < 1) {
    return null;
  }
  const arr = d[1];
  if (!isArray(arr)) {
    return null;
  }
  return arr
    .map(e => {
      if (!isArray(e) || e.length < 6) {
        return null;
      }
      const uid = e[0];
      const imageUpdateDate = e[2];
      const albumAddDate = e[5];
      if (typeof uid !== 'string' || typeof imageUpdateDate !== 'number' || typeof albumAddDate !== 'number') {
        return null;
      }
      const detail = e[1];
      if (!isArray(detail) || detail.length < 3) {
        return null;
      }
      const url = detail[0];
      const width = detail[1];
      const height = detail[2];
      if (typeof url !== 'string' || typeof width !== 'number' || typeof height !== 'number') {
        return null;
      }
      return {
        uid: uid,
        url: url,
        width: width,
        height: height,
        imageUpdateDate: imageUpdateDate,
        albumAddDate: albumAddDate,
      };
    })
    .filter((e: ImageInfo | null): e is ImageInfo => !(null === e));
}
```

# 永続的リンクの未来

今この瞬間、永続的に見えている、紹介したURLの将来は暗い。

そもそもGoogle Picker API で取得していた画像のURLの動作は予告なく変更された。Google Photos APIでは永続的なリンクが取得できない。

Googleフォトからはてなフォトライフへ移行予約する機能についてはてなに問い合わせをしたのだが、今回紹介したリンクのものも含めて移行するとの回答があった。

完全に閲覧できなくなる日が来る前に、なんらかの手段に移行し終えなければならない。その日はいつ来るのかもわからないのだから。