はじめに
今回はGoogle PhotoからSynology Photoへの写真・動画ファイルの移行するにあたって、日付・位置情報データの移行に苦戦したため、そのことにフォーカスを当てて述べたいと思います。
Google Takeout Helperを使用?
こちらはSynology公式サイトにある方法です。
Google PhotoからTakeoutファイルをダウンロードするとメディアファイル(mp4,mov,jpeg,jpg,png,gif,...etc)とメタデータ(json)とが分離されてダウンロードされます。メタデータには撮影日時や撮影場所などの情報が入っています。
この状態のままSynology Photoにメディアファイルを移動しても、メタデータの情報がメディアファイルに組み込まれていないため、適切な撮影日時で表示してくれません。
そこで、メタデータをメディアファイルに組み込む必要があり、組み込みをしてくれるのがGoogle Takeout Helperになります。
Google Takeout Helperで発生した問題点
Takeout Helperを使用してどうしてもメタデータが付与されないメディアでてしまいます。
そのようなファイルは「date-unknown」というフォルダに格納されます。
これが私の場合非常に多く、この状態ではとてもSynology Photosに移行できませんでした。
この理由として考えられることは、
メタデータとメディアファイルのファイル名が異なり、Helperがそれを見つけられない
根本的な原因はこれになると思います。
メタデータファイルとメディアファイルの名前には以下のような傾向があります。Helperはこのファイル名の関係かメディアファイルにメタデータを付与します。
メディアファイル名:IMG_1234.mp4
メタデータファイル名:IMG_1234.mp4.supplemental-metadata.json
しかし、このファイル名の関係が崩れるとメタデータ付与に失敗します。
Google Photoではメディアファイルは同じ名前がであっても別々に管理することができますが、そのままTakeoutダウンロードしてしまうと、名前が重複してしまうので、ファイル名が自動的に変更されます。これにより、メディアファイルのファイル名がメタデータと異なってしまい、Helperでの付与が失敗してしまう現象が発生します。
メディアファイル名:IMG_1234(1).mp4
メタデータファイル名:IMG_1234.mp4.supplemental-metadata.json
また、別のケースでファイル名が長すぎることでファイル名が異なるケースも存在します。ファイル名が異なると、
メディアファイル名: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
# UNIXタイムスタンプをExif用の日時文字列に変換
def format_datetime(timestamp):
jst = timezone(timedelta(hours=9)) # JST(日本標準時)
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が不完全です'
# 拡張子によってタグの種類を切り替える
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':
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ファイルを取得(候補が1つだけの場合のみ採用)
def get_valid_json(media_filename, search_root):
# 46文字以上の長いファイル名には特別処理(Googleの命名仕様に対応)
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 = [] # メタデータ付与に失敗したファイルの記録リスト
# メディアファイルを再帰的に探索
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ファイルを探索し、見つかったメタデータを付与していきます。
付与前
付与後
おわりに
今回はSynology Photosでとりあえずできそうな暫定処理を加えてみました。それでも漏れてしまったものは手作業になりそう...もう少し工夫できるところがきっとあると思います。不足やご指摘がございましたらコメントくださると大変幸いです。