diff options
Diffstat (limited to 'plugins/shotwell-data-imports')
-rw-r--r-- | plugins/shotwell-data-imports/FSpotDatabase.vala | 58 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/FSpotDatabaseBehavior.vala | 208 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/FSpotDatabaseTable.vala | 54 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/FSpotImporter.vala | 567 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/FSpotMetaTable.vala | 113 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/FSpotPhotoTagsTable.vala | 57 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/FSpotPhotoVersionsTable.vala | 271 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/FSpotPhotosTable.vala | 356 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/FSpotRollsTable.vala | 111 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/FSpotTableBehavior.vala | 28 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/FSpotTagsTable.vala | 129 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/Makefile | 30 | ||||
-rw-r--r-- | plugins/shotwell-data-imports/f-spot-24.png | bin | 0 -> 1741 bytes | |||
-rw-r--r-- | plugins/shotwell-data-imports/shotwell-data-imports.vala | 46 |
14 files changed, 2028 insertions, 0 deletions
diff --git a/plugins/shotwell-data-imports/FSpotDatabase.vala b/plugins/shotwell-data-imports/FSpotDatabase.vala new file mode 100644 index 0000000..634c5c9 --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotDatabase.vala @@ -0,0 +1,58 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace DataImports.FSpot.Db { + +public const int64 NULL_ID = 0; +public const int64 INVALID_ID = -1; + +/** + * Initialization method for the whole module. + */ +public void init() { + FSpotDatabaseBehavior.create_behavior_map(); +} + +/** + * An object that is able to read from the F-Spot + * database and extract the relevant objects. + */ +public class FSpotDatabase : Object { + private Sqlite.Database fspot_db; + private FSpotMetaTable meta_table; + public FSpotPhotosTable photos_table; + public FSpotPhotoVersionsTable photo_versions_table; + public FSpotTagsTable tags_table; + public FSpotRollsTable rolls_table; + public int64 hidden_tag_id; + + public FSpotDatabase(File db_file) throws DatabaseError, Spit.DataImports.DataImportError { + string filename = db_file.get_path(); + int res = Sqlite.Database.open_v2(filename, out fspot_db, + Sqlite.OPEN_READONLY, null); + if (res != Sqlite.OK) + throw new DatabaseError.ERROR("Unable to open F-Spot database %s: %d", filename, res); + meta_table = new FSpotMetaTable(fspot_db); + hidden_tag_id = meta_table.get_hidden_tag_id(); + + FSpotDatabaseBehavior db_behavior = new FSpotDatabaseBehavior(get_version()); + + photos_table = new FSpotPhotosTable(fspot_db, db_behavior); + photo_versions_table = new FSpotPhotoVersionsTable(fspot_db, db_behavior); + tags_table = new FSpotTagsTable(fspot_db, db_behavior); + rolls_table = new FSpotRollsTable(fspot_db, db_behavior); + } + + ~FSpotDatabase() { + } + + private Utils.VersionNumber get_version() throws DatabaseError { + return new Utils.VersionNumber.from_string(meta_table.get_db_version()); + } +} + +} + diff --git a/plugins/shotwell-data-imports/FSpotDatabaseBehavior.vala b/plugins/shotwell-data-imports/FSpotDatabaseBehavior.vala new file mode 100644 index 0000000..973eb38 --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotDatabaseBehavior.vala @@ -0,0 +1,208 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace DataImports.FSpot.Db { + +private class FSpotBehaviorEntry { + private Utils.VersionNumber version; + private FSpotTableBehavior behavior; + + public FSpotBehaviorEntry(Utils.VersionNumber version, FSpotTableBehavior behavior) { + this.version = version; + this.behavior = behavior; + } + + public Utils.VersionNumber get_version() { + return version; + } + + public FSpotTableBehavior get_behavior() { + return behavior; + } +} + +/** + * A class that consolidates the behavior of all F-Spot tables (apart from meta) + * and is the one place to check whether the database version is supported. + */ +public class FSpotDatabaseBehavior : Object { + // Minimum unsupported version: any database from that version and above + // is not supported as it's too new and support has not been provided + // In practice, the code may work with future versions but this cannot be + // guaranteed as it hasn't been tested so it's probably better to just + // bomb out at that point rather than risk importing incorrect data + public static Utils.VersionNumber MIN_UNSUPPORTED_VERSION = + new Utils.VersionNumber({ 19 }); + private static Gee.Map<string, Gee.List<FSpotBehaviorEntry>> behavior_map; + + private FSpotTableBehavior<FSpotPhotoRow> photos_behavior; + private FSpotTableBehavior<FSpotTagRow> tags_behavior; + private FSpotTableBehavior<FSpotPhotoTagRow> photo_tags_behavior; + private FSpotTableBehavior<FSpotPhotoVersionRow> photo_versions_behavior; + private FSpotTableBehavior<FSpotRollRow> rolls_behavior; + + public static void create_behavior_map() { + behavior_map = new Gee.HashMap<string, Gee.List<FSpotBehaviorEntry>>(); + // photos table + Gee.List<FSpotBehaviorEntry> photos_list = new Gee.ArrayList<FSpotBehaviorEntry>(); + // v0-4 + photos_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 0 }), + FSpotPhotosV0Behavior.get_instance() + )); + // v5-6 + photos_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 5 }), + FSpotPhotosV5Behavior.get_instance() + )); + // v7-10 + photos_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 7 }), + FSpotPhotosV7Behavior.get_instance() + )); + // v11-15 + photos_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 11 }), + FSpotPhotosV11Behavior.get_instance() + )); + // v16 + photos_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 16 }), + FSpotPhotosV16Behavior.get_instance() + )); + // v17 + photos_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 17 }), + FSpotPhotosV17Behavior.get_instance() + )); + // v18+ + photos_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 18 }), + FSpotPhotosV18Behavior.get_instance() + )); + behavior_map.set(FSpotPhotosTable.TABLE_NAME, photos_list); + // tags table + Gee.List<FSpotBehaviorEntry> tags_list = new Gee.ArrayList<FSpotBehaviorEntry>(); + // v0+ + tags_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 0 }), + FSpotTagsV0Behavior.get_instance() + )); + behavior_map.set(FSpotTagsTable.TABLE_NAME, tags_list); + // photo_tags table + Gee.List<FSpotBehaviorEntry> photo_tags_list = new Gee.ArrayList<FSpotBehaviorEntry>(); + // v0+ + photo_tags_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 0 }), + FSpotPhotoTagsV0Behavior.get_instance() + )); + behavior_map.set(FSpotPhotoTagsTable.TABLE_NAME, photo_tags_list); + // photo_versions table + Gee.List<FSpotBehaviorEntry> photo_versions_list = new Gee.ArrayList<FSpotBehaviorEntry>(); + // v0-8 + photo_versions_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 0 }), + FSpotPhotoVersionsV0Behavior.get_instance() + )); + // v9-15 + photo_versions_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 9 }), + FSpotPhotoVersionsV9Behavior.get_instance() + )); + // v16 + photo_versions_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 16 }), + FSpotPhotoVersionsV16Behavior.get_instance() + )); + // v17 + photo_versions_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 17 }), + FSpotPhotoVersionsV17Behavior.get_instance() + )); + // v18+ + photo_versions_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 18 }), + FSpotPhotoVersionsV18Behavior.get_instance() + )); + behavior_map.set(FSpotPhotoVersionsTable.TABLE_NAME, photo_versions_list); + // rolls table + Gee.List<FSpotBehaviorEntry> rolls_list = new Gee.ArrayList<FSpotBehaviorEntry>(); + // v0-4 + rolls_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 0 }), + FSpotRollsV0Behavior.get_instance() + )); + // v5+ + rolls_list.add(new FSpotBehaviorEntry( + new Utils.VersionNumber({ 5 }), + FSpotRollsV5Behavior.get_instance() + )); + behavior_map.set(FSpotRollsTable.TABLE_NAME, rolls_list); + } + + public static FSpotTableBehavior? find_behavior(string table_name, Utils.VersionNumber version) { + FSpotTableBehavior behavior = null; + Gee.List<FSpotBehaviorEntry> behavior_list = behavior_map.get(table_name); + if (behavior_list != null) + foreach (FSpotBehaviorEntry entry in behavior_list) { + if (version.compare_to(entry.get_version()) >= 0) + behavior = entry.get_behavior(); + } + else + warning("Could not find behavior list for table %s", table_name); + return behavior; + + } + public FSpotDatabaseBehavior(Utils.VersionNumber version) throws Spit.DataImports.DataImportError { + if (version.compare_to(MIN_UNSUPPORTED_VERSION) >= 0) + throw new Spit.DataImports.DataImportError.UNSUPPORTED_VERSION("Version %s is not yet supported", version.to_string()); + + FSpotTableBehavior? photos_generic_behavior = find_behavior(FSpotPhotosTable.TABLE_NAME, version); + if (photos_generic_behavior != null) + photos_behavior = photos_generic_behavior as FSpotTableBehavior<FSpotPhotoRow>; + FSpotTableBehavior? tags_generic_behavior = find_behavior(FSpotTagsTable.TABLE_NAME, version); + if (tags_generic_behavior != null) + tags_behavior = tags_generic_behavior as FSpotTableBehavior<FSpotTagRow>; + FSpotTableBehavior? photo_tags_generic_behavior = find_behavior(FSpotPhotoTagsTable.TABLE_NAME, version); + if (photo_tags_generic_behavior != null) + photo_tags_behavior = photo_tags_generic_behavior as FSpotTableBehavior<FSpotPhotoTagRow>; + FSpotTableBehavior? photo_versions_generic_behavior = find_behavior(FSpotPhotoVersionsTable.TABLE_NAME, version); + if (photo_versions_generic_behavior != null) + photo_versions_behavior = photo_versions_generic_behavior as FSpotTableBehavior<FSpotPhotoVersionRow>; + FSpotTableBehavior? rolls_generic_behavior = find_behavior(FSpotRollsTable.TABLE_NAME, version); + if (rolls_generic_behavior != null) + rolls_behavior = rolls_generic_behavior as FSpotTableBehavior<FSpotRollRow>; + + if (photos_behavior == null || tags_behavior == null || + photo_tags_behavior == null || photo_versions_behavior == null || + rolls_behavior == null + ) + throw new Spit.DataImports.DataImportError.UNSUPPORTED_VERSION("Version %s is not supported", version.to_string()); + } + + public FSpotTableBehavior<FSpotPhotoRow> get_photos_behavior() { + return photos_behavior; + } + + public FSpotTableBehavior<FSpotTagRow> get_tags_behavior() { + return tags_behavior; + } + + public FSpotTableBehavior<FSpotPhotoTagRow> get_photo_tags_behavior() { + return photo_tags_behavior; + } + + public FSpotTableBehavior<FSpotPhotoVersionRow> get_photo_versions_behavior() { + return photo_versions_behavior; + } + + public FSpotTableBehavior<FSpotRollRow> get_rolls_behavior() { + return rolls_behavior; + } +} + +} + diff --git a/plugins/shotwell-data-imports/FSpotDatabaseTable.vala b/plugins/shotwell-data-imports/FSpotDatabaseTable.vala new file mode 100644 index 0000000..eba64be --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotDatabaseTable.vala @@ -0,0 +1,54 @@ +/* Copyright 2009-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +namespace DataImports.FSpot.Db { + +/** + * This class represents a generic F-Spot table. + */ +public abstract class FSpotDatabaseTable<T> : ImportableDatabaseTable { + protected unowned Sqlite.Database fspot_db; + protected FSpotTableBehavior<T> behavior; + + public FSpotDatabaseTable(Sqlite.Database db) { + this.fspot_db = db; + } + + public void set_behavior(FSpotTableBehavior<T> behavior) { + this.behavior = behavior; + set_table_name(behavior.get_table_name()); + } + + public FSpotTableBehavior<T> get_behavior() { + return behavior; + } + + protected string get_joined_column_list(bool with_table = false) { + string[] columns = behavior.list_columns(); + if (with_table) + for (int i = 0; i < columns.length; i++) + columns[i] = "%s.%s".printf(table_name, columns[i]); + return string.joinv(", ", columns); + } + + protected int select_all(out Sqlite.Statement stmt) throws DatabaseError { + string column_list = get_joined_column_list(); + string sql = "SELECT %s FROM %s".printf(column_list, table_name); + + int res = fspot_db.prepare_v2(sql, -1, out stmt); + if (res != Sqlite.OK) + throw_error("Statement failed: %s".printf(sql), res); + + res = stmt.step(); + if (res != Sqlite.ROW && res != Sqlite.DONE) + throw_error("select_all %s %s".printf(table_name, column_list), res); + + return res; + } +} + +} + diff --git a/plugins/shotwell-data-imports/FSpotImporter.vala b/plugins/shotwell-data-imports/FSpotImporter.vala new file mode 100644 index 0000000..03abe01 --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotImporter.vala @@ -0,0 +1,567 @@ +/* Copyright 2009-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class FSpotService : Object, Spit.Pluggable, Spit.DataImports.Service { + private const string ICON_FILENAME = "f-spot-24.png"; + + private static Gdk.Pixbuf[] icon_pixbuf_set = null; + + public FSpotService(GLib.File resource_directory) { + // initialize the database layer + DataImports.FSpot.Db.init(); + if (icon_pixbuf_set == null) + icon_pixbuf_set = Resources.load_icon_set(resource_directory.get_child(ICON_FILENAME)); + } + + public int get_pluggable_interface(int min_host_interface, int max_host_interface) { + return Spit.negotiate_interfaces(min_host_interface, max_host_interface, + Spit.DataImports.CURRENT_INTERFACE); + } + + public unowned string get_id() { + return "org.yorba.shotwell.dataimports.fspot"; + } + + public unowned string get_pluggable_name() { + return "F-Spot"; + } + + public void get_info(ref Spit.PluggableInfo info) { + info.authors = "Bruno Girin"; + info.copyright = _("Copyright 2009-2014 Yorba Foundation"); + info.translators = Resources.TRANSLATORS; + info.version = _VERSION; + info.website_name = Resources.WEBSITE_NAME; + info.website_url = Resources.WEBSITE_URL; + info.is_license_wordwrapped = false; + info.license = Resources.LICENSE; + info.icons = icon_pixbuf_set; + } + + public void activation(bool enabled) { + } + + public Spit.DataImports.DataImporter create_data_importer(Spit.DataImports.PluginHost host) { + return new DataImports.FSpot.FSpotDataImporter(this, host); + } +} + +namespace DataImports.FSpot { + +internal const string SERVICE_NAME = "F-Spot"; +internal const string SERVICE_WELCOME_MESSAGE = + _("Welcome to the F-Spot library import service.\n\nPlease select a library to import, either by selecting one of the existing libraries found by Shotwell or by selecting an alternative F-Spot database file."); +internal const string SERVICE_WELCOME_MESSAGE_FILE_ONLY = + _("Welcome to the F-Spot library import service.\n\nPlease select an F-Spot database file."); +internal const string FILE_IMPORT_LABEL = + _("Manually select an F-Spot database file to import:"); +internal const string ERROR_CANT_OPEN_DB_FILE = + _("Cannot open the selected F-Spot database file: the file does not exist or is not an F-Spot database"); +internal const string ERROR_UNSUPPORTED_DB_VERSION = + _("Cannot open the selected F-Spot database file: this version of the F-Spot database is not supported by Shotwell"); +internal const string ERROR_CANT_READ_TAGS_TABLE = + _("Cannot read the selected F-Spot database file: error while reading tags table"); +internal const string ERROR_CANT_READ_PHOTOS_TABLE = + _("Cannot read the selected F-Spot database file: error while reading photos table"); +internal const string MESSAGE_FINAL_SCREEN = + _("Shotwell has found %d photos in the F-Spot library and is currently importing them. Duplicates will be automatically detected and removed.\n\nYou can close this dialog and start using Shotwell while the import is taking place in the background."); + +public class FSpotImportableLibrary : Spit.DataImports.ImportableLibrary, GLib.Object { + private File db_file; + + public FSpotImportableLibrary(File db_file) { + this.db_file = db_file; + } + + public File get_db_file() { + return db_file; + } + + public string get_display_name() { + return _("F-Spot library: %s").printf(db_file.get_path()); + } +} + +public class FSpotImportableItem : Spit.DataImports.ImportableMediaItem, GLib.Object { + private DataImports.FSpot.Db.FSpotPhotoRow photo_row; + private DataImports.FSpot.Db.FSpotPhotoVersionRow? photo_version_row; + private DataImports.FSpot.Db.FSpotRollRow? roll_row; + private FSpotImportableTag[] tags; + private FSpotImportableEvent? event; + private FSpotImportableRating rating; + private string folder_path; + private string filename; + + public FSpotImportableItem( + DataImports.FSpot.Db.FSpotPhotoRow photo_row, + DataImports.FSpot.Db.FSpotPhotoVersionRow? photo_version_row, + DataImports.FSpot.Db.FSpotRollRow? roll_row, + FSpotImportableTag[] tags, + FSpotImportableEvent? event, + bool is_hidden, + bool is_favorite + ) { + this.photo_row = photo_row; + this.photo_version_row = photo_version_row; + this.roll_row = roll_row; + this.tags = tags; + this.event = event; + if (photo_row.rating > 0) + this.rating = new FSpotImportableRating(photo_row.rating); + else if (is_hidden) + this.rating = new FSpotImportableRating(FSpotImportableRating.REJECTED); + else if (is_favorite) + this.rating = new FSpotImportableRating(5); + else + this.rating = new FSpotImportableRating(FSpotImportableRating.UNRATED); + + // store path and filename + folder_path = (photo_version_row != null) ? + photo_version_row.base_path.get_path() : + photo_row.base_path.get_path(); + filename = (photo_version_row != null) ? + photo_version_row.filename : + photo_row.filename; + + // In theory, neither field should be null at that point but belts + // and braces don't hurt + if (folder_path != null && filename != null) { + // check if file exist and if not decode as URL + File photo = File.new_for_path(folder_path).get_child(filename); + + // If file not found, parse as URI and store back + if (!photo.query_exists()) { + folder_path = decode_url(folder_path); + filename = decode_url(filename); + } + } + } + + public Spit.DataImports.ImportableTag[] get_tags() { + Spit.DataImports.ImportableTag[] importable_tags = new Spit.DataImports.ImportableTag[0]; + foreach (FSpotImportableTag tag in tags) + importable_tags += tag; + return importable_tags; + } + + public Spit.DataImports.ImportableEvent? get_event() { + return event; + } + + public string get_folder_path() { + return folder_path; + } + + public string get_filename() { + return filename; + } + + public string? get_title() { + return (photo_row.description == null || photo_row.description == "") ? null : photo_row.description; + } + + public Spit.DataImports.ImportableRating get_rating() { + return rating; + } + + private string decode_url(string url) { + StringBuilder builder = new StringBuilder(); + for (int idx = 0; idx < url.length; ) { + int cidx = url.index_of_char('%', idx); + if (cidx > idx) { + builder.append(url.slice(idx, cidx)); + } + if (cidx >= 0) { + if (cidx < url.length - 2) { + char c1 = url.get(cidx + 1); + char c2 = url.get(cidx + 2); + if (c1.isxdigit() && c1.isxdigit()) { + int ccode = 0x10 * c1.xdigit_value() + c2.xdigit_value(); + builder.append_c((char)ccode); + } + idx = cidx + 3; + } else { + idx = cidx + 1; + } + } else { + builder.append(url.substring(idx)); + idx = url.length; + } + } + return builder.str; + } +} + +public class FSpotImportableTag : Spit.DataImports.ImportableTag, GLib.Object { + private DataImports.FSpot.Db.FSpotTagRow row; + private FSpotImportableTag? parent; + + public FSpotImportableTag(DataImports.FSpot.Db.FSpotTagRow row, FSpotImportableTag? parent) { + this.row = row; + this.parent = parent; + } + + public int64 get_id() { + return row.tag_id; + } + + public string get_name() { + return row.name; + } + + public Spit.DataImports.ImportableTag? get_parent() { + return parent; + } + + public FSpotImportableTag? get_fspot_parent() { + return parent; + } + + public string get_stock_icon() { + return row.stock_icon; + } + + public bool is_stock() { + return (row.stock_icon.has_prefix(DataImports.FSpot.Db.FSpotTagsTable.PREFIX_STOCK_ICON)); + } + + public FSpotImportableEvent to_event() { + return new FSpotImportableEvent(this.row); + } +} + +public class FSpotImportableEvent : Spit.DataImports.ImportableEvent, GLib.Object { + private DataImports.FSpot.Db.FSpotTagRow row; + + public FSpotImportableEvent(DataImports.FSpot.Db.FSpotTagRow row) { + this.row = row; + } + + public string get_name() { + return row.name; + } +} + +public class FSpotImportableRating : Spit.DataImports.ImportableRating, GLib.Object { + public static const int REJECTED = -1; + public static const int UNRATED = 0; + + private int rating_value; + + public FSpotImportableRating(int rating_value) { + if (rating_value < -1) + rating_value = -1; + else if (rating_value > 5) + rating_value = 5; + this.rating_value = rating_value; + } + + public bool is_rejected() { + return (rating_value == REJECTED); + } + + public bool is_unrated() { + return (rating_value == UNRATED); + } + + public int get_value() { + return rating_value; + } +} + +internal class FSpotTagsCache : Object { + private DataImports.FSpot.Db.FSpotTagsTable tags_table; + private Gee.HashMap<int64?, FSpotImportableTag> tags_map; + + public FSpotTagsCache(DataImports.FSpot.Db.FSpotTagsTable tags_table) throws DatabaseError { + this.tags_table = tags_table; + tags_map = new Gee.HashMap<int64?, FSpotImportableTag> (); + } + + public FSpotImportableTag get_tag(DataImports.FSpot.Db.FSpotTagRow tag_row) throws DatabaseError { + FSpotImportableTag? tag = tags_map.get(tag_row.tag_id); + if (tag != null) { + return tag; + } else { + FSpotImportableTag? parent_tag = get_tag_from_id(tag_row.category_id); + FSpotImportableTag new_tag = new FSpotImportableTag(tag_row, parent_tag); + tags_map[tag_row.tag_id] = new_tag; + return new_tag; + } + } + + private FSpotImportableTag? get_tag_from_id(int64 tag_id) throws DatabaseError { + // check whether the tag ID is valid first, otherwise return null + if (tag_id < 1) + return null; + FSpotImportableTag? tag = tags_map.get(tag_id); + if (tag != null) + return tag; + DataImports.FSpot.Db.FSpotTagRow? tag_row = tags_table.get_by_id(tag_id); + if (tag_row != null) { + FSpotImportableTag? parent_tag = get_tag_from_id(tag_row.category_id); + FSpotImportableTag new_tag = new FSpotImportableTag(tag_row, parent_tag); + tags_map[tag_id] = new_tag; + return new_tag; + } + return null; + } +} + +public class FSpotDataImporter : Spit.DataImports.DataImporter, GLib.Object { + + private weak Spit.DataImports.PluginHost host = null; + private weak Spit.DataImports.Service service = null; + private bool running = false; + + public FSpotDataImporter(Spit.DataImports.Service service, + Spit.DataImports.PluginHost host) { + debug("FSpotDataImporter instantiated."); + this.service = service; + this.host = host; + } + + private bool is_running() { + return running; + } + + public Spit.DataImports.Service get_service() { + return service; + } + + public void start() { + if (is_running()) + return; + + debug("FSpotDataImporter: starting interaction."); + + running = true; + + do_discover_importable_libraries(); + } + + public void stop() { + debug("FSpotDataImporter: stopping interaction."); + + running = false; + } + + // Actions and event implementation + + /** + * Action that discovers importable libraries based on standard locations. + */ + private void do_discover_importable_libraries() { + Spit.DataImports.ImportableLibrary[] discovered_libraries = + new Spit.DataImports.ImportableLibrary[0]; + + File[] db_files = { + // where the DB is in Ubuntu Lucid + File.new_for_path(Environment.get_user_config_dir()). + get_child("f-spot").get_child("photos.db"), + // where it seems to be in Ubuntu Jaunty + File.new_for_path(Environment.get_home_dir()).get_child(".gnome2"). + get_child("f-spot").get_child("photos.db"), + // where it should really be if it followed the XDG spec + File.new_for_path(Environment.get_user_data_dir()). + get_child("f-spot").get_child("photos.db") + }; + + foreach (File db_file in db_files) { + if (db_file.query_exists(null)) { + discovered_libraries += new FSpotImportableLibrary(db_file); + message("Discovered importable library: %s", db_file.get_path()); + } + } + + host.install_library_selection_pane( + (discovered_libraries.length > 0 ? SERVICE_WELCOME_MESSAGE : SERVICE_WELCOME_MESSAGE_FILE_ONLY), + discovered_libraries, + FILE_IMPORT_LABEL + ); + } + + public void on_library_selected(Spit.DataImports.ImportableLibrary library) { + on_file_selected(((FSpotImportableLibrary)library).get_db_file()); + } + + public void on_file_selected(File file) { + DataImports.FSpot.Db.FSpotDatabase database; + FSpotTagsCache tags_cache; + Gee.ArrayList<DataImports.FSpot.Db.FSpotPhotoRow> all_photos; + double progress_delta_per_photo = 1.0; + double progress_plugin_to_host_ratio = 0.5; + double current_progress = 0.0; + try { + database = new DataImports.FSpot.Db.FSpotDatabase(file); + } catch(DatabaseError e) { + debug("FSpotDataImporter: Can't open database file: %s".printf(e.message)); + host.post_error_message(ERROR_CANT_OPEN_DB_FILE); + return; + } catch(Spit.DataImports.DataImportError e) { + debug("FSpotDataImporter: Unsupported F-Spot database version: %s".printf(e.message)); + host.post_error_message(ERROR_UNSUPPORTED_DB_VERSION); + return; + } + try { + tags_cache = new FSpotTagsCache(database.tags_table); + } catch(DatabaseError e) { + debug("FSpotDataImporter: Can't read tags table: %s".printf(e.message)); + host.post_error_message(ERROR_CANT_READ_TAGS_TABLE); + return; + } + host.install_import_progress_pane(_("Preparing to import")); + try { + all_photos = database.photos_table.get_all(); + } catch(DatabaseError e) { + debug("FSpotDataImporter: Can't read photos table: %s".printf(e.message)); + host.post_error_message(ERROR_CANT_READ_PHOTOS_TABLE); + return; + } + if (all_photos.size > 0) + progress_delta_per_photo = 1.0 / all_photos.size; + foreach (DataImports.FSpot.Db.FSpotPhotoRow photo_row in all_photos) { + bool hidden = false; + bool favorite = false; + FSpotImportableTag[] tags = new FSpotImportableTag[0]; + FSpotImportableEvent? event = null; + DataImports.FSpot.Db.FSpotRollRow? roll_row = null; + + // TODO: We do not convert F-Spot events to Shotwell events because F-Spot's events + // are essentially tags. We would need to detect if the tag is an event (use + // is_tag_event) and then assign the event to the photo ... since a photo can be + // in multiple F-Spot events, we would need to pick one, and since their tags + // are hierarchical, we would need to pick a name (probably the leaf) + try { + foreach ( + DataImports.FSpot.Db.FSpotTagRow tag_row in + database.tags_table.get_by_photo_id(photo_row.photo_id) + ) { + FSpotImportableTag tag = tags_cache.get_tag(tag_row); + if (is_tag_hidden(tag, database.hidden_tag_id)) + hidden = true; + else if (is_tag_favorite(tag)) + favorite = true; + else + tags += tag; + } + } catch(DatabaseError e) { + // log the error and leave the tag list empty + message("Failed to retrieve tags for photo ID %ld: %s", (long) photo_row.photo_id, + e.message); + } + + try { + roll_row = database.rolls_table.get_by_id(photo_row.roll_id); + } catch (DatabaseError e) { + // log the error and leave the roll row null + message("Failed to retrieve roll for photo ID %ld: %s", (long) photo_row.photo_id, + e.message); + } + + Spit.DataImports.ImportableMediaItem[] importable_items = new Spit.DataImports.ImportableMediaItem[0]; + try { + Gee.ArrayList<DataImports.FSpot.Db.FSpotPhotoVersionRow> photo_versions = + database.photo_versions_table.get_by_photo_id(photo_row.photo_id); + bool photo_versions_added = false; // set to true if at least one version was added + bool photo_versions_skipped = false; // set to true if at least one version was skipped due to missing file details + foreach (DataImports.FSpot.Db.FSpotPhotoVersionRow photo_version_row in photo_versions) { + if (photo_version_row.base_path != null && photo_version_row.filename != null) { + importable_items += new FSpotImportableItem( + photo_row, photo_version_row, roll_row, tags, event, hidden, favorite + ); + photo_versions_added = true; + } else { + photo_versions_skipped = true; + } + } + + // Older versions of F-Spot (0.4.3.1 at least, perhaps later) did not maintain photo_versions, + // this handles that case + // It also handles the case when we had to skip any photo version due to missing + // file details + if (photo_versions_skipped || !photo_versions_added) { + if (photo_row.base_path != null && photo_row.filename != null) { + importable_items += new FSpotImportableItem( + photo_row, null, roll_row, tags, event, hidden, favorite + ); + } + } + } catch (DatabaseError e) { + // if we can't load the different versions, do the best we can + // and create one photo from the photo row that was found earlier + message("Failed to retrieve versions for photo ID %ld: %s", (long) photo_row.photo_id, + e.message); + if (photo_row.base_path != null && photo_row.filename != null) { + importable_items += new FSpotImportableItem( + photo_row, null, roll_row, tags, event, hidden, favorite + ); + } + } + // If the importer is still running, import the items and loop, + // otherwise break the loop + if (running) { + host.prepare_media_items_for_import( + importable_items, + current_progress + (progress_delta_per_photo * progress_plugin_to_host_ratio), + progress_delta_per_photo * (1 - progress_plugin_to_host_ratio), + null + ); + current_progress += progress_delta_per_photo; + host.update_import_progress_pane(current_progress); + } else { + break; + } + } + host.finalize_import(on_imported_items_count); + } + + public void on_imported_items_count(int imported_items_count) { + host.install_static_message_pane( + MESSAGE_FINAL_SCREEN.printf(imported_items_count), + Spit.DataImports.PluginHost.ButtonMode.CLOSE + ); + } + + private bool is_tag_event(FSpotImportableTag tag) { + bool result = (DataImports.FSpot.Db.FSpotTagsTable.STOCK_ICON_EVENTS == tag.get_stock_icon()); + if (!result) { + FSpotImportableTag? parent = tag.get_fspot_parent(); + if (parent == null) + result = false; + else + result = is_tag_event(parent); + } + return result; + } + + private bool is_tag_hidden(FSpotImportableTag tag, int64 hidden_tag_id) { + bool result = (hidden_tag_id == tag.get_id()); + if (!result) { + FSpotImportableTag? parent = tag.get_fspot_parent(); + if (parent == null) + result = false; + else + result = is_tag_hidden(parent, hidden_tag_id); + } + return result; + } + + private bool is_tag_favorite(FSpotImportableTag tag) { + bool result = (DataImports.FSpot.Db.FSpotTagsTable.STOCK_ICON_FAV == tag.get_stock_icon()); + if (!result) { + FSpotImportableTag? parent = tag.get_fspot_parent(); + if (parent == null) + result = false; + else + result = is_tag_favorite(parent); + } + return result; + } +} + +} // namespace + diff --git a/plugins/shotwell-data-imports/FSpotMetaTable.vala b/plugins/shotwell-data-imports/FSpotMetaTable.vala new file mode 100644 index 0000000..b2d16eb --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotMetaTable.vala @@ -0,0 +1,113 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace DataImports.FSpot.Db { + +/** + * The value object for the "meta" table, representing a single database row. + */ +public class FSpotMetaRow : Object { + // ignore the ID + public string name; + public string data; +} + +/** + * This class represents the F-Spot meta table, which stores some essential + * meta-data for the whole database. It is implemented as a simple dictionary + * where each row in the table is a key/value pair. + * + * The meta table implementation is the only one that throws a database error + * if something goes wrong because: + * * it is essential to read the content of that table in order to identify + * the version of the database and select the correct behavior, + * * this table is read at the very beginning of the process so any failure + * will occur immediately, + * * failing to read this table means that there is no point in reading the + * attempting to read the rest of the database so we might as well abort. + */ +public class FSpotMetaTable : FSpotDatabaseTable<FSpotMetaRow> { + + public FSpotMetaTable(Sqlite.Database db) { + base(db); + set_behavior(FSpotMetaBehavior.get_instance()); + } + + public string? get_data(string name) throws DatabaseError { + string[] columns = behavior.list_columns(); + string column_list = string.joinv(", ", columns); + string sql = "SELECT %s FROM %s WHERE name=?".printf(column_list, table_name); + Sqlite.Statement stmt; + int res = fspot_db.prepare_v2(sql, -1, out stmt); + if (res != Sqlite.OK) + throw_error("Statement failed: %s".printf(sql), res); + + res = stmt.bind_text(1, name); + if (res != Sqlite.OK) + throw_error("Bind failed for name %s".printf(name), res); + + res = stmt.step(); + if (res != Sqlite.ROW) { + if (res != Sqlite.DONE) + throw_error("FSpotMetaTable.get_data", res); + + return null; + } + + FSpotMetaRow row; + behavior.build_row(stmt, out row); + return row.data; + } + + public string? get_app_version() throws DatabaseError { + return get_data("F-Spot Version"); + } + + public string? get_db_version() throws DatabaseError { + return get_data("F-Spot Database Version"); + } + + public int64 get_hidden_tag_id() throws DatabaseError { + string id_str = get_data("Hidden Tag Id"); + if(id_str != null) { + return int64.parse(id_str); + } else { + return -1; + } + } +} + +public class FSpotMetaBehavior : FSpotTableBehavior<FSpotMetaRow>, Object { + public static const string TABLE_NAME = "Meta"; + + private static FSpotMetaBehavior instance; + + private FSpotMetaBehavior() { + } + + public static FSpotMetaBehavior get_instance() { + if (instance == null) + instance = new FSpotMetaBehavior(); + return instance; + } + + public string get_table_name() { + return TABLE_NAME; + } + + public string[] list_columns() { + return { "name", "data" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotMetaRow row, int offset = 0) { + row = new FSpotMetaRow(); + row.name = stmt.column_text(offset + 0); + row.data = stmt.column_text(offset + 1); + } +} + +} + diff --git a/plugins/shotwell-data-imports/FSpotPhotoTagsTable.vala b/plugins/shotwell-data-imports/FSpotPhotoTagsTable.vala new file mode 100644 index 0000000..7d65594 --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotPhotoTagsTable.vala @@ -0,0 +1,57 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace DataImports.FSpot.Db { + +/** + * The value object for the "photo_tags" table, representing a single database row. + */ +public class FSpotPhotoTagRow : Object { + public int64 photo_id; + public int64 tag_id; +} + +/** + * This class represents the F-Spot photo_tags table. + */ +public class FSpotPhotoTagsTable : FSpotDatabaseTable<FSpotPhotoTagRow> { + public static const string TABLE_NAME = "Photo_Tags"; + + public FSpotPhotoTagsTable(Sqlite.Database db, FSpotDatabaseBehavior db_behavior) { + base(db); + set_behavior(db_behavior.get_photo_tags_behavior()); + } +} + +public class FSpotPhotoTagsV0Behavior : FSpotTableBehavior<FSpotPhotoTagRow>, Object { + private static FSpotPhotoTagsV0Behavior instance; + + private FSpotPhotoTagsV0Behavior() { + } + + public static FSpotPhotoTagsV0Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotoTagsV0Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotoTagsTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "photo_id", "tag_id" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoTagRow row, int offset = 0) { + row = new FSpotPhotoTagRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.tag_id = stmt.column_int64(offset + 1); + } +} + +} + diff --git a/plugins/shotwell-data-imports/FSpotPhotoVersionsTable.vala b/plugins/shotwell-data-imports/FSpotPhotoVersionsTable.vala new file mode 100644 index 0000000..8378884 --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotPhotoVersionsTable.vala @@ -0,0 +1,271 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace DataImports.FSpot.Db { + +/** + * The value object for the "photo_versions" table, representing a single database row. + */ +public class FSpotPhotoVersionRow : Object { + public int64 photo_id; + public int64 version_id; + public string name; + public File? base_path; + public string? filename; + public string md5_sum; + public bool is_protected; +} + +/** + * This class represents the F-Spot photo_versions table. + */ +public class FSpotPhotoVersionsTable : FSpotDatabaseTable<FSpotPhotoVersionRow> { + public static const string TABLE_NAME = "Photo_versions"; + + public FSpotPhotoVersionsTable(Sqlite.Database db, FSpotDatabaseBehavior db_behavior) { + base(db); + set_behavior(db_behavior.get_photo_versions_behavior()); + } + + public Gee.ArrayList<FSpotPhotoVersionRow> get_by_photo_id(int64 photo_id) throws DatabaseError { + Gee.ArrayList<FSpotPhotoVersionRow> rows = new Gee.ArrayList<FSpotPhotoVersionRow?>(); + + Sqlite.Statement stmt; + + string column_list = get_joined_column_list(); + string sql = "SELECT %s FROM %s WHERE photo_id=?".printf( + column_list, table_name + ); + + int res = fspot_db.prepare_v2(sql, -1, out stmt); + if (res != Sqlite.OK) + throw_error("Statement failed: %s".printf(sql), res); + + res = stmt.bind_int64(1, photo_id); + if (res != Sqlite.OK) + throw_error("Bind failed for photo_id", res); + + res = stmt.step(); + while (res == Sqlite.ROW) { + FSpotPhotoVersionRow row; + behavior.build_row(stmt, out row); + rows.add(row); + res = stmt.step(); + } + + return rows; + } +} + +// Photo_versions table behavior for v0-8 +// Note: there is a change in the URI format in version 8 but the File.new_for_uri +// constructor should be able to deal with the variation, so the v8 behavior should +// be handled in a way identical to v0-7 +public class FSpotPhotoVersionsV0Behavior : FSpotTableBehavior<FSpotPhotoVersionRow>, Object { + private static FSpotPhotoVersionsV0Behavior instance; + + private FSpotPhotoVersionsV0Behavior() { + } + + public static FSpotPhotoVersionsV0Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotoVersionsV0Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotoVersionsTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "photo_id", "version_id", "name", "uri" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoVersionRow row, int offset = 0) { + row = new FSpotPhotoVersionRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.version_id = stmt.column_int64(offset + 1); + row.name = stmt.column_text(offset + 2); + + string? full_path = stmt.column_text(offset + 3); + if (full_path != null) { + File uri = File.new_for_uri(full_path); + row.base_path = uri.get_parent(); + row.filename = uri.get_basename(); + } + + row.md5_sum = ""; + row.is_protected = false; + } +} + +// Photo_versions table behavior for v9-15 +// add protected field +public class FSpotPhotoVersionsV9Behavior : FSpotTableBehavior<FSpotPhotoVersionRow>, Object { + private static FSpotPhotoVersionsV9Behavior instance; + + private FSpotPhotoVersionsV9Behavior() { + } + + public static FSpotPhotoVersionsV9Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotoVersionsV9Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotoVersionsTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "photo_id", "version_id", "name", "uri", + "protected" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoVersionRow row, int offset = 0) { + row = new FSpotPhotoVersionRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.version_id = stmt.column_int64(offset + 1); + row.name = stmt.column_text(offset + 2); + + string? full_path = stmt.column_text(offset + 3); + if (full_path != null) { + File uri = File.new_for_uri(full_path); + row.base_path = uri.get_parent(); + row.filename = uri.get_basename(); + } + + row.md5_sum = ""; + row.is_protected = (stmt.column_int(offset + 4) > 0); + } +} + +// Photo_versions table behavior for v16 +// add md5_sum in photo_versions +public class FSpotPhotoVersionsV16Behavior : FSpotTableBehavior<FSpotPhotoVersionRow>, Object { + private static FSpotPhotoVersionsV16Behavior instance; + + private FSpotPhotoVersionsV16Behavior() { + } + + public static FSpotPhotoVersionsV16Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotoVersionsV16Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotoVersionsTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "photo_id", "version_id", "name", "uri", + "md5_sum", "protected" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoVersionRow row, int offset = 0) { + row = new FSpotPhotoVersionRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.version_id = stmt.column_int64(offset + 1); + row.name = stmt.column_text(offset + 2); + + string? full_path = stmt.column_text(offset + 3); + if (full_path != null) { + File uri = File.new_for_uri(full_path); + row.base_path = uri.get_parent(); + row.filename = uri.get_basename(); + } + + row.md5_sum = stmt.column_text(offset + 4); + row.is_protected = (stmt.column_int(offset + 5) > 0); + } +} + +// Photo_versions table behavior for v17 +// v17 split the URI into base_uri and filename (reverting back to the original +// design introduced in v0, albeit with a URI rather than a file system path) +public class FSpotPhotoVersionsV17Behavior : FSpotTableBehavior<FSpotPhotoVersionRow>, Object { + private static FSpotPhotoVersionsV17Behavior instance; + + private FSpotPhotoVersionsV17Behavior() { + } + + public static FSpotPhotoVersionsV17Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotoVersionsV17Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotoVersionsTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "photo_id", "version_id", "name", "base_uri", "filename", + "md5_sum", "protected" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoVersionRow row, int offset = 0) { + row = new FSpotPhotoVersionRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.version_id = stmt.column_int64(offset + 1); + row.name = stmt.column_text(offset + 2); + + string? base_path = stmt.column_text(offset + 3); + string? filename = stmt.column_text(offset + 4); + if (base_path != null && filename != null) { + row.base_path = File.new_for_uri(base_path); + row.filename = filename; + } + + row.md5_sum = stmt.column_text(offset + 5); + row.is_protected = (stmt.column_int(offset + 6) > 0); + } +} + +// Photo_versions table behavior for v18 +// md5_sum renamed import_md5 +public class FSpotPhotoVersionsV18Behavior : FSpotTableBehavior<FSpotPhotoVersionRow>, Object { + private static FSpotPhotoVersionsV18Behavior instance; + + private FSpotPhotoVersionsV18Behavior() { + } + + public static FSpotPhotoVersionsV18Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotoVersionsV18Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotoVersionsTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "photo_id", "version_id", "name", "base_uri", "filename", + "import_md5", "protected" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoVersionRow row, int offset = 0) { + row = new FSpotPhotoVersionRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.version_id = stmt.column_int64(offset + 1); + row.name = stmt.column_text(offset + 2); + + string? base_path = stmt.column_text(offset + 3); + string? filename = stmt.column_text(offset + 4); + if (base_path != null && filename != null) { + row.base_path = File.new_for_uri(base_path); + row.filename = filename; + } + + row.md5_sum = stmt.column_text(offset + 5); + row.is_protected = (stmt.column_int(offset + 6) > 0); + } +} + +} + diff --git a/plugins/shotwell-data-imports/FSpotPhotosTable.vala b/plugins/shotwell-data-imports/FSpotPhotosTable.vala new file mode 100644 index 0000000..4231102 --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotPhotosTable.vala @@ -0,0 +1,356 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace DataImports.FSpot.Db { + +/** + * The value object for the "photos" table, representing a single database row. + */ +public class FSpotPhotoRow : Object { + public int64 photo_id; + public time_t time; + public File? base_path; + public string? filename; + public string description; + public int64 roll_id; + public int64 default_version_id; + public int rating; + public string md5_sum; +} + +/** + * This class represents the F-Spot photos table. + */ +public class FSpotPhotosTable : FSpotDatabaseTable<FSpotPhotoRow> { + public static const string TABLE_NAME = "Photos"; + + public FSpotPhotosTable(Sqlite.Database db, FSpotDatabaseBehavior db_behavior) { + base(db); + set_behavior(db_behavior.get_photos_behavior()); + } + + public Gee.ArrayList<FSpotPhotoRow> get_all() throws DatabaseError { + Gee.ArrayList<FSpotPhotoRow> all = new Gee.ArrayList<FSpotPhotoRow?>(); + + Sqlite.Statement stmt; + int res = select_all(out stmt); + while (res == Sqlite.ROW) { + FSpotPhotoRow row; + behavior.build_row(stmt, out row); + all.add(row); + res = stmt.step(); + } + + return all; + } +} + +// Photos table behavior for v0-4 +// The original table format +public class FSpotPhotosV0Behavior : FSpotTableBehavior<FSpotPhotoRow>, Object { + private static FSpotPhotosV0Behavior instance; + + private FSpotPhotosV0Behavior() { + } + + public static FSpotPhotosV0Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotosV0Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotosTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "id", "time", "directory_path", "name", "description", + "default_version_id" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoRow row, int offset = 0) { + row = new FSpotPhotoRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.time = (time_t) stmt.column_int64(offset + 1); + + string? base_path = stmt.column_text(offset + 2); + string? filename = stmt.column_text(offset + 3); + if (base_path != null && filename != null) { + row.base_path = File.new_for_uri(base_path); + row.filename = filename; + } + + row.description = stmt.column_text(offset + 4); + row.roll_id = INVALID_ID; + row.default_version_id = stmt.column_int64(offset + 5); + row.rating = 0; + row.md5_sum = ""; + } +} + +// Photos table behavior for v5-6 +// v5 introduced a roll_id to reference the imported roll (rolls were a new +// table migrated from imports) +public class FSpotPhotosV5Behavior : FSpotTableBehavior<FSpotPhotoRow>, Object { + private static FSpotPhotosV5Behavior instance; + + private FSpotPhotosV5Behavior() { + } + + public static FSpotPhotosV5Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotosV5Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotosTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "id", "time", "directory_path", "name", "description", "roll_id", + "default_version_id" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoRow row, int offset = 0) { + row = new FSpotPhotoRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.time = (time_t) stmt.column_int64(offset + 1); + + string? base_path = stmt.column_text(offset + 2); + string? filename = stmt.column_text(offset + 3); + if (base_path != null && filename != null) { + row.base_path = File.new_for_uri(base_path); + row.filename = filename; + } + + row.description = stmt.column_text(offset + 4); + row.roll_id = stmt.column_int64(offset + 5); + row.default_version_id = stmt.column_int64(offset + 6); + row.rating = 0; + row.md5_sum = ""; + } +} + +// Photos table behavior for v7-10 +// v7 merged directory_path and name into a single URI value with a file:// +// prefix; presumaly this is meant to be able to handle remote files using a +// different URI prefix such as remote files +public class FSpotPhotosV7Behavior : FSpotTableBehavior<FSpotPhotoRow>, Object { + private static FSpotPhotosV7Behavior instance; + + private FSpotPhotosV7Behavior() { + } + + public static FSpotPhotosV7Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotosV7Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotosTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "id", "time", "uri", "description", "roll_id", + "default_version_id" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoRow row, int offset = 0) { + row = new FSpotPhotoRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.time = (time_t) stmt.column_int64(offset + 1); + + string? full_path = stmt.column_text(offset + 2); + if (full_path != null) { + File uri = File.new_for_uri(full_path); + row.base_path = uri.get_parent(); + row.filename = uri.get_basename(); + } + + row.description = stmt.column_text(offset + 3); + row.roll_id = stmt.column_int64(offset + 4); + row.default_version_id = stmt.column_int64(offset + 5); + row.rating = 0; + row.md5_sum = ""; + } +} + +// Photos table behavior for v11-15 +// v11 introduced the concept of rating so add this to the list of fields +public class FSpotPhotosV11Behavior : FSpotTableBehavior<FSpotPhotoRow>, Object { + private static FSpotPhotosV11Behavior instance; + + private FSpotPhotosV11Behavior() { + } + + public static FSpotPhotosV11Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotosV11Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotosTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "id", "time", "uri", "description", "roll_id", + "default_version_id", "rating" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoRow row, int offset = 0) { + row = new FSpotPhotoRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.time = (time_t) stmt.column_int64(offset + 1); + + string? full_path = stmt.column_text(offset + 2); + if (full_path != null) { + File uri = File.new_for_uri(full_path); + row.base_path = uri.get_parent(); + row.filename = uri.get_basename(); + } + + row.description = stmt.column_text(offset + 3); + row.roll_id = stmt.column_int64(offset + 4); + row.default_version_id = stmt.column_int64(offset + 5); + row.rating = stmt.column_int(offset + 6); + row.md5_sum = ""; + } +} + +// Photos table behavior for v16 +// v16 introduced the MD5 sum so add this to the list of fields +public class FSpotPhotosV16Behavior : FSpotTableBehavior<FSpotPhotoRow>, Object { + private static FSpotPhotosV16Behavior instance; + + private FSpotPhotosV16Behavior() { + } + + public static FSpotPhotosV16Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotosV16Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotosTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "id", "time", "uri", "description", "roll_id", + "default_version_id", "rating", "md5_sum" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoRow row, int offset = 0) { + row = new FSpotPhotoRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.time = (time_t) stmt.column_int64(offset + 1); + + string? full_path = stmt.column_text(offset + 2); + if (full_path != null) { + File uri = File.new_for_uri(full_path); + row.base_path = uri.get_parent(); + row.filename = uri.get_basename(); + } + + row.description = stmt.column_text(offset + 3); + row.roll_id = stmt.column_int64(offset + 4); + row.default_version_id = stmt.column_int64(offset + 5); + row.rating = stmt.column_int(offset + 6); + row.md5_sum = stmt.column_text(offset + 7); + } +} + +// Photos table behavior for v17 +// v17 split the URI into base_uri and filename (reverting back to the original +// design introduced in v0, albeit with a URI rather than a file system path) +public class FSpotPhotosV17Behavior : FSpotTableBehavior<FSpotPhotoRow>, Object { + private static FSpotPhotosV17Behavior instance; + + private FSpotPhotosV17Behavior() { + } + + public static FSpotPhotosV17Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotosV17Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotosTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "id", "time", "base_uri", "filename", "description", "roll_id", + "default_version_id", "rating", "md5_sum" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoRow row, int offset = 0) { + row = new FSpotPhotoRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.time = (time_t) stmt.column_int64(offset + 1); + + string? base_path = stmt.column_text(offset + 2); + string? filename = stmt.column_text(offset + 3); + if (base_path != null && filename != null) { + row.base_path = File.new_for_uri(base_path); + row.filename = filename; + } + + row.description = stmt.column_text(offset + 4); + row.roll_id = stmt.column_int64(offset + 5); + row.default_version_id = stmt.column_int64(offset + 6); + row.rating = stmt.column_int(offset + 7); + row.md5_sum = stmt.column_text(offset + 8); + } +} + +// v18: no more MD5 hash in the photos table: moved to photo_versions table +public class FSpotPhotosV18Behavior : FSpotTableBehavior<FSpotPhotoRow>, Object { + private static FSpotPhotosV18Behavior instance; + + private FSpotPhotosV18Behavior() { + } + + public static FSpotPhotosV18Behavior get_instance() { + if (instance == null) + instance = new FSpotPhotosV18Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotPhotosTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "id", "time", "base_uri", "filename", "description", "roll_id", + "default_version_id", "rating" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotPhotoRow row, int offset = 0) { + row = new FSpotPhotoRow(); + row.photo_id = stmt.column_int64(offset + 0); + row.time = (time_t) stmt.column_int64(offset + 1); + + string? base_path = stmt.column_text(offset + 2); + string? filename = stmt.column_text(offset + 3); + if (base_path != null && filename != null) { + row.base_path = File.new_for_uri(base_path); + row.filename = filename; + } + + row.description = stmt.column_text(offset + 4); + row.roll_id = stmt.column_int64(offset + 5); + row.default_version_id = stmt.column_int64(offset + 6); + row.rating = stmt.column_int(offset + 7); + row.md5_sum = ""; + } +} + +} + diff --git a/plugins/shotwell-data-imports/FSpotRollsTable.vala b/plugins/shotwell-data-imports/FSpotRollsTable.vala new file mode 100644 index 0000000..fd5362d --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotRollsTable.vala @@ -0,0 +1,111 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace DataImports.FSpot.Db { + +/** + * The value object for the "rolls" table, representing a single database row. + */ +public class FSpotRollRow : Object { + public int64 id; + public time_t time; +} + +/** + * This class represents the F-Spot rolls table. + */ +public class FSpotRollsTable : FSpotDatabaseTable<FSpotRollRow> { + public static const string TABLE_NAME = "Rolls"; + public static const string TABLE_NAME_PRE_V5 = "Imports"; + + public FSpotRollsTable(Sqlite.Database db, FSpotDatabaseBehavior db_behavior) { + base(db); + set_behavior(db_behavior.get_rolls_behavior()); + } + + public FSpotRollRow? get_by_id(int64 roll_id) throws DatabaseError { + Sqlite.Statement stmt; + FSpotRollRow? row = null; + string column_list = get_joined_column_list(); + string sql = "SELECT %s FROM %s WHERE id=?".printf(column_list, table_name); + + int res = fspot_db.prepare_v2(sql, -1, out stmt); + if (res != Sqlite.OK) + throw_error("Statement failed: %s".printf(sql), res); + + res = stmt.bind_int64(1, roll_id); + if (res != Sqlite.OK) + throw_error("Bind failed for roll_id", res); + + res = stmt.step(); + if (res == Sqlite.ROW) + behavior.build_row(stmt, out row); + else if (res == Sqlite.DONE) + message("Could not find roll row with ID %d", (int)roll_id); + + return row; + } +} + +// Rolls table behavior for v0-4 +public class FSpotRollsV0Behavior : FSpotTableBehavior<FSpotRollRow>, Object { + private static FSpotRollsV0Behavior instance; + + private FSpotRollsV0Behavior() { + } + + public static FSpotRollsV0Behavior get_instance() { + if (instance == null) + instance = new FSpotRollsV0Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotRollsTable.TABLE_NAME_PRE_V5; + } + + public string[] list_columns() { + return { "id", "time" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotRollRow row, int offset = 0) { + row = new FSpotRollRow(); + row.id = stmt.column_int64(offset + 0); + row.time = (time_t) stmt.column_int64(offset + 1); + } +} + +// Rolls table behavior for v5+ +// Table name changed from "imports" to "rolls" +public class FSpotRollsV5Behavior : FSpotTableBehavior<FSpotRollRow>, Object { + private static FSpotRollsV5Behavior instance; + + private FSpotRollsV5Behavior() { + } + + public static FSpotRollsV5Behavior get_instance() { + if (instance == null) + instance = new FSpotRollsV5Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotRollsTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "id", "time" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotRollRow row, int offset = 0) { + row = new FSpotRollRow(); + row.id = stmt.column_int64(offset + 0); + row.time = (time_t) stmt.column_int64(offset + 1); + } +} + +} + diff --git a/plugins/shotwell-data-imports/FSpotTableBehavior.vala b/plugins/shotwell-data-imports/FSpotTableBehavior.vala new file mode 100644 index 0000000..2d94427 --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotTableBehavior.vala @@ -0,0 +1,28 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace DataImports.FSpot.Db { + +/** + * This class defines a generic table behavior. In practice, it implements + * the concept of a DAO (Data Access Object) in ORM terms and is responsible + * for transforming the data extracted from a relational statement into a + * lightweight value object. + * + * The type T defined in the generic is the value object type a behavior + * implementation is designed to handle. Value object types are designed to + * contain the data for a single database row. + */ +public interface FSpotTableBehavior<T> : Object { + public abstract string get_table_name(); + + public abstract string[] list_columns(); + + public abstract void build_row(Sqlite.Statement stmt, out T row, int offset = 0); +} + +} + diff --git a/plugins/shotwell-data-imports/FSpotTagsTable.vala b/plugins/shotwell-data-imports/FSpotTagsTable.vala new file mode 100644 index 0000000..07045cf --- /dev/null +++ b/plugins/shotwell-data-imports/FSpotTagsTable.vala @@ -0,0 +1,129 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace DataImports.FSpot.Db { + +/** + * The value object for the "tags" table, representing a single database row. + */ +public class FSpotTagRow : Object { + public int64 tag_id; + public string name; + public int64 category_id; + public bool is_category; + public int sort_priority; + public string stock_icon; // only store stock icons +} + +/** + * This class represents the F-Spot tags table. + */ +public class FSpotTagsTable : FSpotDatabaseTable<FSpotTagRow> { + public static const string TABLE_NAME = "Tags"; + + public static const string PREFIX_STOCK_ICON = "stock_icon:"; + public static const string STOCK_ICON_FAV = "stock_icon:emblem-favorite"; + public static const string STOCK_ICON_PEOPLE = "stock_icon:emblem-people"; + public static const string STOCK_ICON_PLACES = "stock_icon:emblem-places"; + public static const string STOCK_ICON_EVENTS = "stock_icon:emblem-event"; + + private FSpotTableBehavior<FSpotPhotoTagRow> photo_tags_behavior; + + public FSpotTagsTable(Sqlite.Database db, FSpotDatabaseBehavior db_behavior) { + base(db); + set_behavior(db_behavior.get_tags_behavior()); + photo_tags_behavior = db_behavior.get_photo_tags_behavior(); + } + + public FSpotTagRow? get_by_id(int64 tag_id) throws DatabaseError { + Sqlite.Statement stmt; + FSpotTagRow? row = null; + string column_list = get_joined_column_list(); + string sql = "SELECT %s FROM %s WHERE id=?".printf(column_list, table_name); + + int res = fspot_db.prepare_v2(sql, -1, out stmt); + if (res != Sqlite.OK) + throw_error("Statement failed: %s".printf(sql), res); + + res = stmt.bind_int64(1, tag_id); + assert(res == Sqlite.OK); + + res = stmt.step(); + if (res == Sqlite.ROW) + behavior.build_row(stmt, out row); + else if (res == Sqlite.DONE) + message("Could not find tag row with ID %d", (int)tag_id); + + return row; + } + + public Gee.ArrayList<FSpotTagRow> get_by_photo_id(int64 photo_id) throws DatabaseError { + Gee.ArrayList<FSpotTagRow> rows = new Gee.ArrayList<FSpotTagRow?>(); + + Sqlite.Statement stmt; + + string column_list = get_joined_column_list(true); + string sql = "SELECT %1$s FROM %2$s, %3$s WHERE %3$s.photo_id=? AND %3$s.tag_id = %2$s.id".printf( + column_list, table_name, photo_tags_behavior.get_table_name() + ); + + int res = fspot_db.prepare_v2(sql, -1, out stmt); + if (res != Sqlite.OK) + throw_error("Statement failed: %s".printf(sql), res); + + res = stmt.bind_int64(1, photo_id); + if (res != Sqlite.OK) + throw_error("Bind failed for photo_id", res); + + res = stmt.step(); + while (res == Sqlite.ROW) { + FSpotTagRow row; + behavior.build_row(stmt, out row); + rows.add(row); + res = stmt.step(); + } + + return rows; + } +} + +public class FSpotTagsV0Behavior : FSpotTableBehavior<FSpotTagRow>, Object { + private static FSpotTagsV0Behavior instance; + + private FSpotTagsV0Behavior() { + } + + public static FSpotTagsV0Behavior get_instance() { + if (instance == null) + instance = new FSpotTagsV0Behavior(); + return instance; + } + + public string get_table_name() { + return FSpotTagsTable.TABLE_NAME; + } + + public string[] list_columns() { + return { "id", "name", "category_id", "is_category", "sort_priority", "icon" }; + } + + public void build_row(Sqlite.Statement stmt, out FSpotTagRow row, int offset = 0) { + row = new FSpotTagRow(); + row.tag_id = stmt.column_int64(offset + 0); + row.name = stmt.column_text(offset + 1); + row.category_id = stmt.column_int64(offset + 2); + row.is_category = (stmt.column_int(offset + 3) > 0); + row.sort_priority = stmt.column_int(offset + 4); + string icon_str = stmt.column_text(offset + 5); + if (icon_str != null && icon_str.has_prefix(FSpotTagsTable.PREFIX_STOCK_ICON)) + row.stock_icon = icon_str; + else + row.stock_icon = ""; + } +} + +} + diff --git a/plugins/shotwell-data-imports/Makefile b/plugins/shotwell-data-imports/Makefile new file mode 100644 index 0000000..52329e7 --- /dev/null +++ b/plugins/shotwell-data-imports/Makefile @@ -0,0 +1,30 @@ + +PLUGIN := shotwell-data-imports + +PLUGIN_PKGS := \ + gtk+-3.0 \ + gexiv2 \ + gee-0.8 \ + sqlite3 + +SRC_FILES := \ + shotwell-data-imports.vala \ + ../common/VersionNumber.vala \ + ../common/SqliteSupport.vala \ + FSpotImporter.vala \ + FSpotDatabaseBehavior.vala \ + FSpotDatabase.vala \ + FSpotDatabaseTable.vala \ + FSpotTableBehavior.vala \ + FSpotMetaTable.vala \ + FSpotPhotosTable.vala \ + FSpotPhotoTagsTable.vala \ + FSpotPhotoVersionsTable.vala \ + FSpotRollsTable.vala \ + FSpotTagsTable.vala + +RC_FILES := \ + f-spot-24.png + +include ../Makefile.plugin.mk + diff --git a/plugins/shotwell-data-imports/f-spot-24.png b/plugins/shotwell-data-imports/f-spot-24.png Binary files differnew file mode 100644 index 0000000..fda9672 --- /dev/null +++ b/plugins/shotwell-data-imports/f-spot-24.png diff --git a/plugins/shotwell-data-imports/shotwell-data-imports.vala b/plugins/shotwell-data-imports/shotwell-data-imports.vala new file mode 100644 index 0000000..c6e7d46 --- /dev/null +++ b/plugins/shotwell-data-imports/shotwell-data-imports.vala @@ -0,0 +1,46 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +extern const string _VERSION; + +// "core services" are: F-Spot +private class ShotwellDataImportsCoreServices : Object, Spit.Module { + private Spit.Pluggable[] pluggables = new Spit.Pluggable[0]; + + // we need to get a module file handle because our pluggables have to load resources from the + // module file directory + public ShotwellDataImportsCoreServices(GLib.File module_file) { + GLib.File resource_directory = module_file.get_parent(); + + pluggables += new FSpotService(resource_directory); + } + + public unowned string get_module_name() { + return _("Core Data Import Services"); + } + + public unowned string get_version() { + return _VERSION; + } + + public unowned string get_id() { + return "org.yorba.shotwell.data_imports.core_services"; + } + + public unowned Spit.Pluggable[]? get_pluggables() { + return pluggables; + } +} + +// This entry point is required for all SPIT modules. +public Spit.Module? spit_entry_point(Spit.EntryPointParams *params) { + params->module_spit_interface = Spit.negotiate_interfaces(params->host_min_spit_interface, + params->host_max_spit_interface, Spit.CURRENT_INTERFACE); + + return (params->module_spit_interface != Spit.UNSUPPORTED_INTERFACE) + ? new ShotwellDataImportsCoreServices(params->module_file) : null; +} + |