diff --git a/README.md b/README.md index 8fa67b9..838e422 100644 --- a/README.md +++ b/README.md @@ -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" diff --git a/database.py b/database.py new file mode 100644 index 0000000..e0c462e --- /dev/null +++ b/database.py @@ -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 " + ")") diff --git a/fs.py b/fs.py new file mode 100644 index 0000000..ded68d4 --- /dev/null +++ b/fs.py @@ -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 \ No newline at end of file diff --git a/iphoto_import.py b/iphoto_import.py index 3213b45..f27236c 100755 --- a/iphoto_import.py +++ b/iphoto_import.py @@ -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 @@ -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()) @@ -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] @@ -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 @@ -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) @@ -327,16 +303,6 @@ 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"]) @@ -344,26 +310,8 @@ def read_metadata(path, photo, prefix="orig_"): 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(""" @@ -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. @@ -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) \ No newline at end of file + import_photos(args.iphoto_dir, args.shotwell_db, args.photos_dir, args.force_copy) \ No newline at end of file