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()) album_data_filename = join_path(iphoto_dir, "AlbumData.xml") if not os.path.exists(album_data_filename): _log.error("Failed to find expected file inside iPhoto library: %s", album_data_filename) sys.exit(1) if not os.path.exists(shotwell_db): _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] if schema_version not in SUPPORTED_SHOTWELL_SCHEMAS: _log.error( "Shotwell DB uses unsupported schema version %s. " "Giving up, just to be safe.", schema_version) sys.exit(3) _log.debug("Sanity checks passed.") # Back up the Shotwell DB. fmt_now = time.strftime('%Y-%m-%d_%H%M%S') db_backup = "%s.iphotobak_%s" % (shotwell_db, fmt_now) _log.debug("Backing up shotwell DB to %s", db_backup) shutil.copy(shotwell_db, db_backup) _log.debug("Backup complete") # Load and parse the iPhoto DB. _log.debug( "Loading the iPhoto library file. Might take a while for a large DB!" ) album_data = plistlib.readPlist(album_data_filename) _log.debug("Finished loading the iPhoto library.") path_prefix = album_data["Archive Path"] def fix_prefix(path, new_prefix=iphoto_dir): if path: if path[:len(path_prefix)] != path_prefix: raise AssertionError("Path %s didn't begin with %s" % (path, path_prefix)) path = path[len(path_prefix):] path = join_path(new_prefix, path.strip(os.path.sep)) return path photos = {} # Map from photo ID to photo info. copy_queue = [] # id = 224 # filename = /home/shaun/Pictures/Photos/2008/03/24/DSCN2416 (Modified (2)).JPG # width = 1600 # height = 1200 # filesize = 480914 # timestamp = 1348718403 # exposure_time = 1206392706 # orientation = 1 #original_orientation = 1 # import_id = 1348941635 # event_id = 3 # transformations = # md5 = 3ca3cf05312d0c1a4c141bb582fc43d0 # thumbnail_md5 = # exif_md5 = cec27a47c34c89f571c0fd4e9eb4a9fe # time_created = 1348941635 # flags = 0 # rating = 0 # file_format = 0 # title = # backlinks = # time_reimported = # editable_id = 1 # metadata_dirty = 1 # developer = SHOTWELL # develop_shotwell_id = -1 # develop_camera_id = -1 # develop_embedded_id = -1 skipped = [] for key, i_photo in album_data["Master Image List"].items(): mod_image_path = fix_prefix(i_photo.get("ImagePath", None)) orig_image_path = fix_prefix(i_photo.get("OriginalPath", None)) new_mod_path = fix_prefix(i_photo.get("ImagePath"), new_prefix=photos_dir) new_orig_path = fix_prefix(i_photo.get("OriginalPath", None), new_prefix=photos_dir) if not orig_image_path or not os.path.exists(mod_image_path): orig_image_path = mod_image_path new_orig_path = new_mod_path new_mod_path = None mod_image_path = None mod_file_size = None else: mod_file_size = os.path.getsize(mod_image_path) if not os.path.exists(orig_image_path): _log.error("Original file not found %s", orig_image_path) skipped.append(orig_image_path) continue copy_queue.append((orig_image_path, new_orig_path)) if mod_image_path: copy_queue.append((mod_image_path, new_mod_path)) mime, _ = mimetypes.guess_type(orig_image_path) sys.stdout.write('.') sys.stdout.flush() if mime not in ("image/jpeg", "image/png", "image/x-ms-bmp", "image/tiff"): print _log.error("Skipping %s, it's not an image, it's a %s", orig_image_path, mime) skipped.append(orig_image_path) continue caption = i_photo.get("Caption", "") img = Image.open(orig_image_path) w, h = img.size 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 if mod_image_path: try: mod_img = Image.open(mod_image_path) except Exception: _log.error("Failed to open modified image %s, skipping", mod_image_path) orig_image_path = mod_image_path new_orig_path = new_mod_path new_mod_path = None mod_image_path = None mod_file_size = None else: mod_w, mod_h = mod_img.size 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) if file_format == -1: raise Exception("Unknown image type %s" % mime) photo = { "orig_image_path": orig_image_path, "mod_image_path": mod_image_path, "new_mod_path": new_mod_path, "new_orig_path": new_orig_path, "orig_file_size": os.path.getsize(orig_image_path), "mod_file_size": mod_file_size, "mod_timestamp": mod_timestamp, "orig_timestamp": orig_timestamp, "caption": caption, "rating": i_photo["Rating"], "event": i_photo["Roll"], "orig_exposure_time": int(parse_date(i_photo["DateAsTimerInterval"])), "width": w, "height": h, "mod_width": mod_w, "mod_height": mod_h, "orig_md5": md5, "mod_md5": md5, "file_format": file_format, "time_created": now, "import_id": now, } # May be it's available in previous versions if schema_version >= 20: photo['comment'] = i_photo["Comment"] def read_metadata(path, photo, prefix="orig_"): photo[prefix + "orientation"] = 1 photo[prefix + "original_orientation"] = 1 try: meta = ImageMetadata(path) meta.read() try: photo[prefix + "orientation"] = meta[ "Exif.Image.Orientation"].value photo[prefix + "original_orientation"] = meta[ "Exif.Image.Orientation"].value except KeyError: print _log.debug("Failed to read the orientation from %s" % path) exposure_dt = meta["Exif.Image.DateTime"].value photo[prefix + "exposure_time"] = exif_datetime_to_time(exposure_dt) except KeyError: pass except Exception: print _log.exception("Failed to read date from %s", path) raise try: read_metadata(orig_image_path, photo, "orig_") photo["orientation"] = photo["orig_orientation"] if mod_image_path: read_metadata(mod_image_path, photo, "mod_") photo["orientation"] = photo["mod_orientation"] except Exception: _log.error("**** Skipping %s" % orig_image_path) skipped.append(orig_image_path) continue photos[key] = photo events = {} for event in album_data["List of Rolls"]: key = event["RollID"] events[key] = { "date": parse_date(event["RollDateAsTimerInterval"]), "key_photo": event["KeyPhotoKey"], "photos": event["KeyList"], "name": event["RollName"] } for photo_key in event["KeyList"]: assert photo_key not in photos or photos[photo_key][ "event"] == key # Insert into the Shotwell DB. for _, event in events.items(): c = db.execute( """ INSERT INTO EventTable (time_created, name) VALUES (?, ?) """, (event["date"], event["name"])) assert c.lastrowid is not None event["row_id"] = c.lastrowid for photo_key in event["photos"]: if photo_key in photos: photos[photo_key]["event_id"] = event["row_id"] for key, photo in photos.items(): 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 editable_id = backingPhotoTable.insert(photo) photo["editable_id"] = editable_id try: c = db.execute( """ INSERT INTO PhotoTable (filename, width, height, filesize, timestamp, exposure_time, orientation, original_orientation, import_id, event_id, md5, time_created, flags, rating, file_format, title, editable_id, metadata_dirty, developer, develop_shotwell_id, develop_camera_id, develop_embedded_id, comment) VALUES (:new_orig_path, :width, :height, :orig_file_size, :orig_timestamp, :orig_exposure_time, :orientation, :orig_original_orientation, :import_id, :event_id, :orig_md5, :time_created, 0, :rating, :file_format, :caption, :editable_id, 1, 'SHOTWELL', -1, -1, -1, :comment); """, photo) except Exception: _log.exception("Failed to insert photo %s" % photo) raise print >> sys.stderr, "Skipped importing these files:\n", "\n".join( skipped) print >> sys.stderr, "%s file skipped (they will still be copied)" % len( skipped) for src, dst in copy_queue: fs.safe_link_file(src, dst) db.commit()