Google Photo가 너무 많은 용량을 차지할 때

구글 코랩에 포토에 드라이브까지 사용하다 보니 용량이 점점 커지더니 이제는 200G에 육박한다고 업그레이드를 하라고 한다. 매달 내는 돈도 신경쓰이는데 더 큰 것은 내 개인 정보가 구글 손안에 있다는 것이 영 꺼림직 해왔던차에, 이번에 해외 출장으로 생긴 돈으로 그동안 사용하던 시놀로지 NAS DS118을 DS223으로 업그레이드하려고 했다. 그런데 조사를 해보니 덜컥 8 테라 하드디스크를 사고 이전에 사용하던 DS118의 4 테라 하드디스크를 사용하려고 했는데 2 베이에서 사용하려면 2개의 하드디스크 용량이 같은 것이 좋다고 하여 두개를 독립적으로 운영하기로 결정하였다. 나중에 8 테라 하드디스크 하나 더 사서  확장할 계획을 세웠다.

주로 파일 백업 용도로 사용하였는데 구글 포토를 시놀로지 NAS가 제공하는 시놀로지 포토로 옮기려고 조사를 해 보았다. 친절하게도 누군가 나를 위하여~~~ 다음과 같은 자료를 인터넷에 올려 놓았다.

https://www.androidpolice.com/move-google-photos-synology-nas/

여러가지 방법을 쓰다가 결국 마지막에 있는 Google Takeout을 사용하여 사진을 다운로드 받은후 이를 Synology에 업로드 하기로 하였다. 그런데 어찌된 이유로 다운로드 되거나 연동하거나 간에 사진 촬영시점이 보관이 되지 않아서 다운로드한 사진들의 생성된 날자 메타데이터를 다시 집어 넣기로 하여 간단하게 ChatGPT의 도움을 받았다.

import os
import json
import zipfile
import hashlib
from shutil import copy2, rmtree
from tempfile import mkdtemp
from PIL import Image
import piexif
from datetime import datetime
import re

def file_hash(filepath):
    """Generate MD5 hash for a file."""
    hash_md5 = hashlib.md5()
    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()

def extract_zip(zip_path, extract_to):
    """Extracts zip file to the specified directory."""
    with zipfile.ZipFile(zip_path, 'r') as zip_ref:
        zip_ref.extractall(extract_to)

def handle_json_metadata(json_path):
    """Extracts metadata from JSON file."""
    if os.path.exists(json_path):
        with open(json_path, 'r') as file:
            return json.load(file)
    return {}

def update_file_timestamps(file_path, date_time_str):
    """Update file's created and modified timestamps."""
    try:
        # The format from the JSON might not include time, so we assume a default time if missing
        date_time = datetime.strptime(date_time_str, '%Y:%m:%d %H:%M:%S')
        timestamp = date_time.timestamp()
        os.utime(file_path, (timestamp, timestamp))
    except ValueError as e:
        print(f"Error updating timestamps for {file_path}: {e}")

def update_image_metadata(image_path, metadata, file_name):
    """Update the EXIF data of an image based on metadata."""
    try:
        img = Image.open(image_path)
        exif_dict = piexif.load(img.info.get('exif', b''))
        
        date_time = metadata.get('photoTakenTime', {}).get('formatted')
        if not date_time:
            match = re.search(r'\d{8}', file_name)
            if match:
                date_time = datetime.strptime(match.group(), '%Y%m%d').strftime('%Y:%m:%d %H:%M:%S')
        
        if date_time:
            exif_dict['Exif'][piexif.ExifIFD.DateTimeOriginal] = date_time.encode()
            update_file_timestamps(image_path, date_time)

        # Correct the setting of GPS information
        if 'location' in metadata:
            lat = metadata['location'].get('latitude')
            lon = metadata['location'].get('longitude')
            if lat is not None and lon is not None:
                lat_ref = 'N' if lat >= 0 else 'S'
                lon_ref = 'E' if lon >= 0 else 'W'
                lat_deg = abs(int(lat * 1000000))
                lon_deg = abs(int(lon * 1000000))
                exif_dict['GPS'] = {
                    piexif.GPSIFD.GPSLatitudeRef: lat_ref,
                    piexif.GPSIFD.GPSLatitude: [(lat_deg, 1000000), (0, 1), (0, 1)],
                    piexif.GPSIFD.GPSLongitudeRef: lon_ref,
                    piexif.GPSIFD.GPSLongitude: [(lon_deg, 1000000), (0, 1), (0, 1)]
                }

        exif_bytes = piexif.dump(exif_dict)
        img.save(image_path, exif=exif_bytes)
        img.close()
    except Exception as e:
        print(f"Error updating metadata for {image_path}: {e}")

def collect_files(directory, extensions, target_directory, seen_hashes):
    """Walk through directory, collect and copy image/video files to the target directory avoiding duplicates."""
    for root, dirs, files in os.walk(directory):
        for file in files:
            filepath = os.path.join(root, file)
            if file.endswith('.zip'):
                temp_dir = mkdtemp()
                extract_zip(filepath, temp_dir)
                collect_files(temp_dir, extensions, target_directory, seen_hashes)
                rmtree(temp_dir)
            elif any(file.endswith(ext) for ext in extensions):
                file_hash_val = file_hash(filepath)
                if file_hash_val not in seen_hashes:
                    seen_hashes.add(file_hash_val)
                    destination_path = os.path.join(target_directory, os.path.basename(filepath))
                    if not os.path.exists(destination_path):
                        copy2(filepath, destination_path)
                        if file.endswith(('.jpg', '.jpeg', '.png')):
                            json_path = os.path.splitext(filepath)[0] + '.json'
                            metadata = handle_json_metadata(json_path)
                            update_image_metadata(destination_path, metadata, file)

# Main execution
source_directories = ['Download01', 'Download02', 'Download03']  # Source directories
target_directory = 'TargetDir03'  # Destination for unique media files
file_extensions = ['.jpg', '.jpeg', '.png', '.mp4', '.mov', '.avi']  # File types
seen_hashes = set()

os.makedirs(target_directory, exist_ok=True)

for directory in source_directories:
    collect_files(directory, file_extensions, target_directory, seen_hashes)

이 코드는 그림과 같이 “Content created” 메타데이터를 생성할 수 있었다. 보다시피 “Created” 와 “Modified” 메타데이터는 다운로드 시점으로 되어있다.

Synology Photo의 사진 디렉토리 구조를 보니 사진이 연도별 디렉토리로 구성되어 있어서 연도별로 소팅하는 프로그램을 역시 친절한 chatGPT에 의해 완성!

그런데 들어가 보니 연도별 안에 달별로도 디렉토리가 구성되어 있다. ㅠㅠ 일단 여기서 앞서 구축한 VPN 서비스를 이용하여 (학교에서 작업할 때) 집의 네트워크를 파일로 연결하여 파일 복사를 수행하였다. 그런데 중복된 파일이 너무 많다. 이리 저리 연동 및 복사등 작업을 하느라고 중복된 파일들이 생긴듯하다. 중복된 파일을 지우는 방법은 메타데이터로 또는 미디어 파일의 콘텐츠 내용으로 지우는 방법이 있는 듯 한데 콘텐츠 내용이 동일한지 추출하는게 잘 안되어서 “Content created” 날자가 동일한 파일들을 지우는 방법으로 실행하였다.

import os
import subprocess
from datetime import datetime, timedelta
from pytz import timezone

def get_content_created_date_seoul(filepath):
    """Retrieve the full 'Content Created' date and time as Seoul time."""
    try:
        cmd = ['mdls', '-name', 'kMDItemContentCreationDate', '-raw', filepath]
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.stdout.strip():
            # Extract the full datetime and convert to Seoul time
            local_time = datetime.strptime(result.stdout.strip(), '%Y-%m-%d %H:%M:%S %z')
            seoul_time = local_time.astimezone(timezone('Asia/Seoul'))
            return seoul_time
    except Exception as e:
        print(f"Error retrieving metadata for {filepath}: {e}")
    return None

def find_and_remove_duplicates(directory):
    """Remove duplicates based on exact 'Content Created' timestamp and filename similarity."""
    files_by_datetime = {}
    files_to_remove = []

    # Organize files by their exact 'Content Created' datetime in Seoul time
    for filename in os.listdir(directory):
        filepath = os.path.join(directory, filename)
        content_datetime = get_content_created_date_seoul(filepath)
        if content_datetime:
            if content_datetime not in files_by_datetime:
                files_by_datetime[content_datetime] = []
            files_by_datetime[content_datetime].append(filepath)
    
    # For each datetime, remove files with longer names if they are duplicates
    for datetime_key, files in files_by_datetime.items():
        print(f"Processing files for datetime: {datetime_key}")
        if len(files) > 1:
            # Sort files by name length (shortest first)
            print(files)
            files.sort(key=lambda x: len(x))
            shortest_file = files[0]
            print(files)
            # Keep the shortest filename, mark others for deletion
            for file in files[1:2]:
                #if os.path.basename(shortest_file) in os.path.basename(file):
                    files_to_remove.append(file)
            print(files_to_remove)
    
    # Remove the marked duplicate files
    
    for file in files_to_remove:
        os.remove(file)
        print(f"Removed duplicate file: {file}")
        
    print(str(len(files_to_remove)) + " files are removed")

# Example usage
directory = '2024'
find_and_remove_duplicates(directory)

일단 Synology Photo에서 제대로 보이는 듯 하다.

Synology Phto는 저급 NAS를 설치한 관계로 인물 검색만 가능하고 객체 검색이 되지 않는다고 한다. 확실히 Google Photo가 다양한 검색기능과 자동 앨범 생성기능들을 제공하는 장점이 있다. Synology NAS의 사진 디렉토리를 접속하여 다양한 분석과 AI를 통한 사진 관리기능 구현이 가능할 것 같으니 다음에는 이러한 내용에 대해 공부하도록 해보자.

Leave a Reply

Your email address will not be published. Required fields are marked *