OneDriveにクソたまった写真と動画をpythonとか色々駆使して整理する物語 第一話

長年動画や写真を撮っているとどんどんたまってしまいます。子供が生まれてからさらに加速し、写真とスマホの動画だけで、ファイル数は90000を超え、300GBを超えてしまいました。動画と合わせて750GB、OneDriveの上限に近づいて来たので色々駆使して整理することにしました。

# 環境

- MacBook Air 2014 late
- Python 3.7
- ffmpeg
- ffprobe
- Microsoft OneDrive

# このシリーズでやりたいこと

1. 過去１つのフォルダに全てバックアップされてしまっているデータを年月のフォルダを作って分けたい
2. バックアップや手動アップのミスなどで重複してしまったファイルを消して容量を削減したい
3. １フォルダあたりに保存する写真の数の上限を決めてスムースに表示できるようにしたい
4. 表示がスムースになったら勝手にバックアップされてしまった意味なしやピンボケ画像も削除して容量を削減したい

# 手順

1. パソコン用の同期アプリでローカルパソコンの**外付けドライブ**に写真フォルダを同期する
2. 写真のフォルダ内に自動で年月フォルダを作って写真を振り分けるPythonプログラムを作成して実行、その際、同じファイル名がかってに上書きされないようにする
3. 重複検出のpythonプログラムを作ってファイル名は違うけど同じ中身のファイルを削除する。
4. 類似を検出して目視で確認しつつ不要ファイルを削除する

上記手順を実行すれば、そのまま同期したOneDrive内の写真も整理され、容量も削減できるというわけです。万歳。


# OneDriveのメリットとデメリット

写真のバックアップにMicrosoftのOneDriveはオススメです。何しろどうせ必要なOfficeを使うのに、昨今はOffice365を契約する人が多いと思いますが、Office365を契約すると、自動的にOneDriveの容量1TBがついてくるからです。

#### OneDriveが写真バックアップに便利な点
1. 写真が劣化しない！
2. Office365に1TBついてくる
3. iOSもAndroidもアプリで写真を自動バックアップできる
4. Windows/MacOSでも自動保存用のローカルアプリケーションがある
5. GoxxleやAxxzonのphotoと違って画質を劣化させないで大容量保存できる

#### OneDriveの写真バックアップでダメな点
1. iOSもAndroidも**全ての写真と動画が同じフォルダ**(※1)に保存される
2. 人物名のタグ付けや検索がない (Googleより不便）
3. Googleほどアカウントが普及しておらず共有しづらい

※1 この仕様は数年前に改善されており今は年月ごとのフォルダに自動で保存されます。長く使い続けているユーザーならではの悩みです。


この1が非常に問題なのです。専用スマホアプリやブラウザ上で見れば、撮影日時の時系列で閲覧できる機能を備えているものの、**特定の年月をピンポイントに見たい場合にとにかく表示が重い**し、その上、**フォルダ単位でしかローカルパソコンと同期できない**ため、写真があるフォルダをローカルで同期して色々したい時に、**写真が何万件もたまるとファインダーやlsコマンドだけでもものすごく待たされてしまう**のです。


## 1. OneDriveアプリケーションで外付けドライブと同期する

macのソフトは設定でそのまま外付けドライブを同期対象フォルダとして設定できません。なんか色々考えるのも面倒なのでシンボリックリンクで解決しました。

最初にOneDriveフォルダをすでに実行している場合、「基本設定」の「アカウント」タブから、「このMacのリンクを解除」をクリックして一旦同期を停止します。

次に、外付けドライブを接続したのち、外付けドライブ内にOneDriveフォルダを作ってからホームフォルダにシンボリックリンクを作成します。

```shell
cd
mkdir -p /Volumes/[外付けドライブデバイス名]/OneDrive
ln -s /Volumes/[外付けドライブデバイス名]/OneDrive ~/OneDrive
```
完了したらOneDriveアプリで再度ログインし、同期フォルダ選択でホームフォルダ内のOneDriveシンボリックリンクを指定すると、無事同期させることができます万歳。

## 2. Pythonでフォルダ内の全ての写真や動画を読み込んで振り分けするプログラム

ポイントだけこの記事に書きますので、全ソースが欲しい方はgithubご覧ください

https://github.com/mychaelstyle/photofles_sorter

### JPEGからEXIFを読み込む
pillowを使います

```python
from PIL import Image
from PIL.ExifTags import TAGS

img = Image.open([file path])
exif = img._getexif()
img.close()
```

これでexifの中にEXIF項目のタグIDと値のセットの配列が読み込まれます。
ここで読み取ったタグIDは数値なので、DateTimeOriginalなどのEXIFの項目名を確認したい場合はimportしたTAGSから項目名を取得する必要があります。

```python
labeled_items = []
for id, value in exif.items():
    tag_name = TAGS.get(id,id)
    labeled_item = ( tag_name, value )
    labeled_items.append(labeled_item)
```

これで項目のタプルの配列ができましたが、私の場合は撮影日だけわかればいいのでDateTimeOriginalだけ取り出します。

```python
dt_origin = None
for id, value in exif.items():
    tag_name = TAGS.get(id,id)
    if tag_name == "DateTimeOriginal":
        dt_origin = value
        break
```

これで無事撮影日を取得できました
取得した撮影日は文字列で"2019:10:08 12:00:00"という全てコロン区切りの見慣れないフォーマットになっていますが、(ymd, hms) = dt_origin.split()した後にymd.split(":")で年月日を別々にしてフォルダパス文字列を作ればOKですね。

```python
(ymd, hms) = dt_origin.split()
(year, month, day) = ymd.split(":")
```

### Canon Rawファイル(.CR2）から撮影日を読み込む

こちらの仕様を参考にします
http://lclevy.free.fr/cr2/

byte演算が必要になるのでstructを利用します。

```python
from struct import *
```

色々書くと長くなるので割愛しますが簡単にいうと**ファイルの冒頭にバイトオーダーとかCR2のバージョンとか固定の情報が入ってますよ**というところと、**16byte目の情報は基本情報の項目数が保存されていますよ**というところ。項目数保存のデータ長は固定で上記URLに記載の通り。

まずバイトオーダーがリトルエンディアンなのかビッグエンディアンなのか読み取ります
structからunpack_fromを使って1byte目を数値として読み込みます。unpack_fromはbyte数が幾つだろうが有無を言わさずbyteごとの値が入ったタプルで返すので受け取り方に注意

```python
with open(path, "rb") as f:
    buffer = f.read(1024)
    (byte_order) = unpack_from('H', buffer)
    # Set the endian flag
    endian_flag = '@'
    if byte_order == 0x4D4D:
        endian_flag = '>'
        elif byte_order == 0x4949:
        endian_flag = '<'
```

エントリーの数を取り出しループしてdateTimeの項目を表す番号0x0132を探します

```python
(num_of_entries,) = unpack_from(endian_flag+'H', buffer, 0x10)
for entry_num in range(0,num_of_entries):
    (tag_id, tag_type, num_of_value, value) = unpack_from(endian_flag+'HHLL', buffer, 0x10+2+entry_num*12)
    if tag_id == 0x0132:
        assert tag_type == 2                                                                                                         
        assert num_of_value == 20
        datetime_offset = value
        break

datetime_strings = unpack_from(20*'c', buffer, datetime_offset)
datetime_string = ""

for str in datetime_strings:
    datetime_string += str.decode()

print(datetime_string)
```

これで2019:10:07 12:00:00のような書式で日付を取得できました！

### 動画ファイル.mov/.3gp/.mp4の撮影日時を取得する

iPhoneのMOV動画とか.mp4とか、旧Androidやガラケーの3gp動画とか、デジタルビデオカメラの.mp4ファイルの撮影日を取得します。

これ、python3でいいライブラリがなかったので結局、ffmpegとffprobeをいれてsubprocessで叩くという恥ずかしいやり方をせざるをえず・・ Home brewでffmpegとffprobeをインストール

```bash
brew install ffmpeg
brew install ffprobe
```

Pythonからはsubprocessで叩く

```python
import subprocess

proc = subprocess.run(["/usr/local/bin/ffprobe","-show_chapters","-hide_banner",path], stdout = subprocess.PIPE, stderr = subprocess.PIPE)   
    response = proc.stdout.decode("utf8")+"\n"+(proc.stderr.decode("utf8"))
    for line in response.splitlines():
        if "creation_time" in line:
            str = line.replace("    creation_time   : ","").replace("T"," ").replace("Z","")
            print(str)
            break
```

雑なやり方ではありますが、割とあらゆる動画形式に対応できました。
U動くプログラムがそのまま欲しい方はgithubからどうぞ。

https://github.com/mychaelstyle/photofles_sorter

### 整理手順

2019-10段階では、上記githubのプログラムは、**隠しファイル、撮影日時が取得できないファイル**は移動せず放置します。また、**移動先に名前・ファイルサイズ・撮影日時が完全一致するファイルが既に存在していた場合**も移動せず放置します。
移動やスキップの実行結果は実行フォルダ内にログとしても吐き出します。

下記の手順でファイルを整理しました。

1. OneDrive同期フォルダ内に画像と写真を集約するための新しいフォルダを作成します。
2. 写真・動画分散しているフォルダごとに上記フォルダを出力先に指定して実行していきます。
3. 移動元ごとにログを見ながら、重複ファイルは削除、必要なファイルは退避させてから移動元フォルダを削除します。

吐き出されたログは書式が決まっているので下記のようにして確認できます。ファイル名は**info-年-月-日-時分秒.log**の形式で吐き出されます。

```bash
# 隠しファイルではなく撮影日時を取得できなかったファイルのログ一覧
cat info-YYYY-MM-DD-HHmmss.log | grep "No timestamp" | grep -v "/\."
# 隠しファイルではなく重複していたので移動しなかったファイルのログ一覧
cat info-YYYY-MM-DD-HHmmss.log | grep "Same file" | grep -v "/\."
```

## 3. ファイル名が違うけど中身が同じファイルの検出と削除

ファイル名と撮影日時とファイルサイズが完全一致する重複ファイルはここまでの手順でなくなりました。

問題なのは、OSダウンロード時やOneDriveアップロード時、バックアップ時にこれまで量産されてしまった、ファイル名は違うけど同じ写真や動画の削除です。
上書きしない選択をした時に**(1).JPGとか、番号がついて作られてしまったファイル。

### 写真の重複検出

第二話で進めます。まて！次号！