Google フォトからSynology Photosへの移行に苦戦した話

# はじめに

今回はGoogle フォトからSynology Photosへの写真・動画ファイルの移行するにあたって、日付・位置情報の移行に苦戦したため、この点にフォーカスを当てて述べたいと思います。

# Google Takeout Helperを使用？

まず、Google フォトからTakeoutファイルをダウンロードするとメディアファイル(mp4,mov,jpeg,png,...etc)とメタデータファイル(json)とが分離されてダウンロードされます。メタデータファイルには撮影日時や撮影場所などの情報が入っています。
この状態のままSynology Photosにメディアファイルを移動しても、メタデータがメディアファイルに組み込まれていないため、適切な撮影日時で表示してくれません。
そこで、メタデータをメディアファイルに組み込む必要があり、組み込みをしてくれるのがGoogle Takeout Helperになります。

https://kb.synology.com/ja-jp/DSM/tutorial/How_do_I_migrate_photos_from_Google_Photos


# Google Takeout Helperで発生した問題点

しかし、Takeout Helperを使用してどうしてもメタデータが付与されないメディアでてしまいます。
そのようなファイルは「date-unknown」というフォルダに格納されます。
これが私の場合非常に多く、この状態ではとてもSynology Photosに移行できませんでした。
この理由として考えられることは、

**メディアファイルとメタデータファイルのファイル名関係が崩れてしまい、Helperが適切なファイルを見つけられない**

根本的な原因はこれになると思います。

メタデータファイルとメディアファイルの名前には以下のような傾向があります。Helperはこのファイル名の関係から、メディアファイルにメタデータを付与します。

    メディアファイル名：IMG_1234.mp4
    メタデータファイル名：IMG_1234.mp4.supplemental-metadata.json

しかし、このファイル名の関係が崩れるとメタデータ付与に失敗します。
Google フォトではメディアファイルは同じ名前がであっても別々に管理することができますが、そのままTakeoutをダウンロードしてしまうと、名前が重複してしまうので、ファイル名が自動的に変更されます。
これにより、メディアファイルとメタデータファイルの関係が崩れてしまい、Helperでの付与が失敗してしまう現象が発生します。

    メディアファイル名：IMG_1234(1).mp4
    メタデータファイル名：IMG_1234.mp4.supplemental-metadata.json


また、別のケースでメディアファイル名が長すぎることでTakeoutダウンロード時に、メタデータファイル名が切れてしまう現象もあります。

    メディアファイル名：IMG_1234512345123451234512345123.mp4
    メタデータファイル名：IMG_1234512345123451234512345123.mp4.supplemental-met.json


# 対策

Takeout Helperを使用せずに、そのようなある程度不規則なメタデータファイルのファイル名でも、
メディアファイルと紐づくよう、正規表現をつかった簡易プログラムを作成しました。

# 動作環境

OS：Windows11
インストール環境：Python、exiftool、VSCode

# フォルダ構成

構成は適当ですが、今回は以下のようにしました。add_metadataというフォルダを作成し、その中にプログラム(add_metadata.py)とexiftool、Takeoutを格納しています。

```
Download/
    └── add_metadata/
        ├── add_metadata.py
        ├── exiftool.exe
        ├── exiftool_files/
        └── Takeout/
            └── Google フォト/
                ├── 20XX0000_〇〇旅行/
                ├── 20YY1111_〇〇展示会/
                ├── ...
                ├── Photos from 20XX/
                └── Photos from 20YY/
         
```

# プログラム(add_metadata.py)

~~~
import os
import json
import subprocess
import re
from datetime import datetime, timezone, timedelta
import csv

# --- 設定 ---
MEDIA_EXTS = ['.jpg', '.jpeg', '.png', '.mov', '.mp4', '.heic']  # 対象とするメディア拡張子
MEDIA_DIR = 'Takeout'       # メディアファイルのルートディレクトリ
NOT_FOUND_CSV = 'not_found_metadata.csv'  # メタデータ付与失敗時のCSVログ出力ファイル名

# --- サポート関数 ---

# 指定されたファイル名に一致するJSON候補を、ディレクトリ内から探す
def find_json_candidates(base_name, search_root):
    candidates = []
    for root, _, files in os.walk(search_root):
        print(f"\n[探索中] メディアディレクトリ: {root}")
        for file in files:
            if not file.endswith('.json'):
                continue
            if file.startswith(base_name):
                candidates.append(os.path.join(root, file))
    return candidates

# ファイル名を正規化する（重複番号や編集済み表記を除去）
def normalize_name(filename):
    base, ext = os.path.splitext(filename)
    cleaned = re.sub(r'\(\d+\)|-編集済み', '', base, flags=re.IGNORECASE)
    return cleaned + ext

# タイムスタンプ（秒）をExif用の日付文字列、年、月に変換する
def format_datetime(timestamp):
    jst = timezone(timedelta(hours=9))  # JST (UTC+9)
    dt = datetime.fromtimestamp(int(timestamp), tz=jst)
    return dt.strftime("%Y:%m:%d %H:%M:%S"), dt.strftime("%Y"), dt.strftime("%m")

# ExifToolを使ってメディアファイルにメタデータ（日時・位置）を埋め込む
def embed_metadata(filepath, metadata, ext):
    if 'photoTakenTime' not in metadata or 'timestamp' not in metadata['photoTakenTime']:
        return None, None, None, 'photoTakenTimeが存在しません'

    cmd = ['exiftool']
    timestamp = metadata['photoTakenTime']['timestamp']
    dt_str, year, month = format_datetime(timestamp)

    try:
        lat = metadata["geoData"]["latitude"]
        lon = metadata["geoData"]["longitude"]
        alt = metadata["geoData"].get("altitude", 0)
    except KeyError:
        return None, None, None, 'geoDataが不完全です'

    # 拡張子に応じたExifタグの指定
    if ext in ['.jpg', '.jpeg', '.mov', '.mp4', '.heic']:
        cmd += [
            f'-DateTimeOriginal={dt_str}',
            f'-CreateDate={dt_str}',
            f'-ModifyDate={dt_str}',
            f'-GPSLatitude={lat}',
            '-GPSLatitudeRef=N',
            f'-GPSLongitude={lon}',
            '-GPSLongitudeRef=E',
            f'-GPSAltitude={alt}'
        ]
    elif ext == '.png':
        # PNGはXMPメタデータで対応
        cmd += [
            f'-XMP:DateTimeOriginal={dt_str}',
            f'-XMP:GPSLatitude={lat}',
            f'-XMP:GPSLongitude={lon}',
            f'-XMP:GPSAltitude={alt}'
        ]
    else:
        return None, None, None, '未対応の拡張子'

    cmd += ['-overwrite_original', filepath]  # 元ファイルを上書き
    subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    return year, month, dt_str, None

# 指定メディアファイルに対応する正しいJSONファイルを探す
def get_valid_json(media_filename, search_root):
    # Google Takeoutの46文字超ファイル名特例
    if len(media_filename) >= 40:
        print(f"  [46文字特例] '{media_filename[:46]}' に対する JSON を検索中...")
        candidates = find_json_candidates(media_filename[:46], search_root)
        if not candidates:
            return None
        if len(candidates) == 1:
            return candidates[0]
        return None

    # ファイル名正規化による検索
    normalized_name = normalize_name(media_filename)
    base_name, _ = os.path.splitext(normalized_name)

    for name in [media_filename, normalized_name, base_name]:
        print(f"  [一致候補探索] '{name}' に対する JSON を検索中...")
        candidates = find_json_candidates(name, search_root)
        if not candidates:
            continue
        if len(candidates) == 1:
            return candidates[0]
    return None

# --- メイン処理 ---

not_found_list = []  # メタデータ付与失敗ファイルのログ用リスト

# メディアファイルを探索し、対応するJSONからメタデータを埋め込む
for root, _, files in os.walk(MEDIA_DIR):
    print(f"\n[探索中] メディアディレクトリ: {root}")
    for file in files:
        ext = os.path.splitext(file)[1].lower()
        if ext not in MEDIA_EXTS:
            continue  # 対象拡張子以外はスキップ

        print(f"\n[処理開始] ファイル: {file}")
        media_path = os.path.join(root, file)

        # 対応するJSONを取得
        json_path = get_valid_json(file, root)

        if not json_path:
            # JSONが見つからなかった場合
            print(f"  [スキップ] JSONが見つかりません: {file}")
            not_found_list.append({
                'directory': root,
                'filename': file,
                'reason': '対応するJSONが見つかりません'
            })
            continue

        try:
            with open(json_path, 'r', encoding='utf-8') as jf:
                metadata = json.load(jf)
        except Exception:
            # JSON読み込みエラー
            print(f"  [スキップ] JSONの読み込みに失敗: {json_path}")
            not_found_list.append({
                'directory': root,
                'filename': file,
                'reason': 'JSONの読み込みに失敗'
            })
            continue

        # メタデータ埋め込み処理
        year, month, dt_str, error = embed_metadata(media_path, metadata, ext)

        if error:
            # エラーが発生した場合
            print(f"  [エラー] {file}: {error}")
            not_found_list.append({
                'directory': root,
                'filename': file,
                'reason': error
            })
        else:
            # 正常に完了
            print(f"  [OK] {file} にメタデータ付与成功 ({dt_str})")

# --- 失敗ログCSV出力 ---
if not_found_list:
    with open(NOT_FOUND_CSV, 'w', newline='', encoding='utf-8') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=['directory', 'filename', 'reason'])
        writer.writeheader()
        writer.writerows(not_found_list)
    print(f"\n[完了] メタデータ付与に失敗したファイル一覧を {NOT_FOUND_CSV} に保存しました。")
else:
    print("\n[完了] すべてのファイルにメタデータを正常に付与できました。")

~~~

# プログラム実行結果

python add_metadata.pyと実行するとこのようにTakeout内のメディアファイルを探索し、メディアファイルが見つかったら同一ディレクトリ内にあるjsonファイルを探索し、見つかったメタデータを付与していきます。

![スクリーンショット 2025-06-08 133927.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246786/3c2d7a52-8b60-43af-873b-30ae6dcc81a4.jpeg)

## 付与前
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246786/82004b5b-041d-4d5d-bd75-03d01eef428e.png)

## 付与後
![スクリーンショット 2025-06-08 134818.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246786/d2f09b8e-396d-4174-b8fa-27f685f54a9c.png)


# おわりに

今回はSynology Photosでとりあえずできそうな暫定処理を加えてみました。私の場合はこれで一通りメタデータを付与できましたが、例外があるかもしれません。不足やご指摘がございましたらコメントくださると大変幸いです。