Commit a7386215 authored by Jim Nelson's avatar Jim Nelson

#189: Bug in libgphoto2 + pausing between unmount and camera_init() solved...

#189: Bug in libgphoto2 + pausing between unmount and camera_init() solved problem.  #301: Vala 0.7.3 resolves.  #192:Not a dirty flag issue, but iPhone changing its mount point name every time it's locked.  Resolved.  #196:Imported photos now saved in heirarchical directories, and unique filenames are guaranteed.
parent 97cff74d
......@@ -226,12 +226,11 @@ public class AppWindow : Gtk.Window {
public static void init(string[] args) {
AppWindow.args = args;
File dataDir = get_data_dir();
File data_dir = get_data_dir();
try {
if (dataDir.query_exists(null) == false) {
if (dataDir.make_directory_with_parents(null) == false) {
error("Unable to create data directory %s", dataDir.get_path());
}
if (data_dir.query_exists(null) == false) {
if (!data_dir.make_directory_with_parents(null))
error("Unable to create data directory %s", data_dir.get_path());
}
} catch (Error err) {
error("%s", err.message);
......@@ -272,9 +271,8 @@ public class AppWindow : Gtk.Window {
try {
if (subdir.query_exists(null) == false) {
if (subdir.make_directory_with_parents(null) == false) {
if (!subdir.make_directory_with_parents(null))
error("Unable to create data subdirectory %s", subdir.get_path());
}
}
} catch (Error err) {
error("%s", err.message);
......@@ -325,9 +323,6 @@ public class AppWindow : Gtk.Window {
private GPhoto.Context null_context = new GPhoto.Context();
private GPhoto.CameraAbilitiesList abilities_list;
private SortedList<int64?> imported_photos = null;
private ImportID import_id = ImportID();
private FullscreenWindow fullscreen_window = null;
public AppWindow() {
......@@ -488,103 +483,56 @@ public class AppWindow : Gtk.Window {
present();
}
public class DateComparator : Comparator<int64?> {
private PhotoTable photo_table;
public DateComparator(PhotoTable photo_table) {
this.photo_table = photo_table;
}
public override int64 compare(int64? ida, int64? idb) {
time_t timea = photo_table.get_exposure_time(PhotoID(ida));
time_t timeb = photo_table.get_exposure_time(PhotoID(idb));
return timea - timeb;
}
}
public void start_import_batch() {
imported_photos = new SortedList<int64?>(new Gee.ArrayList<int64?>(), new DateComparator(photo_table));
import_id = photo_table.generate_import_id();
}
public void import(File file) {
FileType type = file.query_file_type(FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
if(type == FileType.REGULAR) {
if (!import_file(file)) {
// TODO: These should be aggregated so the user gets one report and not multiple,
// one for each file imported
Gtk.MessageDialog dialog = new Gtk.MessageDialog(this, Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, "%s already stored",
file.get_path());
dialog.run();
dialog.destroy();
}
return;
} else if (type != FileType.DIRECTORY) {
debug("Skipping file %s (neither a directory nor a file)", file.get_path());
return;
}
debug("Importing directory %s", file.get_path());
import_dir(file);
}
public void end_import_batch() {
if (imported_photos == null)
return;
split_into_events(imported_photos);
public void photo_imported(Photo photo) {
// want to know when it's removed from the system for cleanup
photo.removed += on_photo_removed;
// reset
imported_photos = null;
import_id = ImportID();
// automatically add to the Photos page
collection_page.add_photo(photo);
collection_page.refresh();
}
public void split_into_events(SortedList<int64?> list) {
debug("Processing photos to create events ...");
public void batch_import_complete(SortedList<int64?> imported_photos) {
debug("Processing imported photos to create events ...");
// walk through photos, splitting into events based on criteria
time_t last_exposure = 0;
time_t current_event_start = 0;
EventID current_event_id = EventID();
EventPage current_page = null;
EventPage current_event_page = null;
foreach (int64 id in imported_photos) {
PhotoID photo_id = PhotoID(id);
Photo photo = Photo.fetch(PhotoID(id));
time_t exposure_time = photo.get_exposure_time();
PhotoRow photo;
bool found = photo_table.get_photo(photo_id, out photo);
assert(found);
if (photo.exposure_time == 0) {
if (exposure_time == 0) {
// no time recorded; skip
debug("Skipping %s: No exposure time", photo.file.get_path());
debug("Skipping event assignment to %s: No exposure time", photo.to_string());
continue;
}
if (photo.event_id.is_valid()) {
if (photo.get_event_id().is_valid()) {
// already part of an event; skip
debug("Skipping %s: Already part of event %lld", photo.file.get_path(),
photo.event_id.id);
debug("Skipping event assignment to %s: Already part of event %lld", photo.to_string(),
photo.get_event_id().id);
continue;
}
// see if enough time has elapsed to create a new event, or to store this photo in
// the current one
bool create_event = false;
if (last_exposure == 0) {
// first photo, start a new event
create_event = true;
} else {
assert(last_exposure <= photo.exposure_time);
assert(current_event_start <= photo.exposure_time);
assert(last_exposure <= exposure_time);
assert(current_event_start <= exposure_time);
if (photo.exposure_time - last_exposure >= EVENT_LULL_SEC) {
if (exposure_time - last_exposure >= EVENT_LULL_SEC) {
// enough time has passed between photos to signify a new event
create_event = true;
} else if (photo.exposure_time - current_event_start >= EVENT_MAX_DURATION_SEC) {
} else if (exposure_time - current_event_start >= EVENT_MAX_DURATION_SEC) {
// the current event has gone on for too long, stop here and start a new one
create_event = true;
}
......@@ -599,98 +547,28 @@ public class AppWindow : Gtk.Window {
events_directory_page.refresh();
}
current_event_start = photo.exposure_time;
current_event_id = event_table.create(photo_id, current_event_start);
current_event_start = exposure_time;
current_event_id = event_table.create(photo.get_photo_id(), current_event_start);
current_page = add_event_page(current_event_id);
current_event_page = add_event_page(current_event_id);
debug("Created event [%lld]", current_event_id.id);
}
assert(current_event_id.is_valid());
debug("Adding %s to event %lld (exposure=%ld last_exposure=%ld)", photo.file.get_path(),
current_event_id.id, photo.exposure_time, last_exposure);
debug("Adding %s to event %lld (exposure=%ld last_exposure=%ld)", photo.to_string(),
current_event_id.id, exposure_time, last_exposure);
photo_table.set_event(photo_id, current_event_id);
photo.set_event_id(current_event_id);
current_page.add_photo(Photo.fetch(photo_id));
last_exposure = photo.exposure_time;
}
}
private void import_dir(File dir) {
assert(dir.query_file_type(FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null) == FileType.DIRECTORY);
try {
FileEnumerator enumerator = dir.enumerate_children("*",
FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
if (enumerator == null) {
return;
}
for (;;) {
FileInfo info = enumerator.next_file(null);
if (info == null) {
break;
}
File file = dir.get_child(info.get_name());
FileType type = info.get_file_type();
if (type == FileType.REGULAR) {
if (!import_file(file)) {
// TODO: Better error reporting
message("Failed to import %s (already imported?)", file.get_path());
}
} else if (type == FileType.DIRECTORY) {
debug("Importing directory %s", file.get_path());
current_event_page.add_photo(photo);
import_dir(file);
} else {
debug("Skipped %s", file.get_path());
}
}
} catch (Error err) {
// TODO: Better error reporting
error("Error importing: %s", err.message);
last_exposure = exposure_time;
}
}
private bool import_file(File file) {
// TODO: This eases the pain until background threads are implemented
while (Gtk.events_pending()) {
if (Gtk.main_iteration()) {
debug("import_dir: Gtk.main_quit called");
return false;
}
}
Photo photo = Photo.import(file, import_id);
if (photo == null)
return false;
// want to know when it's removed from the system for cleanup
photo.removed += on_photo_removed;
// automatically add to the Photos page
collection_page.add_photo(photo);
collection_page.refresh();
// add to imported list for splitting into events
if (imported_photos != null) {
PhotoID photo_id = photo.get_photo_id();
imported_photos.add(photo_id.id);
}
return true;
}
private void on_photo_removed(Photo photo) {
debug("on_photo_removed");
PhotoID photo_id = photo.get_photo_id();
// update event's primary photo if this is the one; remove event if no more photos in it
......@@ -752,13 +630,11 @@ public class AppWindow : Gtk.Window {
// TODO: Try to read JFIF metadata too
PhotoExif exif = new PhotoExif(row.file);
if (exif.has_exif()) {
if (!exif.get_dimensions(out dim)) {
if (!exif.get_dimensions(out dim))
error("Unable to read EXIF dimensions for %s", row.file.get_path());
}
if (!exif.get_datetime_time(out exposure_time)) {
if (!exif.get_timestamp(out exposure_time))
error("Unable to read EXIF orientation for %s", row.file.get_path());
}
orientation = exif.get_orientation();
}
......@@ -789,21 +665,17 @@ public class AppWindow : Gtk.Window {
}
}
}
public override void drag_data_received(Gdk.DragContext context, int x, int y,
Gtk.SelectionData selection_data, uint info, uint time) {
// grab data and release back to system
// grab URIs and release back to system
string[] uris = selection_data.get_uris();
Gtk.drag_finish(context, true, false, time);
// TODO: Background threads
start_import_batch();
foreach (string uri in uris) {
import(File.new_for_uri(uri));
}
end_import_batch();
collection_page.refresh();
// do the importing from within the idle loop, so the DnD transaction is completed
// TODO: Configure if photos are copied into library
BatchImport batch_import = new BatchImport(uris);
batch_import.schedule();
}
public static void error_message(string message) {
......@@ -1385,7 +1257,7 @@ public class AppWindow : Gtk.Window {
}
private static void on_device_added(Hal.Context context, string udi) {
debug("******* on_device_added: %s", udi);
debug("on_device_added: %s", udi);
try {
AppWindow.get_instance().update_camera_table();
......@@ -1395,7 +1267,7 @@ public class AppWindow : Gtk.Window {
}
private static void on_device_removed(Hal.Context context, string udi) {
debug("******** on_device_removed: %s", udi);
debug("on_device_removed: %s", udi);
try {
AppWindow.get_instance().update_camera_table();
......
public class BatchImport {
private class DateComparator : Comparator<int64?> {
private PhotoTable photo_table;
public DateComparator(PhotoTable photo_table) {
this.photo_table = photo_table;
}
public override int64 compare(int64? ida, int64? idb) {
time_t timea = photo_table.get_exposure_time(PhotoID(ida));
time_t timeb = photo_table.get_exposure_time(PhotoID(idb));
return timea - timeb;
}
}
private string[] uris;
private BatchImport ref_holder = null;
private PhotoTable photo_table = new PhotoTable();
private SortedList<int64?> imported_photos = null;
private Gee.ArrayList<string> import_failed = null;
private ImportID import_id = ImportID();
public static File? create_library_path(string filename, Exif.Data? exif, time_t ts, out bool collision) {
File dir = AppWindow.get_photos_dir();
time_t timestamp = ts;
// use EXIF exposure timestamp over the supplied one (which probably comes from the file's
// modified time, or is simply now())
if (exif != null) {
Exif.Entry entry = Exif.find_first_entry(exif, Exif.Tag.DATE_TIME_ORIGINAL, Exif.Format.ASCII);
if (entry != null) {
string datetime = entry.get_value();
if (datetime != null) {
time_t stamp;
if (Exif.convert_datetime(datetime, out stamp)) {
timestamp = stamp;
}
}
}
}
// if no timestamp, use now()
if (timestamp == 0)
timestamp = time_t();
Time tm = Time.local(timestamp);
// build a directory tree inside the library:
// yyyy/mm/dd
dir = dir.get_child("%04u".printf(tm.year + 1900));
dir = dir.get_child("%02u".printf(tm.month + 1));
dir = dir.get_child("%02u".printf(tm.day));
try {
if (dir.query_exists(null) == false)
dir.make_directory_with_parents(null);
} catch (Error err) {
error("Unable to create photo library directory %s", dir.get_path());
}
// if file doesn't exist, use that and done
File file = dir.get_child(filename);
if (!file.query_exists(null)) {
collision = false;
return file;
}
collision = true;
string name, ext;
disassemble_filename(file.get_basename(), out name, out ext);
// generate a unique filename
for (int ctr = 1; ctr < int.MAX; ctr++) {
string new_name = (ext != null) ? "%s_%d.%s".printf(name, ctr, ext) : "%s_%d".printf(name, ctr);
file = dir.get_child(new_name);
if (!file.query_exists(null))
return file;
}
return null;
}
public BatchImport(string[] uris) {
this.uris = uris;
}
public void schedule() {
// XXX: This is necessary because Idle.add doesn't ref SourceFunc:
// http://bugzilla.gnome.org/show_bug.cgi?id=548427
this.ref_holder = this;
Idle.add(on_import_uris);
}
private bool on_import_uris() {
imported_photos = new SortedList<int64?>(new Gee.ArrayList<int64?>(), new DateComparator(photo_table));
import_failed = new Gee.ArrayList<string>();
import_id = photo_table.generate_import_id();
// import one at a time
foreach (string uri in uris)
import(File.new_for_uri(uri));
// report errors, if any
// TODO: More informative dialog box
if (import_failed.size > 0)
AppWindow.error_message("Unable to import %d photos".printf(import_failed.size));
// report all new photos to AppWindow
AppWindow.get_instance().batch_import_complete(imported_photos);
// XXX: unref "this" ... vital that the self pointer is not touched from here on out
ref_holder = null;
return false;
}
private void import(File file) {
FileType type = file.query_file_type(FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
bool imported = false;
switch (type) {
case FileType.DIRECTORY:
imported = import_dir(file);
break;
case FileType.REGULAR:
imported = import_file(file);
break;
default:
debug("Skipping file %s (neither a directory nor a file)", file.get_path());
break;
}
if (!imported)
import_failed.add(file.get_path());
}
private bool import_dir(File dir) {
try {
FileEnumerator enumerator = dir.enumerate_children("*",
FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
if (enumerator == null)
return false;
if (!spin_event_loop())
return false;
FileInfo info = null;
while ((info = enumerator.next_file(null)) != null) {
import(dir.get_child(info.get_name()));
}
} catch (Error err) {
debug("Unable to import from %s: %s", dir.get_path(), err.message);
return false;
}
return true;
}
private bool import_file(File file) {
Photo photo = Photo.import(file, import_id);
if (photo == null)
return false;
if (!spin_event_loop())
return false;
// add to imported list for splitting into events
PhotoID photo_id = photo.get_photo_id();
imported_photos.add(photo_id.id);
// report to AppWindow for it to disseminate
AppWindow.get_instance().photo_imported(photo);
return true;
}
}
......@@ -261,13 +261,9 @@ public class CollectionPage : CheckerboardPage {
}
private void on_photo_removed(Photo photo) {
debug("%s on_photo_removed", get_name());
Thumbnail found = get_thumbnail_for_photo(photo);
if (found != null) {
debug("Removing %s from %s", photo.to_string(), get_name());
if (found != null)
remove_item(found);
}
}
private void on_thumbnail_altered(Photo photo) {
......
......@@ -20,6 +20,22 @@ namespace Exif {
return null;
}
public bool convert_datetime(string datetime, out time_t timestamp) {
Time tm = Time();
int count = datetime.scanf("%d:%d:%d %d:%d:%d", &tm.year, &tm.month, &tm.day, &tm.hour,
&tm.minute, &tm.second);
if (count != 6)
return false;
tm.year -= 1900;
tm.month--;
tm.isdst = -1;
timestamp = tm.mktime();
return true;
}
}
namespace Jpeg {
......@@ -114,8 +130,8 @@ public class PhotoExif {
return Orientation.TOP_LEFT;
int o = Exif.Convert.get_short(entry.data, exifData.get_byte_order());
assert(o >= (int) Orientation.MIN);
assert(o <= (int) Orientation.MAX);
if (o < (int) Orientation.MIN || o > (int) Orientation.MAX)
return Orientation.TOP_LEFT;
return (Orientation) o;
}
......@@ -171,24 +187,12 @@ public class PhotoExif {
return datetime.get_value();
}
public bool get_datetime_time(out time_t timet) {
string text = get_datetime();
if (text == null)
return false;
Time tm = Time();
int count = text.scanf("%d:%d:%d %d:%d:%d", &tm.year, &tm.month, &tm.day, &tm.hour,
&tm.minute, &tm.second);
if (count != 6)
public bool get_timestamp(out time_t timestamp) {
string datetime = get_datetime();
if (datetime == null)
return false;
tm.year -= 1900;
tm.month--;
tm.isdst = -1;
timet = tm.mktime();
return true;
return Exif.convert_datetime(datetime, out timestamp);
}
private void update() {
......
......@@ -13,30 +13,30 @@ namespace GPhoto {
public Gdk.Pixbuf? load_preview(Context context, Camera camera, string folder, string filename,
uint8[] buffer) throws Error {
int bytesRead = load_file(context, camera, folder, filename, GPhoto.CameraFileType.PREVIEW, buffer);
if (bytesRead == 0)
int bytes_read = load_file(context, camera, folder, filename, GPhoto.CameraFileType.PREVIEW, buffer);
if (bytes_read == 0)
return null;
assert(bytesRead > 0);
assert(bytes_read > 0);
MemoryInputStream mins = new MemoryInputStream.from_data(buffer, bytesRead, null);
MemoryInputStream mins = new MemoryInputStream.from_data(buffer, bytes_read, null);
return new Gdk.Pixbuf.from_stream(mins, null);
}
public Gdk.Pixbuf? load_image(Context context, Camera camera, string folder, string filename) throws Error {
GPhoto.CameraFile cameraFile;
GPhoto.Result res = GPhoto.CameraFile.create(out cameraFile);
GPhoto.CameraFile camera_file;
GPhoto.Result res = GPhoto.CameraFile.create(out camera_file);
if (res != Result.OK)
throw new GPhotoError.LIBRARY("[%d] Error allocating camera file: %s", (int) res, res.as_string());
res = camera.get_file(folder, filename, GPhoto.CameraFileType.NORMAL, cameraFile, context);
res = camera.get_file(folder, filename, GPhoto.CameraFileType.NORMAL, camera_file, context);
if (res != Result.OK)
throw new GPhotoError.LIBRARY("[%d] Error retrieving file object for %s/%s: %s",
(int) res, folder, filename, res.as_string());
// TODO: I know, I know.
res = cameraFile.save("shotwell.tmp");
res = camera_file.save("shotwell.tmp");
if (res != Result.OK)
throw new GPhotoError.LIBRARY("[%d] Error copying file %s/%s to %s: %s", (int) res,
folder, filename, "shotwell.tmp", res.as_string());
......@@ -64,36 +64,40 @@ namespace GPhoto {
public Exif.Data? load_exif(Context context, Camera camera, string folder, string filename,
uint8[] buffer) throws Error {
int bytesRead = load_file(context, camera, folder, filename, GPhoto.CameraFileType.EXIF, buffer);
if (bytesRead == 0)
int bytes_read = load_file(context, camera, folder, filename, GPhoto.CameraFileType.EXIF, buffer);
if (bytes_read == 0)
return null;
assert(bytesRead > 0);
assert(bytes_read > 0);
Exif.Data data = Exif.Data.new_from_data(buffer, bytesRead);
Exif.Data data = Exif.Data.new_from_data(buffer, bytes_read);
data.fix();
return data;
}
public int load_file(Context context, Camera camera, string folder, string filename, GPhoto.CameraFileType filetype,
uint8[] buffer) throws Error{
GPhoto.CameraFile cameraFile;
GPhoto.Result res = GPhoto.CameraFile.create(out cameraFile);
public int load_file(Context context, Camera camera, string folder, string filename,
GPhoto.CameraFileType filetype, uint8[] buffer) throws Error{
GPhoto.CameraFile camera_file;
GPhoto.Result res = GPhoto.CameraFile.create(out camera_file);
if (res != Result.OK)
throw new GPhotoError.LIBRARY("[%d] Error allocating camera file: %s", (int) res, res.as_string());
res = camera.get_file(folder, filename, filetype, cameraFile, context);
res = camera.get_file(folder, filename, filetype, camera_file, context);
if (res != Result.OK)
throw new GPhotoError.LIBRARY("[%d] Error retrieving file object for %s/%s: %s",
(int) res, folder, filename, res.as_string());
int bytesRead = 0;
res = cameraFile.slurp(buffer, out bytesRead);
unowned uint8[] data;
res = camera_file.get_data_and_size(out data);
if (res != Result.OK)
throw new GPhotoError.LIBRARY("[%d] Error retrieving file %s/%s: %s", (int) res,