Commit 92ba8a5b authored by Bruno Girin's avatar Bruno Girin Committed by Lucas Beeler

Adds a SPIT-based extension point and API for importing photos from other...

Adds a SPIT-based extension point and API for importing photos from other applications; ports the existing F-Spot importer to use this new API. Closes #3614.
parent 4e477819
......@@ -383,6 +383,14 @@
</key>
</schema>
<schema id="org.yorba.shotwell.dataimports" path="/apps/shotwell/dataimports/">
<key name="last-used-dataimports-service" type="s">
<default>""</default>
<summary>last used import service</summary>
<description>A numeric code representing the last service from which photos were imported</description>
</key>
</schema>
<schema id="org.yorba.shotwell.video" path="/apps/shotwell/video/">
<key name="interpreter-state-cookie" type="i">
<default>-1</default>
......@@ -494,6 +502,12 @@
<description>True if the Yandex.Fotki publishing plugin is enabled, false otherwise</description>
</key>
<key name="dataimports-fspot" type="b">
<default>true</default>
<summary>enable F-Spot import plugin</summary>
<description>True if the F-Spot import plugin is enabled, false otherwise</description>
</key>
<key name="transitions-crumble" type="b">
<default>true</default>
<summary>enable slideshow crumble transition</summary>
......
/* Copyright 2009-2011 Yorba Foundation
*
* This software is licensed under the GNU LGPL (version 2.1 or later).
* See the COPYING file in this distribution.
*/
public errordomain DatabaseError {
ERROR,
BACKING,
MEMORY,
ABORT,
LIMITS,
TYPESPEC
}
public abstract class DatabaseTable {
protected static Sqlite.Database db;
public string table_name = null;
protected void set_table_name(string table_name) {
this.table_name = table_name;
}
// This method will throw an error on an SQLite return code unless it's OK, DONE, or ROW, which
// are considered normal results.
protected static void throw_error(string method, int res) throws DatabaseError {
string msg = "(%s) [%d] - %s".printf(method, res, db.errmsg());
switch (res) {
case Sqlite.OK:
case Sqlite.DONE:
case Sqlite.ROW:
return;
case Sqlite.PERM:
case Sqlite.BUSY:
case Sqlite.READONLY:
case Sqlite.IOERR:
case Sqlite.CORRUPT:
case Sqlite.CANTOPEN:
case Sqlite.NOLFS:
case Sqlite.AUTH:
case Sqlite.FORMAT:
case Sqlite.NOTADB:
throw new DatabaseError.BACKING(msg);
case Sqlite.NOMEM:
throw new DatabaseError.MEMORY(msg);
case Sqlite.ABORT:
case Sqlite.LOCKED:
case Sqlite.INTERRUPT:
throw new DatabaseError.ABORT(msg);
case Sqlite.FULL:
case Sqlite.EMPTY:
case Sqlite.TOOBIG:
case Sqlite.CONSTRAINT:
case Sqlite.RANGE:
throw new DatabaseError.LIMITS(msg);
case Sqlite.SCHEMA:
case Sqlite.MISMATCH:
throw new DatabaseError.TYPESPEC(msg);
case Sqlite.ERROR:
case Sqlite.INTERNAL:
case Sqlite.MISUSE:
default:
throw new DatabaseError.ERROR(msg);
}
}
}
/* Copyright 2011 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 Utils {
/**
* A class that represents a version number in the form x.y.z and is able to compare
* different versions.
*/
public class VersionNumber : Object, Gee.Comparable<VersionNumber> {
private int[] version;
public VersionNumber(int[] version) {
this.version = version;
}
public VersionNumber.from_string(string str_version, string separator = ".") {
string[] version_items = str_version.split(separator);
this.version = new int[version_items.length];
for (int i = 0; i < version_items.length; i++)
this.version[i] = int.parse(version_items[i]);
}
public string to_string() {
string[] version_items = new string[this.version.length];
for (int i = 0; i < this.version.length; i++)
version_items[i] = this.version[i].to_string();
return string.joinv(".", version_items);
}
public int compare_to(VersionNumber other) {
int max_len = ((this.version.length > other.version.length) ?
this.version.length : other.version.length);
int res = 0;
for(int i = 0; i < max_len; i++) {
int this_v = (i < this.version.length ? this.version[i] : 0);
int other_v = (i < other.version.length ? other.version[i] : 0);
res = this_v - other_v;
if (res != 0)
break;
}
return res;
}
}
}
PLUGINS := \
shotwell-transitions \
shotwell-publishing
shotwell-publishing \
shotwell-data-imports
PLUGINS_RC := \
plugins/shotwell-publishing/facebook.png \
......@@ -17,7 +18,8 @@ EXTRA_PLUGINS_RC := \
plugins/shotwell-publishing-extras/yandex_publish_model.glade \
plugins/shotwell-publishing-extras/piwigo.png \
plugins/shotwell-publishing-extras/piwigo_authentication_pane.glade \
plugins/shotwell-publishing-extras/piwigo_publishing_options_pane.glade
plugins/shotwell-publishing-extras/piwigo_publishing_options_pane.glade \
plugins/shotwell-data-imports/f-spot-24.png
ALL_PLUGINS := $(PLUGINS) $(EXTRA_PLUGINS)
/* Copyright 2011 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 implementation of AlienDatabase that is able to read from the F-Spot
* database and extract the relevant objects.
*/
public class FSpotDatabase : Object {
//private AlienDatabaseID id;
private Sqlite.Database fspot_db;
private FSpotMetaTable meta_table;
public FSpotPhotosTable photos_table;
public FSpotPhotoVersionsTable photo_versions_table;
public FSpotTagsTable tags_table;
//private FSpotTagsCache tags_cache;
public FSpotRollsTable rolls_table;
public int64 hidden_tag_id;
/*public FSpotDatabase(FSpotDatabaseDriver driver, AlienDatabaseID id) throws DatabaseError, AlienDatabaseError {
this.id = id;
initialize(driver, id.driver_specific_uri);
}*/
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);
//tags_cache = new FSpotTagsCache(tags_table);
rolls_table = new FSpotRollsTable(fspot_db, db_behavior);
}
~FSpotDatabase() {
}
/*public string get_uri() {
return id.to_uri();
}
public string get_display_name() {
return _("F-Spot");
}*/
private Utils.VersionNumber get_version() throws DatabaseError {
return new Utils.VersionNumber.from_string(meta_table.get_db_version());
}
/*public Gee.Collection<AlienDatabasePhoto> get_photos() throws DatabaseError {
Gee.List<AlienDatabasePhoto> photos = new Gee.ArrayList<AlienDatabasePhoto>();
foreach (FSpotPhotoRow photo_row in photos_table.get_all()) {
bool hidden = false;
bool favorite = false;
Gee.ArrayList<AlienDatabaseTag> tags = new Gee.ArrayList<AlienDatabaseTag>();
AlienDatabaseEvent? event = null;
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 heirarchical, we would need to pick a name (probably the leaf)
try {
foreach (FSpotTagRow tag_row in tags_table.get_by_photo_id(photo_row.photo_id)) {
FSpotDatabaseTag tag = tags_cache.get_tag(tag_row);
if (is_tag_hidden(tag))
hidden = true;
else if (is_tag_favorite(tag))
favorite = true;
else
tags.add(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.id,
e.message);
}
try {
roll_row = 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.id,
e.message);
}
try {
bool photo_versions_added = false;
foreach (FSpotPhotoVersionRow photo_version_row in photo_versions_table.get_by_photo_id(photo_row.photo_id)) {
photos.add(new FSpotDatabasePhoto(
photo_row, photo_version_row, roll_row, tags, event, hidden, favorite
));
photo_versions_added = true;
}
// older versions of F-Spot (0.4.3.1 at least, perhaps later) did not maintain photo_versions,
// this handles that case
if (!photo_versions_added)
photos.add(new FSpotDatabasePhoto(
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.id,
e.message);
photos.add(new FSpotDatabasePhoto(
photo_row, null, roll_row, tags, event, hidden, favorite
));
}
}
return photos;
}*/
/*public bool is_tag_event(FSpotDatabaseTag tag) {
bool result = (FSpotTagsTable.STOCK_ICON_EVENTS == tag.get_row().stock_icon);
if (!result) {
FSpotDatabaseTag? parent = tag.get_fspot_parent();
if (parent == null)
result = false;
else
result = is_tag_event(parent);
}
return result;
}
public bool is_tag_hidden(FSpotDatabaseTag tag) {
bool result = (hidden_tag_id == tag.get_row().tag_id.id);
if (!result) {
FSpotDatabaseTag? parent = tag.get_fspot_parent();
if (parent == null)
result = false;
else
result = is_tag_hidden(parent);
}
return result;
}
public bool is_tag_favorite(FSpotDatabaseTag tag) {
bool result = (FSpotTagsTable.STOCK_ICON_FAV == tag.get_row().stock_icon);
if (!result) {
FSpotDatabaseTag? parent = tag.get_fspot_parent();
if (parent == null)
result = false;
else
result = is_tag_favorite(parent);
}
return result;
}*/
}
}
/* Copyright 2011 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;
}
}
}
/* Copyright 2009-2011 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> : DatabaseTable {
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;
}
}
}
/* Copyright 2009-2011 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-2012 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;
}