Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More fixes #8

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Project moved to https://gitlab.com/bit-man/shotwell-iphoto-import

This repository contains a script to import an iPhoto library into Shotwell.

It preserves modified images using Shotwell's "open in external editor"
Expand Down
51 changes: 51 additions & 0 deletions database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# The BackingPhotoTable
# id = 1
# filepath = /home/shaun/Pictures/Photos/2008/03/24/DSCN2416 (Modified (2))_modified.JPG
# timestamp = 1348968706
# filesize = 1064375
# width = 1600
# height = 1200
# original_orientation = 1
# file_format = 0
# time_created = 1348945103

class BackingPhotoTable:
def __init__(self, db):
self.db = db
self.init()

def insert(self, photo):
cursor = self.db.execute("""
INSERT INTO BackingPhotoTable (filepath,
timestamp,
filesize,
width,
height,
original_orientation,
file_format,
time_created)
VALUES (:new_mod_path,
:mod_timestamp,
:mod_file_size,
:mod_width,
:mod_height,
:mod_original_orientation,
:file_format,
:time_created)
""", photo)
return cursor.lastrowid


def init(self):
cursor = self.db.execute("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='BackingPhotoTable'")
if (cursor.fetchone()[0] == 0):
self.db.execute("CREATE TABLE BackingPhotoTable ("
"id INTEGER PRIMARY KEY, "
"filepath TEXT UNIQUE NOT NULL, "
"timestamp INTEGER, filesize INTEGER, "
"width INTEGER, "
"height INTEGER, "
"original_orientation INTEGER, "
"file_format INTEGER, "
"time_created INTEGER "
")")
55 changes: 55 additions & 0 deletions fs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import hashlib
import os
import shutil

import logging

logger = logging.getLogger("iphotoimport")

class FileSystem:

def __init__(self, forceCopy):
self.forceCopy = forceCopy

def safe_link_file(self, src, dst):
assert os.path.exists(src), "%s didn't exist" % src

if self.forceCopy:
self.mkdir(dst)
shutil.copy(src, dst)
return

if os.path.exists(dst):
if self.is_file_same(src, dst):
# Nothing to do
return
else:
raise Exception("Destination file %s exists and not equal to %s" % (dst, src))
else:
self.mkdir(dst)
# Try to link the file
try:
os.link(src, dst)
except:
logger.debug("Hard link failed, falling back on copy")
shutil.copy(src, dst)

def is_file_same(self, f1, f2):
return os.path.samefile(f1, f2) or self.md5_for_file(f1) == self.md5_for_file(f2)


def md5_for_file(self, filename, block_size=2**20):
with open(filename, "rb") as f:
md5 = hashlib.md5()
while True:
data = f.read(block_size)
if not data:
break
md5.update(data)
return md5.hexdigest()

def mkdir(self,dir):
try:
os.makedirs(os.path.dirname(dir))
except Exception:
pass
91 changes: 21 additions & 70 deletions iphoto_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@
import datetime
from PIL import Image #@UnresolvedImport
from pyexiv2.metadata import ImageMetadata
import hashlib
import mimetypes
import re

from database import BackingPhotoTable
from fs import FileSystem

# Shotwell's orientation enum

TOP_LEFT = 1
TOP_RIGHT = 2
BOTTOM_RIGHT = 3
Expand Down Expand Up @@ -69,41 +72,13 @@ def exif_datetime_to_time(dt):

return int(time.mktime(dt.timetuple()))

def md5_for_file(filename, block_size=2**20):
with open(filename, "rb") as f:
md5 = hashlib.md5()
while True:
data = f.read(block_size)
if not data:
break
md5.update(data)
return md5.hexdigest()

def is_file_same(f1, f2):
return os.path.samefile(f1, f2) or md5_for_file(f1) == md5_for_file(f2)

def safe_link_file(src, dst):
assert os.path.exists(src), "%s didn't exist" % src
if os.path.exists(dst):
if is_file_same(src, dst):
# Nothing to do
return
else:
raise Exception("Destination file %s exists and not equal to %s" % (dst, src))
else:
# Try to link the file
try:
os.makedirs(os.path.dirname(dst))
except Exception:
pass
try:
os.link(src, dst)
except:
_log.debug("Hard link failed, falling back on copy")
shutil.copy(src, dst)


def import_photos(iphoto_dir, shotwell_db, photos_dir):
def import_photos(iphoto_dir, shotwell_db, photos_dir, force_copy):
_log.debug("Arguments")
_log.debug("\t- iPhoto dir : %s", iphoto_dir)
_log.debug("\t- Shotwell db : %s", shotwell_db)
_log.debug("\t- Shotwell dir : %s", photos_dir)
_log.debug("\t- force copy : %s", force_copy)
fs = FileSystem(force_copy)
# Sanity check the iPhoto dir and Shotwell DB.
_log.debug("Performing sanity checks on iPhoto and Shotwell DBs.")
now = int(time.time())
Expand All @@ -116,6 +91,7 @@ def import_photos(iphoto_dir, shotwell_db, photos_dir):
_log.error("Shotwell DB not found at %s", shotwell_db)
sys.exit(2)
db = sqlite3.connect(shotwell_db) #@UndefinedVariable
backingPhotoTable = BackingPhotoTable(db)
with db:
cursor = db.execute("SELECT schema_version from VersionTable;")
schema_version = cursor.fetchone()[0]
Expand Down Expand Up @@ -216,7 +192,7 @@ def fix_prefix(path, new_prefix=iphoto_dir):
img = Image.open(orig_image_path)
w, h = img.size

md5 = md5_for_file(orig_image_path)
md5 = fs.md5_for_file(orig_image_path)
orig_timestamp = int(os.path.getmtime(orig_image_path))

mod_w, mod_h, mod_md5, mod_timestamp = None, None, None, None
Expand All @@ -232,7 +208,7 @@ def fix_prefix(path, new_prefix=iphoto_dir):
mod_file_size = None
else:
mod_w, mod_h = mod_img.size
mod_md5 = md5_for_file(mod_image_path)
mod_md5 = fs.md5_for_file(mod_image_path)
mod_timestamp = int(os.path.getmtime(mod_image_path))

file_format = FILE_FORMAT.get(mime, -1)
Expand Down Expand Up @@ -327,43 +303,15 @@ def read_metadata(path, photo, prefix="orig_"):


for key, photo in photos.items():
# The BackingPhotoTable
# id = 1
# filepath = /home/shaun/Pictures/Photos/2008/03/24/DSCN2416 (Modified (2))_modified.JPG
# timestamp = 1348968706
# filesize = 1064375
# width = 1600
# height = 1200
#original_orientation = 1
# file_format = 0
# time_created = 1348945103
if "event_id" not in photo:
_log.error("Photo didn't have an event: %s", photo)
skipped.append(photo["orig_image_path"])
continue
editable_id = -1
if photo["mod_image_path"] is not None:
# This photo has a backing image
c = db.execute("""
INSERT INTO BackingPhotoTable (filepath,
timestamp,
filesize,
width,
height,
original_orientation,
file_format,
time_created)
VALUES (:new_mod_path,
:mod_timestamp,
:mod_file_size,
:mod_width,
:mod_height,
:mod_original_orientation,
:file_format,
:time_created)
""", photo)
editable_id = c.lastrowid

editable_id = backingPhotoTable.insert(photo)

photo["editable_id"] = editable_id
try:
c = db.execute("""
Expand Down Expand Up @@ -422,7 +370,7 @@ def read_metadata(path, photo, prefix="orig_"):
print >> sys.stderr, "%s file skipped (they will still be copied)" % len(skipped)

for src, dst in copy_queue:
safe_link_file(src, dst)
fs.safe_link_file(src, dst)

db.commit()
# Commit the transaction.
Expand All @@ -444,7 +392,10 @@ def parse_date(timer_interval):
parser.add_argument('photos_dir', metavar='PHOTOS_DIR', type=str,
default=None, action='store',
help='location of your photos dir')
parser.add_argument('--force-copy', dest='force_copy',
action='store_true', help='Force image copy')

args = parser.parse_args()

logging.basicConfig(level=logging.DEBUG)
import_photos(args.iphoto_dir, args.shotwell_db, args.photos_dir)
import_photos(args.iphoto_dir, args.shotwell_db, args.photos_dir, args.force_copy)