Commit 222cb80a authored by Jim Nelson's avatar Jim Nelson

#139: F-Spot migration. Courtesy the diligent and dedicated work of Bruno Girin. Thanks, Bruno!

parent ccadbad0
......@@ -117,7 +117,12 @@ SRC_FILES = \
DirectoryMonitor.vala \
LibraryMonitor.vala \
OfflinePage.vala \
LastImportPage.vala
LastImportPage.vala \
AlienDatabase.vala \
AlienDatabaseImportJob.vala \
AlienDatabaseImportDialog.vala \
FSpotDatabaseDriver.vala \
FSpotDatabaseTables.vala
ifndef LINUX
SRC_FILES += \
......
......@@ -9,13 +9,13 @@ David Jeske <davidj@gmail.com>
Matt Jones <mattjones@workhorsy.org>
Andreas Kühntopf <andreas@kuehntopf.org>
Dominic Lloyd <dwlloyd@telus.net>
Tobias Lott <tobias@lott.eu.org>
Rafael Monica <monraaf@gmail.com>
Paul Novak <pnovak@alumni.caltech.edu>
Martin Robinson <martin.james.robinson@gmail.com>
Marcel Stimberg <stimberg@users.sourceforge.net>
Patrick Tierney <patrick@yorba.org>
Shan Xiong <shan.xiong@gmail.com>
Tobias Lott <tobias@lott.eu.org>
Translations courtesy of:
......
/* Copyright 2009-2010 Yorba Foundation
*
* This software is licensed under the GNU LGPL (version 2.1 or later).
* See the COPYING file in this distribution.
*/
/**
* The error domain for alien databases
*/
public errordomain AlienDatabaseError {
// Generic database error
DATABASE_ERROR,
// Unsupported version: this can be due to an old legacy database,
// a new database version that this version of Shotwell doesn't support
// or a database version that could not be identified properly.
UNSUPPORTED_VERSION,
// Unsupported data: although the database version is supported, the
// driver has been unable to read a specific piece of the data from the
// database. This can be due to invalid data or a data format that is not
// supported by the mapping behavior objects.
UNSUPPORTED_DATA
}
/**
* The core handler that is responsible for handling the different plugins
* and dispatching requests to the relevant driver.
*/
public class AlienDatabaseHandler {
private static AlienDatabaseHandler instance;
private Gee.Map<AlienDatabaseDriverID?, AlienDatabaseDriver> driver_map;
/**
* Initialisation method that creates a singleton instance.
*/
public static void init() {
instance = new AlienDatabaseHandler();
}
/**
* Termination method that clears the singleton instance.
*/
public static void terminate() {
instance = null;
}
public static AlienDatabaseHandler get_instance() {
return instance;
}
private AlienDatabaseHandler() {
driver_map = new Gee.HashMap<AlienDatabaseDriverID?, AlienDatabaseDriver>(
AlienDatabaseDriverID.hash, AlienDatabaseDriverID.equal, AlienDatabaseDriverID.equal
);
// at the moment, just one driver
// TODO: change to a real plugin mechanism where each driver can
// be installed independently from Shotwell
register_driver(new FSpotDatabaseDriver());
}
public void register_driver(AlienDatabaseDriver driver) {
driver_map.set(driver.get_id(), driver);
}
public void unregister_driver(AlienDatabaseDriver driver) {
driver_map.unset(driver.get_id());
}
public Gee.Collection<AlienDatabaseDriver> get_drivers() {
return driver_map.values;
}
public AlienDatabaseDriver get_driver(AlienDatabaseDriverID driver_id) {
AlienDatabaseDriver driver = driver_map.get(driver_id);
if (driver == null)
warning("Could not find driver for id: %s", driver_id.id);
return driver;
}
public Gee.Collection<DiscoveredAlienDatabase> get_discovered_databases() {
Gee.ArrayList<DiscoveredAlienDatabase> discovered_databases =
new Gee.ArrayList<DiscoveredAlienDatabase>();
foreach (AlienDatabaseDriver driver in driver_map.values) {
discovered_databases.add_all(driver.get_discovered_databases());
}
return discovered_databases;
}
}
/**
* A simple struct to represent an alien database driver ID.
*/
public struct AlienDatabaseDriverID {
public string id;
public AlienDatabaseDriverID(string id) {
this.id = id;
}
public static uint hash(void *a) {
return ((AlienDatabaseDriverID *) a)->id.hash();
}
public static bool equal(void *a, void *b) {
return ((AlienDatabaseDriverID *) a)->id == ((AlienDatabaseDriverID *) b)->id;
}
}
/**
* The main driver interface that all plugins should implement. This driver
* interface is designed to automatically discover databases and create
* instances of AlienDatabase that can interrogate the data.
*/
public interface AlienDatabaseDriver : Object {
/**
* Return a unique ID for this alien database driver.
*/
public abstract AlienDatabaseDriverID get_id();
/**
* Return the display name for this driver.
*/
public abstract string get_display_name();
/**
* This method returns all databases that are automatically discovered by
* the driver.
*/
public abstract Gee.Collection<DiscoveredAlienDatabase> get_discovered_databases();
/**
* This method opens a database given a database ID and returns an object
* that is able to interrogate the data contained in the database.
*/
public abstract AlienDatabase open_database(AlienDatabaseID db_id) throws AlienDatabaseError;
/**
* This method opens a database given a file and returns an object
* that is able to interrogate the data contained in the database.
*/
public abstract AlienDatabase open_database_from_file(File db_file) throws AlienDatabaseError;
public abstract string get_menu_name();
public abstract Gtk.ActionEntry get_action_entry();
}
/**
* A simple struct to represent an alien database ID.
*/
public struct AlienDatabaseID {
public AlienDatabaseDriverID driver_id;
public string driver_specific_uri;
public AlienDatabaseID(AlienDatabaseDriverID driver_id, string driver_specific_uri) {
this.driver_id = driver_id;
this.driver_specific_uri = driver_specific_uri;
}
public AlienDatabaseID.from_uri(string db_uri) {
string[] uri_elements = db_uri.split(":", 2);
if (uri_elements.length < 2) {
error("Cannot create alien database ID from URI: %s", db_uri);
} else {
this.driver_id = AlienDatabaseDriverID(uri_elements[0]);
this.driver_specific_uri = uri_elements[1];
}
}
public string to_uri() {
return "%s:%s".printf(driver_id.id, driver_specific_uri);
}
public static uint hash(void *a) {
return (
AlienDatabaseDriverID.hash(&((AlienDatabaseID *) a)->driver_id) ^
((AlienDatabaseID *) a)->driver_specific_uri.hash()
);
}
public static bool equal(void *a, void *b) {
return (
AlienDatabaseDriverID.equal(&((AlienDatabaseID *) a)->driver_id, &((AlienDatabaseID *) b)->driver_id) &&
(((AlienDatabaseID *) a)->driver_specific_uri == ((AlienDatabaseID *) b)->driver_specific_uri)
);
}
}
/**
* A light-weight wrapper that contains enough information to display the
* database entry in the library window but can delay instantiating the
* actual database instance until it is actually needed.
*/
public class DiscoveredAlienDatabase : Object {
private AlienDatabaseID id;
private AlienDatabaseDriver driver;
private AlienDatabase database;
public DiscoveredAlienDatabase(AlienDatabaseID id) {
this.id = id;
driver = AlienDatabaseHandler.get_instance().get_driver(id.driver_id);
}
public AlienDatabaseID get_id() {
return id;
}
public string get_uri() {
return id.to_uri();
}
/**
* This method creates an actual instance of the database interface.
* It is called when the application is ready to present the database
* to the user as a page in the main library window.
*/
public AlienDatabase get_database() throws AlienDatabaseError {
if (database == null) {
database = driver.open_database(id);
}
return database;
}
/**
* Release the underlying database object.
*/
public void release_database() {
database = null;
}
}
/**
* The main database interface that all plugins should implement. The driver
* should return an instance of a class that implements this interface for
* each open database. This interface is then used to query the underlying
* database in order to import photographs. The driver itself is free to
* instantiate objects of different classes for different database files if
* required. For example, it is conceivable that a driver could supply
* different implementations for different versions of the same database.
*/
public interface AlienDatabase : Object {
public abstract string get_uri();
public abstract string get_display_name();
public abstract AlienDatabaseVersion get_version() throws AlienDatabaseError;
public abstract Gee.Collection<AlienDatabasePhoto> get_photos() throws AlienDatabaseError;
}
/**
* The main interface for a single instance of a photo held in the database.
* This interface assumes that the photograph can be accessed via the Vala
* File classes.
*/
public interface AlienDatabasePhoto : Object {
public abstract string get_folder_path();
public abstract string get_filename();
public abstract Gee.Collection<AlienDatabaseTag> get_tags();
public abstract AlienDatabaseEvent? get_event();
public abstract Rating get_rating();
public abstract string? get_title();
public abstract ImportID? get_import_id();
}
/**
* The main interface for a single instance of a tag held in the database.
*/
public interface AlienDatabaseTag : Object {
public abstract string get_name();
}
/**
* The main interface for a single instance of an event held in the database.
*/
public interface AlienDatabaseEvent : Object {
public abstract string get_name();
}
/**
* A class that represents a version in the form x.y.z and is able to compare
* different versions.
*/
public class AlienDatabaseVersion : Object, Gee.Comparable<AlienDatabaseVersion> {
private int[] version;
public AlienDatabaseVersion(int[] version) {
this.version = version;
}
public AlienDatabaseVersion.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] = version_items[i].to_int();
}
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(AlienDatabaseVersion 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;
}
}
/* Copyright 2009-2010 Yorba Foundation
*
* This software is licensed under the GNU LGPL (version 2.1 or later).
* See the COPYING file in this distribution.
*/
public class AlienDatabaseImportDialog {
private static const int MSG_NOTEBOOK_PAGE_EMPTY = 0;
private static const int MSG_NOTEBOOK_PAGE_PROGRESS = 1;
private static const int MSG_NOTEBOOK_PAGE_ERROR = 2;
private Gtk.Dialog dialog;
private Gtk.Builder builder;
private AlienDatabaseDriver driver;
private DiscoveredAlienDatabase? selected_database = null;
private File? selected_file = null;
private Gtk.FileChooserButton file_chooser;
private Gtk.RadioButton? file_chooser_radio;
private Gtk.Notebook message_notebook;
private Gtk.ProgressBar prepare_progress_bar;
private Gtk.Label error_message_label;
private Gtk.Button ok_button;
private Gtk.Button cancel_button;
public AlienDatabaseImportDialog(string title, AlienDatabaseDriver driver) {
this.driver = driver;
builder = AppWindow.create_builder();
dialog = builder.get_object("alien-db-import_dialog") as Gtk.Dialog;
dialog.title = title;
dialog.set_parent_window(AppWindow.get_instance().get_parent_window());
dialog.set_transient_for(AppWindow.get_instance());
ok_button = builder.get_object("ok_button") as Gtk.Button;
ok_button.clicked.connect(on_ok_button_clicked);
cancel_button = builder.get_object("cancel_button") as Gtk.Button;
cancel_button.clicked.connect(on_cancel_button_clicked);
file_chooser = builder.get_object("db_filechooserbutton") as Gtk.FileChooserButton;
file_chooser.file_set.connect(on_file_chooser_file_set);
message_notebook = builder.get_object("message_notebook") as Gtk.Notebook;
message_notebook.set_current_page(MSG_NOTEBOOK_PAGE_EMPTY);
prepare_progress_bar = builder.get_object("prepare_progress_bar") as Gtk.ProgressBar;
error_message_label = builder.get_object("import_error_label") as Gtk.Label;
Gtk.Box options_box = builder.get_object("options_box") as Gtk.Box;
Gee.Collection<DiscoveredAlienDatabase> discovered_databases = driver.get_discovered_databases();
if (discovered_databases.size > 0) {
Gtk.RadioButton db_radio = null;
foreach (DiscoveredAlienDatabase db in discovered_databases) {
string db_radio_label =
_("Import from default %1$s library (%2$s)").printf(
driver.get_display_name(),
collapse_user_path(db.get_id().driver_specific_uri)
);
db_radio = create_radio_button(options_box, db_radio, db, db_radio_label);
}
file_chooser_radio = create_radio_button(
options_box, db_radio, null,
_("Import from another %s database file:").printf(driver.get_display_name())
);
} else {
Gtk.Label custom_file_label = new Gtk.Label(
_("Import from a %s database file:").printf(driver.get_display_name())
);
options_box.pack_start(custom_file_label, true, true, 6);
}
set_ok_sensitivity();
}
public string collapse_user_path(string path) {
string result = path;
string home_dir = Environment.get_home_dir();
if (path.has_prefix(home_dir)) {
long cidx = home_dir.length;
if (home_dir[home_dir.length - 1] == '/')
cidx--;
result = "~%s".printf(path.substring(cidx));
}
return result;
}
public void on_ok_button_clicked() {
AlienDatabase? alien_db = null;
try {
if (selected_database != null)
alien_db = selected_database.get_database();
else if (selected_file != null)
alien_db = driver.open_database_from_file(selected_file);
if (alien_db == null) {
message_notebook.set_current_page(MSG_NOTEBOOK_PAGE_ERROR);
error_message_label.set_label(_("No database selected"));
} else {
message_notebook.set_current_page(MSG_NOTEBOOK_PAGE_PROGRESS);
prepare_progress_bar.set_fraction(0.0);
ok_button.set_sensitive(false);
cancel_button.set_sensitive(false);
spin_event_loop();
SortedList<AlienDatabaseImportJob> jobs =
new SortedList<AlienDatabaseImportJob>(import_job_comparator);
Gee.ArrayList<AlienDatabaseImportJob> already_imported =
new Gee.ArrayList<AlienDatabaseImportJob>();
Gee.ArrayList<AlienDatabaseImportJob> failed =
new Gee.ArrayList<AlienDatabaseImportJob>();
Gee.Collection<AlienDatabasePhoto> photos = alien_db.get_photos();
int photo_total = photos.size;
int photo_idx = 0;
foreach (AlienDatabasePhoto src_photo in photos) {
AlienDatabaseImportSource import_source = new AlienDatabaseImportSource(src_photo);
if (import_source.is_already_imported()) {
message("Skipping import of %s: checksum detected in library",
import_source.get_filename());
already_imported.add(new AlienDatabaseImportJob(import_source));
continue;
}
jobs.add(new AlienDatabaseImportJob(import_source));
photo_idx++;
prepare_progress_bar.set_fraction((double)photo_idx / (double)photo_total);
spin_event_loop();
}
if (jobs.size > 0) {
string db_name = _("%s Database").printf(alien_db.get_display_name());
BatchImport batch_import = new BatchImport(jobs, db_name, alien_import_reporter,
failed, already_imported);
LibraryWindow.get_app().enqueue_batch_import(batch_import, true);
LibraryWindow.get_app().switch_to_import_queue_page();
}
// clean up
if (selected_database != null) {
selected_database.release_database();
selected_database = null;
}
dialog.destroy();
}
} catch (AlienDatabaseError e) {
message_notebook.set_current_page(MSG_NOTEBOOK_PAGE_ERROR);
error_message_label.set_label(_("Shotwell failed to load the database file"));
// most failures should happen before the two buttons have been set
// to the insensitive state but you never know so set them back to the
// normal state so that the user can interact with them
ok_button.set_sensitive(true);
cancel_button.set_sensitive(true);
}
}
public void on_cancel_button_clicked() {
dialog.destroy();
}
public void on_file_chooser_file_set() {
selected_file = file_chooser.get_file();
if (file_chooser_radio != null)
file_chooser_radio.active = true;
set_ok_sensitivity();
}
public void show() {
dialog.show_all();
}
public Gtk.RadioButton create_radio_button(
Gtk.Box box, Gtk.RadioButton? group, DiscoveredAlienDatabase? alien_db, string label
) {
var button = new Gtk.RadioButton.with_label_from_widget (group, label);
if (group == null) { // first radio button is active
button.active = true;
selected_database = alien_db;
}
button.toggled.connect (() => {
if (button.active) {
this.selected_database = alien_db;
set_ok_sensitivity();
}
});
box.pack_start(button, true, true, 6);
return button;
}
private void set_ok_sensitivity() {
ok_button.set_sensitive((selected_database != null || selected_file != null));
}
private int64 import_job_comparator(void *a, void *b) {
return ((AlienDatabaseImportJob *) a)->get_exposure_time()
- ((AlienDatabaseImportJob *) b)->get_exposure_time();
}
}
private void alien_import_reporter(ImportManifest manifest, BatchImportRoll import_roll) {
ImportUI.report_manifest(manifest, true);
}
/* Copyright 2009-2010 Yorba Foundation
*
* This software is licensed under the GNU LGPL (version 2.1 or later).
* See the COPYING file in this distribution.
*/
/**
* Photo source implementation for alien databases. This class is responsible
* for extracting meta-data out of a source photo to support the import
* process.
*/
public class AlienDatabaseImportSource : PhotoSource {
protected new const string THUMBNAIL_NAME_PREFIX = "import";
public const Gdk.InterpType INTERP = Gdk.InterpType.BILINEAR;
private AlienDatabasePhoto db_photo;
private PhotoMetadata? metadata = null;
private PhotoPreview? preview = null;
private Gdk.Pixbuf? preview_pixbuf = null;
private string? preview_md5 = null;
private string? exif_md5 = null;
private uint64 file_size;
private time_t modification_time;
public AlienDatabaseImportSource(AlienDatabasePhoto db_photo) {
this.db_photo = db_photo;
File photo = File.new_for_path(db_photo.get_folder_path()).
get_child(db_photo.get_filename());
metadata = new PhotoMetadata();
try {
metadata.read_from_file(photo);
} catch(Error e) {
warning("Could not get file metadata for %s: %s", get_filename(), e.message);
}
uint8[]? flattened_sans_thumbnail = metadata.flatten_exif(false);
if (flattened_sans_thumbnail != null && flattened_sans_thumbnail.length > 0)
exif_md5 = md5_binary(flattened_sans_thumbnail, flattened_sans_thumbnail.length);
preview = metadata != null ? metadata.get_preview(0) : null;
try {
preview_pixbuf = preview != null ? preview.get_pixbuf() : null;
} catch(Error e) {
warning("Could not get preview pixbuf for %s: %s", get_filename(), e.message);
}
if (preview != null) {
try {
uint8[] preview_raw = preview.flatten();
preview_md5 = md5_binary(preview_raw, preview_raw.length);
} catch(Error e) {
warning("Could not get raw preview for %s: %s", get_filename(), e.message);
}
}
#if TRACE_MD5
debug("Photo MD5 %s: exif=%s preview=%s", get_filename(), exif_md5, preview_md5);
#endif
try {
file_size = query_total_file_size(photo);
} catch(Error e) {
warning("Could not get file size for %s: %s", get_filename(), e.message);
}
try {
modification_time = query_file_modified(photo);
} catch(Error e) {
warning("Could not get modification time for %s: %s", get_filename(), e.message);
}
}
public string get_filename() {
return db_photo.get_filename();
}
public string get_fulldir() {
return db_photo.get_folder_path();
}
public File get_file() {
return File.new_for_path(get_fulldir()).get_child(get_filename());
}
public override string get_name() {
string? title = get_title();
return !is_string_empty(title) ? title : get_filename();
}
public string? get_title() {
return (metadata != null) ? metadata.get_title() : null;
}
public string? get_preview_md5() {
return preview_md5;
}
public PhotoFileFormat get_file_format() {
return PhotoFileFormat.get_by_basename_extension(get_filename());
}
public override string to_string() {
return get_name();
}
public override time_t get_exposure_time() {
if (metadata == null)
return modification_time;
MetadataDateTime? date_time = metadata.get_exposure_date_time();
return (date_time != null) ? date_time.get_timestamp() : modification_time;
}
public override Dimensions get_dimensions() {
if (metadata == null)
return Dimensions(0, 0);
Dimensions? dim = metadata.get_pixel_dimensions();
if (dim == null)
return Dimensions(0, 0);
return metadata.get_orientation().rotate_dimensions(dim);
}
public override uint64 get_filesize() {
return file_size;
}
public override PhotoMetadata? get_metadata() {
return metadata;
}
public override Gdk.Pixbuf get_pixbuf(Scaling scaling) throws Error {
return preview_pixbuf != null ? scaling.perform_on_pixbuf(preview_pixbuf, INTERP, false) : null;
}
public override Gdk.Pixbuf? get_thumbnail(int scale) throws Error {
if (preview_pixbuf == null)
return null;
return (scale > 0) ? scale_pixbuf(preview_pixbuf, scale, INTERP, true) : preview_pixbuf;
}
public AlienDatabasePhoto get_photo() {
return db_photo;
}
public override string? get_unique_thumbnail_name() {
return (THUMBNAIL_NAME_PREFIX + "-%" + int64.FORMAT).printf(get_object_id());
}
public override PhotoFileFormat get_preferred_thumbnail_format() {
return PhotoFileFormat.get_system_default_format();
}
public override Gdk.Pixbuf? create_thumbnail(int scale) throws Error {
if (preview_pixbuf == null)
return null;
// this satifies the return-a-new-instance requirement of create_thumbnail( ) because
// scale_pixbuf( ) allocates a new pixbuf
return (scale > 0) ? scale_pixbuf(preview_pixbuf, scale, INTERP, true) : preview_pixbuf;
}
public bool is_already_imported() {
// ignore trashed duplicates
return (preview_md5 != null)
? LibraryPhoto.has_nontrash_duplicate(null, preview_md5, null, get_file_format())
: false;
}