Closed3

Googleフォトのデータ移行

Hidden comment
omi-otsukeomi-otsuke

第2版

やり方を変えたので版を改めて整理します。

動機

Googleの無料ストレージ枠をそろそろ使い切ってしまいそうだったので、昔Googleフォトにアップロードしていた昔の写真や動画をNASに移行しようと思いました。

環境

OS: macOS 12
gpth: 3.4.3
exiftool: 13.30

問題

Google Takeoutを使って写真・動画ファイルをエクスポートした後にそのファイルをNASに移動させるだけかと思いきや、ファイルの最終更新日時がダウンロード日時に書き変わるということがわかりました。このためNAS側で日付順のソートをすると、古い写真と最近の写真が混ざってしまう問題が発生しました。
またオリジナルファイルとは別に拡張子を除いたファイル名の末尾に「-編集済み」の文字列が追加されたファイルがあり、どのように扱えば良いかわからない問題もありました。

問題の調査

最終更新日時の問題について、どうやらこの日付以外のメタデータは書き変わっていないことがわかりました。そのため元の撮影日時に書き戻すだけでよさそうです。
またファイル名末尾に「-編集済み」がついたファイルの扱いについて、Google フォトの編集機能で編集するために生成されたファイルだと考えられます。私は当該機能を使っていないため、ファイルを削除する対応でいいと判断しました。

日付変更方法の調査

色々探したところ、以下のツールを使って最終更新日時を変更している方が多くいました。

  • Pythonのpillowライブラリ
  • コマンドラインツールexiftool
  • コマンドラインツールGoogle Photos Takeout Helper (gpth)

この中から私は3つ目のgpthを使おうと思いました。決め手としては一番操作方法が簡単そうだったからです。

gpthを使用した結果

初めに「-編集済み」がついたファイルを削除しておくために以下のコマンドを実行します。

# 削除前の確認用
find . -type f -name '*-編集済み*'
# 削除実行用
find . -type f -name '*-編集済み*' -delete

次にgpthREADMEに従ってツールを実行します。
また実行前後に下記コマンドでファイル数を集計します。

# jsonを除いた全ファイル数集計用
find . -type f -not -name '*.json' | wc -l
# 拡張子別に分類するとき用
find . -type f -iname '*.(拡張子)' | wc -l

gpth実行前後のファイル数確認

拡張子 実行前 実行後
jpg 1800 1748
png 20 20
gif 9 9
HEIC 42 42
mp4 58 56
MOV 6 6
合計 1935 1881

実行前と後で合計値が合いませんでした。

日付変更結果の確認

いくつかのメディアファイルは日付変更ができなかったようです。
日付変更後のメディアファイルはALL_PHOTOSディレクトリ、変更できなかったファイルはALL_PHOTOS配下のdate-unknownディレクトリに移動されます。
ファイルの日付をいくつ変えられた/変えられなかったか確認するため、それぞれのディレクトリで下記コマンドを実行して集計します。

find . -type f -maxdepth 1 | wc -l

変更完了(ALL_PHOTOS直下):1771-1=1770 (.DS_Storeファイルを除いた数)
変更失敗(date-unknown):111
合計:1881

数が合わない問題の調査

Google フォトのアルバム機能でグループ分けされたメディアファイルが問題の原因でした。
実行前は実ファイルが「Photos from YYYY」フォルダ、アルバムフォルダそれぞれに入っていたのですが、実行後は「ALL_PHOTOS」フォルダに実ファイルが集約され、アルバムフォルダにあったファイルは代わりにシンボリックリンクが置かれるようです。
つまり元々2つ存在していた実ファイルが1つに減ったことで差分が生じていたということでした。

日付が変更されないファイルの調査

今のところ要因と思われるものは以下の通りです。

  1. ファイル名が長すぎて、対応するメタデータファイルの名前が途中で切れてしまっている
    • 実ファイルとメタデータファイルが紐付かなくなってしまった?
  2. 動画ファイルである
    • 動画ファイルのメタデータ形式に対応していない?

しかし上記要因に当てはまらないファイルがまだ多数あり、その調査に時間をかけても仕方ないと思ったため、諦めてexiftoolを使って残ったファイルの日付を変更しようと思います。。。
なおメタデータファイルがあっても日付が変更されない問題はgpth作者のGitHubでもissueにあがっているようで、もしかしたら今後修正版がリリースされるかもしれません。

omi-otsukeomi-otsuke

exiftoolの実行

date-unknownディレクトリに移動されたファイルをそのまま使おうと思いましたが、重複ファイルがリネームされておりメタデータファイルとの紐付けができなくなっていました。そのため結局最初にGoogle Takeoutからダウンロードしてきたファイル群を対象にやっていくことにしようと思います。。。

前準備

気を取り直して準備に取り掛かります。まずgpthの時と同様に編集済みの文字列を削除します。

find . -type f -name '*-編集済み*' -delete

次に実ファイルとメタファイルを紐づけられるよう、メタファイル名フォーマットを統一させます。
単純にGoogle Takeoutからダウンロードしてきたままのメタファイル名は以下のように実ファイル名と拡張子の後に.supplemental-metadata.jsonの文字列が付加されています。

<実ファイル名>.<拡張子>.supplemental-metadata.json

しかし実ファイル名が長い場合、付加される文字列が例えば.supplementa.jsonのように途中で途切れています。
また同名の実ファイルが同じ年に複数ある場合、Takeoutは実ファイルとメタファイルにそれぞれ以下のようなフォーマットでファイル名をリネームします。

# 実ファイル名
<実ファイル名>(<通し番号>).<拡張子>
# メタファイル名
<実ファイル名>.<拡張子>.supplemental-metadata(<通し番号>).json

<通し番号>は1から始まる整数です。
なお実ファイル名が長い場合に「supplemental-metadata」と通し番号がどう縮まるのかについて気になりましたが、私の環境ではそのような実ファイルが無かったため未確認です。
以上のように大きく分けて2つのルールにの基づいて実ファイル名とメタファイル名が決まっており、それを統一させるために付加される「supplemental-metadata」またはこれが短縮された「supplementa」のような文字列を削除します。
さらに通し番号は実ファイル名の直後に来るように変更します。
最終的に以下のようにしたいと思います。

# 実ファイル名
<実ファイル名>.<拡張子>
<実ファイル名>(<通し番号>).<拡張子>  # 同名ファイルの2つ目以降
# メタファイル名
<実ファイル名>.<拡張子>.json
<実ファイル名>(<通し番号>).<拡張子>.json  # 同名ファイルの2つ目以降

これを実現するためにpythonスクリプトrename_metadata.pyを作成しました。
スクリプトの概要を説明すると以下の5ステップになっています(変数の定義や最終結果出力の部分は省いています)。

  1. ディレクトリ内のファイルを種類毎のリストに代入
  2. 基本形式メタデータファイルとメディアファイルを照合してリネーム
  3. 番号付き形式メタデータファイルとメディアファイルを照合してリネーム
  4. 基本形式メタデータファイルでリネームされなかったファイルを標準出力
  5. 番号付き形式メタデータファイルでリネームされなかったファイルを標準出力

実際のスクリプトは以下の通りです。

rename_metadata.py
import os
import re

# メディアファイルとメタデータファイルが置かれているディレクトリ
TARGET_DIR = "."

# 正規表現パターン
pattern_media_basic = re.compile(
    r"^(?P<basename>[^()]+)\.(?P<ext>jpg|JPG|png|PNG|gif|GIF|heic|HEIC|mp4|MP4|mov|MOV)$"
)
pattern_media_numbered = re.compile(
    r"^(?P<basename>[^()]+)\((?P<num>\d+)\)\.(?P<ext>jpg|JPG|png|PNG|gif|GIF|heic|HEIC|mp4|MP4|mov|MOV)$"
)
pattern_meta_basic = re.compile(
    r"^(?P<basename>[^()]+)\.(?P<ext>jpg|JPG|png|PNG|gif|GIF|heic|HEIC|mp4|MP4|mov|MOV)(?P<suffix>[.supplemental\-metadata]*)\.json$"
)
pattern_meta_numbered = re.compile(
    r"^(?P<basename>[^()]+)\.(?P<ext>jpg|JPG|png|PNG|gif|GIF|heic|HEIC|mp4|MP4|mov|MOV)(?P<suffix>[.supplemental\-metadata]*)\((?P<num>\d+)\)\.json$"
)

files = []
matches_media_basic = []
matches_media_numbered = []
matches_meta_basic = []
matches_meta_numbered = []
unmatched_files = []

# ディレクトリ内のファイルを種類毎のリストに代入
for file_name in os.listdir(TARGET_DIR):
    match_media_basic = pattern_media_basic.match(file_name)
    match_media_numbered = pattern_media_numbered.match(file_name)
    match_meta_basic = pattern_meta_basic.match(file_name)
    match_meta_numbered = pattern_meta_numbered.match(file_name)

    if os.path.isfile(os.path.join(TARGET_DIR, file_name)):
        files.append(file_name)

    if match_media_basic:
        matches_media_basic.append(match_media_basic)
    elif match_media_numbered:
        matches_media_numbered.append(match_media_numbered)
    elif match_meta_basic:
        matches_meta_basic.append(match_meta_basic)
    elif match_meta_numbered:
        matches_meta_numbered.append(match_meta_numbered)
    else:
        unmatched_files.append(file_name)
        print(f"Unmatched file: {file_name}")

renamed_files_basic = []

# 基本形式メタデータファイルとメディアファイルを照合してリネーム
for match_meta_basic in matches_meta_basic:
    basename_meta, ext_meta, suffix_meta = match_meta_basic.groups()
    for match_media_basic in matches_media_basic:
        basename_media, ext_media = match_media_basic.groups()
        if basename_meta == basename_media and ext_meta == ext_media:
            new_filename = f"{basename_meta}.{ext_meta}.json"
            print(f"Renaming: {match_meta_basic.string} -> {new_filename}")
            os.rename(
                os.path.join(TARGET_DIR, match_meta_basic.string),
                os.path.join(TARGET_DIR, new_filename),
            )
            renamed_files_basic.append(match_meta_basic)

renamed_files_numbered = []

# 番号付き形式メタデータファイルとメディアファイルを照合してリネーム
for match_meta_numbered in matches_meta_numbered:
    basename_meta, ext_meta, suffix_meta, num_meta = match_meta_numbered.groups()
    for match_media_numbered in matches_media_numbered:
        basename_media, num_media, ext_media = match_media_numbered.groups()
        if (
            basename_meta == basename_media
            and ext_meta == ext_media
            and num_meta == num_media
        ):
            new_filename = f"{basename_meta}({num_meta}).{ext_meta}.json"
            print(f"Renaming: {match_meta_numbered.string} -> {new_filename}")
            os.rename(
                os.path.join(TARGET_DIR, match_meta_numbered.string),
                os.path.join(TARGET_DIR, new_filename),
            )
            renamed_files_numbered.append(match_meta_numbered)

skipped_files_basic = []

# 基本形式メタデータファイルでリネームされなかったファイルを標準出力
for match_meta_basic in matches_meta_basic:
    if match_meta_basic not in renamed_files_basic:
        print(f"Skipped: {match_meta_basic.string}")
        skipped_files_basic.append(match_meta_basic)

skipped_files_numbered = []

# 番号付き形式メタデータファイルでリネームされなかったファイルを標準出力
for match_meta_numbered in matches_meta_numbered:
    if match_meta_numbered not in renamed_files_numbered:
        print(f"Skipped: {match_meta_numbered.string}")
        skipped_files_numbered.append(match_meta_numbered)

# 結果サマリーの出力
print("\nSummary:")
print(f"Total files processed: {len(files)}")
print(f"Total media files (basic): {len(matches_media_basic)}")
print(f"Total media files (numbered): {len(matches_media_numbered)}")
print(f"Total metadata files (basic): {len(matches_meta_basic)}")
print(f"Total metadata files (numbered): {len(matches_meta_numbered)}")
print(f"Total unmatched files: {len(unmatched_files)}")
print(f"Renamed basic files: {len(renamed_files_basic)}")
print(f"Renamed numbered files: {len(renamed_files_numbered)}")
print(f"Skipped basic files: {len(skipped_files_basic)}")
print(f"Skipped numbered files: {len(skipped_files_numbered)}")

このスクリプトによってほぼ全てのメタファイルを期待通りにリネームできました。
期待通りにならなかった残りのファイルは手動で直します。

実行

実行前の状態をCSVファイルexiftool_before.csvに出力します。

exiftool -csv -ext jpg -ext png -ext gif -ext heic -ext mp4 -ext mov -r . > exiftool_before.csv

それでは以下のスクリプトで最終更新日時を変更します。
概要としてはメディアファイルのDateTimeOriginalFileModifyDateというタグに対してメタファイルのPhotoTakenTimeTimestampというタグのデータを割り当てるということをしています。

exiftool.sh
#!/bin/zsh

exiftool -d "%s" -tagsfromfile %d%f.%e.json \
"-DateTimeOriginal<PhotoTakenTimeTimestamp" \
"-FileModifyDate<PhotoTakenTimeTimestamp" \
-overwrite_original \
-ext jpg -ext png -ext gif -ext heic -ext mp4 -ext mov \
-api largefilesupport=1 \
-r .

またiPhoneのLive Photos機能で撮影した写真について、HEICとMP4の2ファイルに対して1つのメタファイルが対応するという形式でした。なおメタファイルに付加される実ファイルの拡張子はHEICでした。これに対応するためexiftool.shを実行した後に別途下記スクリプトを実行しました。

exiftool_for_live_photos.sh
#!/bin/zsh

exiftool -d "%s" -tagsfromfile %d%f.HEIC.json \
"-DateTimeOriginal<PhotoTakenTimeTimestamp" \
"-FileModifyDate<PhotoTakenTimeTimestamp" \
-overwrite_original \
-ext mp4 \
-api largefilesupport=1 \
-r .

各スクリプトの実行中に下記2種類の警告が出ました。これらの警告に対して変更後のファイルが期待通りに変更されていること、想定外の変更がないこと、見た目の変化がないことを確認できたため問題ないと判断しました。

Warning: [minor] Maker notes could not be parsed - <メディアファイルパス>
Warning: [minor] Entries in IFD0 were out of sequence. Fixed. - <メディアファイルパス>

実行前と同様に実行後の状態をCSVファイルexiftool_after.csvに出力します。

exiftool -csv -ext jpg -ext png -ext gif -ext heic -ext mp4 -ext mov -r . > exiftool_after.csv

結果確認

実行前後で比較して想定通りに日付を変更できたか確認します。
まずは変更させたい項目が変更されていることを確認します。

  • 全メディアファイル数: 1881
  • DateTimeOriginal: 196; 元々オリジナルのデータが入っているファイルもあったため、ファイル全量とは一致していないものの問題なし。
  • FileModifyDate: 1881; 全メディアファイル数と一致しており問題なし。

次に変更させたい項目以外に変更されていた項目を確認します。
ただ全ての項目を正確に調査しようとすると膨大な時間が必要になるため、ざっくりと調べるだけにします。最終的に画像や動画が問題なく見れることで正常と判断します。

  • ColorSpace: 40; (無し) -> Uncalibrated; 画像は問題なく表示できた。
  • ComponentsConfiguration: 39; (無し) -> Y, Cb, Cr, -; 画像は問題なく表示できた。
  • ExifByteOrder: 32; (無し) -> Big-endian (Motorola, MM); 画像は問題なく表示できた。
  • ExifVersion: 40;(無し) -> 232; 画像は問題なく表示できた。
  • FileAccessDate: 1925; 1881よりも大きな数になっているが変更対象外のファイルも更新されたためである。対象のファイルが全て更新されているため異常ではない。
  • FileInodeChangeDate: 1881 最終更新日を変更したことによりinode情報が変更されることは当然なので問題なし。
  • FileSize: 21; 数KBの増加。メタデータ付加によるものと思われる。画像は問題なく表示できた。
  • GIFVersion: 8; 87a -> 89a; 画像は問題なく表示できた。
  • MediaDataOffset: 62; 全てmp4ファイル。違いがあっても動画ファイルの見た目には影響がない模様[1]

    When ExifTool writes a MOV/MP4 video it puts the media data at the end of the file. I believe that some metadata readers prefer it that way, but I don't think it is worth changing all your files to have this structure.

  • MediaDataSize: 42; 全てHEICファイルであり、値が2(単位はbyte?)増えた状態。画像は問題なく表示できた。
  • OtherImageStart: 78; 全てjpegファイル。値の変化は32や64の増加が多かったが、原因は不明。ただ画像は問題なく表示できた。
  • ThumbnailOffset: 6; 全てjpegファイル。値の変化は8や32の増加が多かったが、原因は不明。ただ画像は問題なく表示できた。
  • XMPToolkit: 74; 全て「Image::ExifTool 13.30」という値に変更されていた。おそらくXMPデータを変更したファイルに対してこの項目も変更されているものと思われる。画像は問題なく表示できた。
  • YCbCrPositioning: 39; (無し) -> Centered; 画像は問題なく表示できた。

全体を通してこれらの項目は最新のフォーマットに対して情報が不足しており、それをexiftoolが補ってくれたため差分として現れたと考えています。
ひとまずこれで当初の目的である最終更新日をオリジナルの日付に戻すことができたため、本スクラップはクローズとします。

脚注
  1. 引用元 ↩︎

このスクラップは2ヶ月前にクローズされました