From 6710aa856175300e598b23b701c0d2741f2cb6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= Date: Sun, 28 Apr 2019 16:45:36 +0200 Subject: New upstream version 0.30.4 --- .../shotwell/FlickrPublishingAuthenticator.vala | 6 +- .../shotwell/GoogleAuthenticator.vala | 3 +- .../shotwell/ShotwellAuthenticatorFactory.vala | 9 +- plugins/common/RESTSupport.vala | 12 + plugins/common/Resources.vala | 7 +- plugins/shotwell-publishing/PhotosPublisher.vala | 551 +++++++++++++++ .../shotwell-publishing/PhotosPublishingPane.vala | 150 ++++ plugins/shotwell-publishing/PhotosService.vala | 58 ++ plugins/shotwell-publishing/PhotosUploader.vala | 104 +++ plugins/shotwell-publishing/PicasaPublishing.vala | 770 --------------------- plugins/shotwell-publishing/flickr.png | Bin 689 -> 0 bytes plugins/shotwell-publishing/google-photos.svg | 1 + .../google_photos_publishing_options_pane.ui | 221 ++++++ plugins/shotwell-publishing/meson.build | 11 +- .../org.gnome.Shotwell.Publishing.gresource.xml | 5 +- .../picasa_publishing_options_pane.ui | 176 ----- .../shotwell-publishing/shotwell-publishing.vala | 8 +- 17 files changed, 1123 insertions(+), 969 deletions(-) create mode 100644 plugins/shotwell-publishing/PhotosPublisher.vala create mode 100644 plugins/shotwell-publishing/PhotosPublishingPane.vala create mode 100644 plugins/shotwell-publishing/PhotosService.vala create mode 100644 plugins/shotwell-publishing/PhotosUploader.vala delete mode 100644 plugins/shotwell-publishing/PicasaPublishing.vala delete mode 100644 plugins/shotwell-publishing/flickr.png create mode 100644 plugins/shotwell-publishing/google-photos.svg create mode 100644 plugins/shotwell-publishing/google_photos_publishing_options_pane.ui delete mode 100644 plugins/shotwell-publishing/picasa_publishing_options_pane.ui (limited to 'plugins') diff --git a/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala b/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala index 97629ed..5188ed6 100644 --- a/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala +++ b/plugins/authenticator/shotwell/FlickrPublishingAuthenticator.vala @@ -15,6 +15,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr { internal const string SERVICE_WELCOME_MESSAGE = _("You are not currently logged into Flickr.\n\nClick Log in to log into Flickr in your Web browser. You will have to authorize Shotwell Connect to link to your Flickr account."); + internal const string SERVICE_DISCLAIMER = "This product uses the Flickr API but is not endorsed or certified by SmugMug, Inc."; internal class AuthenticationRequestTransaction : Publishing.RESTSupport.OAuth1.Transaction { public AuthenticationRequestTransaction(Publishing.RESTSupport.OAuth1.Session session) { @@ -55,7 +56,8 @@ namespace Publishing.Authenticator.Shotwell.Flickr { public override void on_page_load() { var uri = new Soup.URI(get_view().get_uri()); if (uri.scheme == "shotwell-auth" && this.auth_code == null) { - this.error(); + var form_data = Soup.Form.decode (uri.query); + this.auth_code = form_data.lookup("oauth_verifier"); } if (this.auth_code != null) { @@ -108,7 +110,7 @@ namespace Publishing.Authenticator.Shotwell.Flickr { debug("ACTION: installing login welcome pane"); host.set_service_locked(false); - host.install_welcome_pane(SERVICE_WELCOME_MESSAGE, on_welcome_pane_login_clicked); + host.install_welcome_pane("%s\n\n%s".printf(SERVICE_WELCOME_MESSAGE, SERVICE_DISCLAIMER), on_welcome_pane_login_clicked); } private void on_welcome_pane_login_clicked() { diff --git a/plugins/authenticator/shotwell/GoogleAuthenticator.vala b/plugins/authenticator/shotwell/GoogleAuthenticator.vala index f561197..75d8f37 100644 --- a/plugins/authenticator/shotwell/GoogleAuthenticator.vala +++ b/plugins/authenticator/shotwell/GoogleAuthenticator.vala @@ -23,7 +23,8 @@ namespace Publishing.Authenticator.Shotwell.Google { public override void on_page_load() { var uri = new Soup.URI(get_view().get_uri()); if (uri.scheme == REVERSE_CLIENT_ID && this.auth_code == null) { - this.error(); + var form_data = Soup.Form.decode (uri.query); + this.auth_code = form_data.lookup("code"); } if (this.auth_code != null) { diff --git a/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala b/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala index 0d813ac..36fb290 100644 --- a/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala +++ b/plugins/authenticator/shotwell/ShotwellAuthenticatorFactory.vala @@ -14,9 +14,9 @@ namespace Publishing.Authenticator { var list = new Gee.ArrayList(); list.add("flickr"); list.add("facebook"); - list.add("picasa"); list.add("youtube"); list.add("tumblr"); + list.add("google-photos"); return list; } @@ -28,14 +28,13 @@ namespace Publishing.Authenticator { return new Shotwell.Flickr.Flickr(host); case "facebook": return new Shotwell.Facebook.Facebook(host); - case "picasa": - return new Shotwell.Google.Google("https://picasaweb.google.com/data/", _("You are not currently logged into Picasa Web Albums.\n\nClick Log in to log into Picasa Web Albums in your Web browser. You will have to authorize Shotwell Connect to link to your Picasa Web Albums account."), host); - case "youtube": return new Shotwell.Google.Google("https://gdata.youtube.com/", _("You are not currently logged into YouTube.\n\nYou must have already signed up for a Google account and set it up for use with YouTube to continue. You can set up most accounts by using your browser to log into the YouTube site at least once."), host); case "tumblr": return new Shotwell.Tumblr.Tumblr(host); - default: + case "google-photos": + return new Shotwell.Google.Google("https://www.googleapis.com/auth/photoslibrary", _("You are not currently logged into Google Photos.\n\nYou must have already signed up for a Google account and set it up for use with Google Photos.\n\nYou will have to authorize Shotwell to link to your Google Photos account."), host); + default: return null; } } diff --git a/plugins/common/RESTSupport.vala b/plugins/common/RESTSupport.vala index 482fd2c..5cd3768 100644 --- a/plugins/common/RESTSupport.vala +++ b/plugins/common/RESTSupport.vala @@ -444,6 +444,18 @@ public class Transaction { public void add_argument(string name, string value) { arguments += new Argument(name, value); } + + public void set_argument(string name, string value) { + foreach (var arg in arguments) { + if (arg.key == name) { + arg.value = value; + + return; + } + } + + add_argument(name, value); + } public string? get_endpoint_url() { return (endpoint_url != null) ? endpoint_url : parent_session.get_endpoint_url(); diff --git a/plugins/common/Resources.vala b/plugins/common/Resources.vala index 29c7294..ecbf2f8 100644 --- a/plugins/common/Resources.vala +++ b/plugins/common/Resources.vala @@ -42,7 +42,7 @@ public Gdk.Pixbuf[]? load_icon_set(GLib.File? icon_file) { try { icon = new Gdk.Pixbuf.from_file(icon_file.get_path()); } catch (Error err) { - warning("couldn't load icon set from %s.", icon_file.get_path()); + warning("couldn't load icon set from %s: %s", icon_file.get_path(), err.message); } if (icon != null) { @@ -57,9 +57,10 @@ public Gdk.Pixbuf[]? load_icon_set(GLib.File? icon_file) { public Gdk.Pixbuf[]? load_from_resource (string resource_path) { Gdk.Pixbuf? icon = null; try { - icon = new Gdk.Pixbuf.from_resource (resource_path); + debug ("Loading icon from %s", resource_path); + icon = new Gdk.Pixbuf.from_resource_at_scale (resource_path, 24, 24, true); } catch (Error error) { - warning ("Couldn't load icon set from %s", resource_path); + warning ("Couldn't load icon set from %s: %s", resource_path, error.message); } if (icon != null) { diff --git a/plugins/shotwell-publishing/PhotosPublisher.vala b/plugins/shotwell-publishing/PhotosPublisher.vala new file mode 100644 index 0000000..d878158 --- /dev/null +++ b/plugins/shotwell-publishing/PhotosPublisher.vala @@ -0,0 +1,551 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2019 Jens Georg + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +namespace Publishing.GooglePhotos { +internal const string DEFAULT_ALBUM_NAME = N_("Shotwell Connect"); + +internal class Album { + public string name; + public string id; + + public Album(string name, string id) { + this.name = name; + this.id = id; + } +} + +internal class PublishingParameters { + public const int ORIGINAL_SIZE = -1; + + private string? target_album_name; + private string? target_album_id; + private bool album_public; + private bool strip_metadata; + private int major_axis_size_pixels; + private int major_axis_size_selection_id; + private string user_name; + private Album[] albums; + private Spit.Publishing.Publisher.MediaType media_type; + + public PublishingParameters() { + this.user_name = "[unknown]"; + this.target_album_name = null; + this.target_album_id = null; + this.major_axis_size_selection_id = 0; + this.major_axis_size_pixels = ORIGINAL_SIZE; + this.album_public = false; + this.albums = null; + this.strip_metadata = false; + this.media_type = Spit.Publishing.Publisher.MediaType.PHOTO; + } + + public string get_target_album_name() { + return target_album_name; + } + + public void set_target_album_name(string target_album_name) { + this.target_album_name = target_album_name; + } + + public void set_target_album_entry_id(string target_album_id) { + this.target_album_id = target_album_id; + } + + public string get_target_album_entry_id() { + return this.target_album_id; + } + + public string get_user_name() { + return user_name; + } + + public void set_user_name(string user_name) { + this.user_name = user_name; + } + + public Album[] get_albums() { + return albums; + } + + public void set_albums(Album[] albums) { + this.albums = albums; + } + + + public void set_major_axis_size_pixels(int pixels) { + this.major_axis_size_pixels = pixels; + } + + public int get_major_axis_size_pixels() { + return major_axis_size_pixels; + } + + public void set_major_axis_size_selection_id(int selection_id) { + this.major_axis_size_selection_id = selection_id; + } + + public int get_major_axis_size_selection_id() { + return major_axis_size_selection_id; + } + + public void set_strip_metadata(bool strip_metadata) { + this.strip_metadata = strip_metadata; + } + + public bool get_strip_metadata() { + return strip_metadata; + } + + public void set_media_type(Spit.Publishing.Publisher.MediaType media_type) { + this.media_type = media_type; + } + + public Spit.Publishing.Publisher.MediaType get_media_type() { + return media_type; + } +} + +private class MediaCreationTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate"; + private string[] upload_tokens; + private string[] titles; + private string album_id; + + public MediaCreationTransaction(Publishing.RESTSupport.GoogleSession session, + string[] upload_tokens, + string[] titles, + string album_id) { + base(session, ENDPOINT_URL, Publishing.RESTSupport.HttpMethod.POST); + assert(upload_tokens.length == titles.length); + this.upload_tokens = upload_tokens; + this.album_id = album_id; + this.titles = titles; + } + + public override void execute () throws Spit.Publishing.PublishingError { + var builder = new Json.Builder(); + builder.begin_object(); + builder.set_member_name("albumId"); + builder.add_string_value(this.album_id); + builder.set_member_name("newMediaItems"); + builder.begin_array(); + for (var i = 0; i < this.upload_tokens.length; i++) { + builder.begin_object(); + builder.set_member_name("description"); + builder.add_string_value(this.titles[i]); + builder.set_member_name("simpleMediaItem"); + builder.begin_object(); + builder.set_member_name("uploadToken"); + builder.add_string_value(this.upload_tokens[i]); + builder.end_object(); + builder.end_object(); + } + builder.end_array(); + builder.end_object(); + set_custom_payload(Json.to_string (builder.get_root (), false), "application/json"); + + base.execute(); + } +} + +private class AlbumCreationTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/albums"; + private string title; + + public AlbumCreationTransaction(Publishing.RESTSupport.GoogleSession session, + string title) { + base(session, ENDPOINT_URL, Publishing.RESTSupport.HttpMethod.POST); + this.title = title; + } + + public override void execute () throws Spit.Publishing.PublishingError { + var builder = new Json.Builder(); + builder.begin_object(); + builder.set_member_name("album"); + builder.begin_object(); + builder.set_member_name("title"); + builder.add_string_value(this.title); + builder.end_object(); + builder.end_object(); + set_custom_payload(Json.to_string (builder.get_root (), false), "application/json"); + + base.execute(); + } +} + +private class AlbumDirectoryTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/albums"; + private Album[] albums = new Album[0]; + + public AlbumDirectoryTransaction(Publishing.RESTSupport.GoogleSession session) { + base(session, ENDPOINT_URL, Publishing.RESTSupport.HttpMethod.GET); + this.completed.connect(on_internal_continue_pagination); + } + + public Album[] get_albums() { + return this.albums; + } + + private void on_internal_continue_pagination() { + try { + debug(this.get_response()); + var json = Json.from_string (this.get_response()); + var object = json.get_object (); + if (!object.has_member ("albums")) { + return; + } + + var pagination_token_node = object.get_member ("nextPageToken"); + var response_albums = object.get_member ("albums").get_array(); + response_albums.foreach_element( (a, b, element) => { + var album = element.get_object(); + var is_writable = album.get_member("isWriteable"); + if (is_writable != null && is_writable.get_boolean()) + albums += new Album(album.get_string_member("title"), album.get_string_member("id")); + }); + + if (pagination_token_node != null) { + this.set_argument ("pageToken", pagination_token_node.get_string ()); + Signal.stop_emission_by_name (this, "completed"); + Idle.add(() => { + try { + this.execute(); + } catch (Spit.Publishing.PublishingError error) { + this.network_error(error); + } + + return false; + }); + } + } catch (Error error) { + critical ("Got error %s while trying to parse response, delegating", error.message); + this.network_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message)); + } + } +} + +public class Publisher : Publishing.RESTSupport.GooglePublisher { + private Spit.Publishing.Authenticator authenticator; + private bool running = false; + private PublishingParameters publishing_parameters; + private Spit.Publishing.ProgressCallback progress_reporter; + + public Publisher(Spit.Publishing.Service service, + Spit.Publishing.PluginHost host) { + base(service, host, "https://www.googleapis.com/auth/photoslibrary"); + + this.publishing_parameters = new PublishingParameters(); + load_parameters_from_configuration_system(publishing_parameters); + + var media_type = Spit.Publishing.Publisher.MediaType.NONE; + foreach(Spit.Publishing.Publishable p in host.get_publishables()) + media_type |= p.get_media_type(); + + publishing_parameters.set_media_type(media_type); + } + + private void load_parameters_from_configuration_system(PublishingParameters parameters) { + parameters.set_major_axis_size_selection_id(get_host().get_config_int("default-size", 0)); + parameters.set_strip_metadata(get_host().get_config_bool("strip-metadata", false)); + parameters.set_target_album_name(get_host().get_config_string("last-album", null)); + } + + private void save_parameters_to_configuration_system(PublishingParameters parameters) { + get_host().set_config_int("default-size", parameters.get_major_axis_size_selection_id()); + get_host().set_config_bool("strip_metadata", parameters.get_strip_metadata()); + get_host().set_config_string("last-album", parameters.get_target_album_name()); + } + + protected override void on_login_flow_complete() { + debug("EVENT: OAuth login flow complete."); + this.publishing_parameters.set_user_name (this.authenticator.get_authentication_parameter()["UserName"].get_string()); + + get_host().install_account_fetch_wait_pane(); + get_host().set_service_locked(true); + + AlbumDirectoryTransaction txn = new AlbumDirectoryTransaction(get_session()); + txn.completed.connect(on_initial_album_fetch_complete); + txn.network_error.connect(on_initial_album_fetch_error); + + try { + txn.execute(); + } catch (Spit.Publishing.PublishingError error) { + on_initial_album_fetch_error(txn, error); + } + } + + private void on_initial_album_fetch_complete(Publishing.RESTSupport.Transaction txn) { + txn.completed.disconnect(on_initial_album_fetch_complete); + txn.network_error.disconnect(on_initial_album_fetch_error); + + if (!is_running()) + return; + + debug("EVENT: finished fetching album information."); + + display_account_information((AlbumDirectoryTransaction)txn); + } + + private void on_initial_album_fetch_error(Publishing.RESTSupport.Transaction txn, + Spit.Publishing.PublishingError error) { + txn.completed.disconnect(on_initial_album_fetch_complete); + txn.network_error.disconnect(on_initial_album_fetch_error); + + if (!is_running()) + return; + + debug("EVENT: fetching album information failed; response = '%s'.", + txn.get_response()); + + if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { + do_logout(); + } else { + // If we get any other kind of error, we can't recover, so just post it to the user + get_host().post_error(error); + } + } + + private void display_account_information(AlbumDirectoryTransaction txn) { + debug("ACTION: parsing album information"); + this.publishing_parameters.set_albums(txn.get_albums()); + + show_publishing_options_pane(); + } + + private void show_publishing_options_pane() { + debug("ACTION: showing publishing options pane."); + + var opts_pane = new PublishingOptionsPane(this.publishing_parameters, this.authenticator.can_logout()); + opts_pane.publish.connect(on_publishing_options_publish); + opts_pane.logout.connect(on_publishing_options_logout); + get_host().install_dialog_pane(opts_pane); + + get_host().set_service_locked(false); + } + + private void on_publishing_options_logout() { + if (!is_running()) + return; + + debug("EVENT: user clicked 'Logout' in the publishing options pane."); + + do_logout(); + } + + private void on_publishing_options_publish() { + if (!is_running()) + return; + + debug("EVENT: user clicked 'Publish' in the publishing options pane."); + + save_parameters_to_configuration_system(publishing_parameters); + + if (publishing_parameters.get_target_album_entry_id () != null) { + do_upload(); + } else { + do_create_album(); + } + } + + private void do_create_album() { + debug("ACTION: Creating album"); + assert(publishing_parameters.get_target_album_entry_id () == null); + + get_host().set_service_locked(true); + + var txn = new AlbumCreationTransaction(get_session(), publishing_parameters.get_target_album_name()); + txn.completed.connect(on_album_create_complete); + txn.network_error.connect(on_album_create_error); + + try { + txn.execute(); + } catch (Spit.Publishing.PublishingError error) { + on_album_create_error(txn, error); + } + } + + private void on_album_create_complete(Publishing.RESTSupport.Transaction txn) { + txn.completed.disconnect(on_album_create_complete); + txn.network_error.disconnect(on_album_create_error); + + if (!is_running()) + return; + + debug("EVENT: finished creating album information: %s", txn.get_response()); + + try { + var node = Json.from_string(txn.get_response()); + var object = node.get_object(); + publishing_parameters.set_target_album_entry_id (object.get_string_member ("id")); + + do_upload(); + } catch (Error error) { + on_album_create_error(txn, new Spit.Publishing.PublishingError.MALFORMED_RESPONSE (error.message)); + } + } + + private void on_album_create_error(Publishing.RESTSupport.Transaction txn, + Spit.Publishing.PublishingError error) { + txn.completed.disconnect(on_initial_album_fetch_complete); + txn.network_error.disconnect(on_initial_album_fetch_error); + + if (!is_running()) + return; + + debug("EVENT: creating album failed; response = '%s'.", + txn.get_response()); + + if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { + do_logout(); + } else { + // If we get any other kind of error, we can't recover, so just post it to the user + get_host().post_error(error); + } + } + + protected override void do_logout() { + debug("ACTION: logging out user."); + get_session().deauthenticate(); + + if (this.authenticator.can_logout()) { + this.authenticator.logout(); + this.authenticator.authenticate(); + } + } + + private void do_upload() { + debug("ACTION: uploading media items to remote server."); + + get_host().set_service_locked(true); + + progress_reporter = get_host().serialize_publishables( + publishing_parameters.get_major_axis_size_pixels(), + publishing_parameters.get_strip_metadata()); + + // Serialization is a long and potentially cancellable operation, so before we use + // the publishables, make sure that the publishing interaction is still running. If it + // isn't the publishing environment may be partially torn down so do a short-circuit + // return + if (!is_running()) + return; + + Spit.Publishing.Publishable[] publishables = get_host().get_publishables(); + Uploader uploader = new Uploader(get_session(), publishables, publishing_parameters); + + uploader.upload_complete.connect(on_upload_complete); + uploader.upload_error.connect(on_upload_error); + + uploader.upload(on_upload_status_updated); + } + + private void on_upload_status_updated(int file_number, double completed_fraction) { + if (!is_running()) + return; + + debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction); + + assert(progress_reporter != null); + + progress_reporter(file_number, completed_fraction); + } + + private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, + int num_published) { + if (!is_running()) + return; + + debug("EVENT: uploader reports upload complete; %d items published.", num_published); + + uploader.upload_complete.disconnect(on_upload_complete); + uploader.upload_error.disconnect(on_upload_error); + + var txn = new MediaCreationTransaction(get_session(), + ((Uploader) uploader).upload_tokens, + ((Uploader) uploader).titles, + publishing_parameters.get_target_album_entry_id()); + + txn.completed.connect(on_media_creation_complete); + txn.network_error.connect(on_media_creation_error); + + try { + txn.execute(); + } catch (Spit.Publishing.PublishingError error) { + on_media_creation_error(txn, error); + } + } + + private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, + Spit.Publishing.PublishingError err) { + if (!is_running()) + return; + + debug("EVENT: uploader reports upload error = '%s'.", err.message); + + uploader.upload_complete.disconnect(on_upload_complete); + uploader.upload_error.disconnect(on_upload_error); + + get_host().post_error(err); + } + + private void on_media_creation_complete(Publishing.RESTSupport.Transaction txn) { + txn.completed.disconnect(on_media_creation_complete); + txn.network_error.disconnect(on_media_creation_error); + + if (!is_running()) + return; + + debug("EVENT: Media creation reports success."); + + get_host().set_service_locked(false); + get_host().install_success_pane(); + } + + private void on_media_creation_error(Publishing.RESTSupport.Transaction txn, + Spit.Publishing.PublishingError err) { + txn.completed.disconnect(on_media_creation_complete); + txn.network_error.disconnect(on_media_creation_error); + + if (!is_running()) + return; + + debug("EVENT: Media creation reports error: %s", err.message); + + get_host().post_error(err); + } + + public override bool is_running() { + return running; + } + + public override void start() { + debug("GooglePhotos.Publisher: start() invoked."); + + if (is_running()) + return; + + running = true; + + this.authenticator.authenticate(); + } + + public override void stop() { + debug("GooglePhotos.Publisher: stop() invoked."); + + get_session().stop_transactions(); + + running = false; + } + + protected override Spit.Publishing.Authenticator get_authenticator() { + if (this.authenticator == null) { + this.authenticator = Publishing.Authenticator.Factory.get_instance().create("google-photos", get_host()); + } + + return this.authenticator; + } +} +} // namespace Publishing.GooglePhotos diff --git a/plugins/shotwell-publishing/PhotosPublishingPane.vala b/plugins/shotwell-publishing/PhotosPublishingPane.vala new file mode 100644 index 0000000..d1b00d6 --- /dev/null +++ b/plugins/shotwell-publishing/PhotosPublishingPane.vala @@ -0,0 +1,150 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2019 Jens Georg + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +namespace Publishing.GooglePhotos { +[GtkTemplate (ui = "/org/gnome/Shotwell/Publishing/google_photos_publishing_options_pane.ui")] +internal class PublishingOptionsPane : Gtk.Box, Spit.Publishing.DialogPane { + private struct SizeDescription { + public string name; + public int major_axis_pixels; + } + + private const SizeDescription size_descriptions[] = { + { N_("Small (640 × 480 pixels)"), 640}, + { N_("Medium (1024 × 768 pixels)"), 1024 }, + { N_("Recommended (1600 × 1200 pixels)"), 1600}, + { N_("Google+ (2048 × 1536 pixels)"), 2048}, + { N_("Original Size"), PublishingParameters.ORIGINAL_SIZE } + }; + + [GtkChild] + private Gtk.Button logout_button; + [GtkChild] + private Gtk.Button publish_button; + [GtkChild] + private Gtk.RadioButton existing_album_radio; + [GtkChild] + private Gtk.ComboBoxText existing_albums_combo; + [GtkChild] + private Gtk.ComboBoxText size_combo; + [GtkChild] + private Gtk.Label publish_to_label; + [GtkChild] + private Gtk.Label login_identity_label; + [GtkChild] + private Gtk.CheckButton strip_metadata_check; + [GtkChild] + private Gtk.RadioButton new_album_radio; + [GtkChild] + private Gtk.Entry new_album_entry; + + public signal void publish(); + public signal void logout(); + + private PublishingParameters parameters; + + public PublishingOptionsPane(PublishingParameters parameters, bool can_logout) { + Object(); + this.parameters = parameters; + + if (!can_logout) { + logout_button.parent.remove(logout_button); + } + + // populate any widgets whose contents are programmatically-generated. + login_identity_label.set_label(_("You are logged into Google Photos as %s.").printf( + parameters.get_user_name())); + strip_metadata_check.set_active(parameters.get_strip_metadata()); + + if((parameters.get_media_type() & Spit.Publishing.Publisher.MediaType.PHOTO) == 0) { + publish_to_label.set_label(_("Videos will appear in:")); + size_combo.set_visible(false); + size_combo.set_sensitive(false); + } + else { + publish_to_label.set_label(_("Photos will appear in:")); + foreach(SizeDescription desc in size_descriptions) { + size_combo.append_text(desc.name); + } + size_combo.set_visible(true); + size_combo.set_sensitive(true); + size_combo.set_active(parameters.get_major_axis_size_selection_id()); + } + + existing_album_radio.bind_property("active", existing_albums_combo, "sensitive", GLib.BindingFlags.SYNC_CREATE); + new_album_radio.bind_property("active", new_album_entry, "sensitive", GLib.BindingFlags.SYNC_CREATE); + + publish_button.clicked.connect (on_publish_clicked); + logout_button.clicked.connect (on_logout_clicked); + } + + // DialogPane interface + public Gtk.Widget get_widget() { + return this; + } + + public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { + return Spit.Publishing.DialogPane.GeometryOptions.NONE; + } + + public void on_pane_installed() { + int default_album_id = -1; + string last_album = parameters.get_target_album_name(); + + var albums = parameters.get_albums(); + + for (int i = 0; i < albums.length; i++) { + existing_albums_combo.append_text(albums[i].name); + // Activate last known album id. If none was chosen, either use the old default (Shotwell connect) + // or the new "Default album" album for Google Photos + if (albums[i].name == last_album || + ((albums[i].name == DEFAULT_ALBUM_NAME || albums[i].name == _("Default album")) && default_album_id == -1)) + default_album_id = i; + } + + if (default_album_id >= 0) { + existing_albums_combo.set_active(default_album_id); + existing_album_radio.set_active(true); + } + + if (albums.length == 0) { + existing_album_radio.set_sensitive(false); + new_album_radio.set_active(true); + } + } + + public void on_pane_uninstalled() { + } + + private void on_publish_clicked() { + // size_combo won't have been set to anything useful if this is the first time we've + // published to Google Photos, and/or we've only published video before, so it may be negative, + // indicating nothing was selected. Clamp it to a valid value... + int size_combo_last_active = (size_combo.get_active() >= 0) ? size_combo.get_active() : 0; + + parameters.set_major_axis_size_selection_id(size_combo_last_active); + parameters.set_major_axis_size_pixels( + size_descriptions[size_combo_last_active].major_axis_pixels); + parameters.set_strip_metadata(strip_metadata_check.get_active()); + + Album[] albums = parameters.get_albums(); + + if (new_album_radio.get_active()) { + parameters.set_target_album_name(new_album_entry.get_text()); + } else { + parameters.set_target_album_name(albums[existing_albums_combo.get_active()].name); + parameters.set_target_album_entry_id(albums[existing_albums_combo.get_active()].id); + } + + publish(); + } + + private void on_logout_clicked() { + logout(); + } + } +} diff --git a/plugins/shotwell-publishing/PhotosService.vala b/plugins/shotwell-publishing/PhotosService.vala new file mode 100644 index 0000000..8e328f4 --- /dev/null +++ b/plugins/shotwell-publishing/PhotosService.vala @@ -0,0 +1,58 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2019 Jens Georg + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +namespace Publishing.GooglePhotos { + +public class Service : Object, Spit.Pluggable, Spit.Publishing.Service { + private const string ICON_FILENAME = "google-photos.svg"; + + private static Gdk.Pixbuf[] icon_pixbuf_set = null; + + static construct { + icon_pixbuf_set = Resources.load_from_resource(Resources.RESOURCE_PATH + "/" + ICON_FILENAME); + } + + public Service(GLib.File resource_directory) {} + + public int get_pluggable_interface(int min_host_interface, int max_host_interface) { + return Spit.negotiate_interfaces(min_host_interface, max_host_interface, + Spit.Publishing.CURRENT_INTERFACE); + } + + public unowned string get_id() { + return "org.gnome.shotwell.publishing.google-photos"; + } + + public unowned string get_pluggable_name() { + return "Google Photos"; + } + + public void get_info(ref Spit.PluggableInfo info) { + info.authors = "Jens Georg"; + info.copyright = _("Copyright 2019 Jens Georg "); + 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 Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { + return new Publishing.GooglePhotos.Publisher(this, host); + } + + public Spit.Publishing.Publisher.MediaType get_supported_media() { + return (Spit.Publishing.Publisher.MediaType.PHOTO | + Spit.Publishing.Publisher.MediaType.VIDEO); + } + + public void activation(bool enabled) { + } +} +} // namespace GooglePhotos diff --git a/plugins/shotwell-publishing/PhotosUploader.vala b/plugins/shotwell-publishing/PhotosUploader.vala new file mode 100644 index 0000000..83137ee --- /dev/null +++ b/plugins/shotwell-publishing/PhotosUploader.vala @@ -0,0 +1,104 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2019 Jens Georg + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +namespace Publishing.GooglePhotos { + +internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + private PublishingParameters parameters; + private Publishing.RESTSupport.GoogleSession session; + private Spit.Publishing.Publishable publishable; + private MappedFile mapped_file; + + public UploadTransaction(Publishing.RESTSupport.GoogleSession session, + PublishingParameters parameters, Spit.Publishing.Publishable publishable) { + base(session, "https://photoslibrary.googleapis.com/v1/uploads", + Publishing.RESTSupport.HttpMethod.POST); + assert(session.is_authenticated()); + + this.session = session; + this.parameters = parameters; + this.publishable = publishable; + } + + public Spit.Publishing.Publishable get_publishable() { + return this.publishable; + } + + public override void execute() throws Spit.Publishing.PublishingError { + var basename = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); + + // attempt to map the binary image data from disk into memory + try { + mapped_file = new MappedFile(publishable.get_serialized_file().get_path(), false); + } catch (FileError e) { + string msg = "Google Photos: couldn't read data from %s: %s".printf( + publishable.get_serialized_file().get_path(), e.message); + warning("%s", msg); + + throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(msg); + } + + unowned uint8[] photo_data = (uint8[]) mapped_file.get_contents(); + photo_data.length = (int) mapped_file.get_length(); + + // bind the binary image data read from disk into a Soup.Buffer object so that we + // can attach it to the multipart request, then actaully append the buffer + // to the multipart request. Then, set the MIME type for this part. + Soup.Buffer bindable_data = new Soup.Buffer(Soup.MemoryUse.TEMPORARY, photo_data); + + // create a message that can be sent over the wire whose payload is the multipart container + // that we've been building up + var outbound_message = new Soup.Message ("POST", get_endpoint_url()); + outbound_message.request_headers.append("Authorization", "Bearer " + + session.get_access_token()); + outbound_message.request_headers.append("X-Goog-Upload-File-Name", basename); + outbound_message.request_headers.append("X-Goog-Upload-Protocol", "raw"); + outbound_message.request_headers.set_content_type("application/octet-stream", null); + outbound_message.request_body.append_buffer (bindable_data); + set_message(outbound_message); + + // send the message and get its response + set_is_executed(true); + send(); + } +} + +internal class Uploader : Publishing.RESTSupport.BatchUploader { + private PublishingParameters parameters; + + public string[] upload_tokens = new string[0]; + public string[] titles = new string[0]; + + public Uploader(Publishing.RESTSupport.GoogleSession session, + Spit.Publishing.Publishable[] publishables, PublishingParameters parameters) { + base(session, publishables); + + this.parameters = parameters; + } + + protected override Publishing.RESTSupport.Transaction create_transaction( + Spit.Publishing.Publishable publishable) { + var txn = new UploadTransaction((Publishing.RESTSupport.GoogleSession) get_session(), + parameters, get_current_publishable()); + txn.completed.connect(this.on_transaction_completed); + + return txn; + } + + private void on_transaction_completed (Publishing.RESTSupport.Transaction txn) { + txn.completed.disconnect (on_transaction_completed); + + this.upload_tokens += txn.get_response(); + var title = ((UploadTransaction)txn).get_publishable().get_publishing_name(); + var publishable = ((UploadTransaction)txn).get_publishable(); + if (title == null || title == "") { + title = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); + } + this.titles += title; + } +} +} diff --git a/plugins/shotwell-publishing/PicasaPublishing.vala b/plugins/shotwell-publishing/PicasaPublishing.vala deleted file mode 100644 index d72ce6d..0000000 --- a/plugins/shotwell-publishing/PicasaPublishing.vala +++ /dev/null @@ -1,770 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * 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 PicasaService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "picasa.png"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public PicasaService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = - Resources.load_from_resource(Resources.RESOURCE_PATH + "/" + 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.Publishing.CURRENT_INTERFACE); - } - - public unowned string get_id() { - return "org.yorba.shotwell.publishing.picasa"; - } - - public unowned string get_pluggable_name() { - return "Picasa Web Albums"; - } - - public void get_info(ref Spit.PluggableInfo info) { - info.authors = "Lucas Beeler"; - info.copyright = _("Copyright 2016 Software Freedom Conservancy Inc."); - 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 Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { - return new Publishing.Picasa.PicasaPublisher(this, host); - } - - public Spit.Publishing.Publisher.MediaType get_supported_media() { - return (Spit.Publishing.Publisher.MediaType.PHOTO | - Spit.Publishing.Publisher.MediaType.VIDEO); - } - - public void activation(bool enabled) { - } -} - -namespace Publishing.Picasa { - -internal const string DEFAULT_ALBUM_NAME = _("Shotwell Connect"); - -public class PicasaPublisher : Publishing.RESTSupport.GooglePublisher { - private const string DEFAULT_ALBUM_FEED_URL = "https://picasaweb.google.com/data/feed/api/user/default/albumid/default"; - private bool running; - private Spit.Publishing.ProgressCallback progress_reporter; - private PublishingParameters publishing_parameters; - private Spit.Publishing.Authenticator authenticator; - - public PicasaPublisher(Spit.Publishing.Service service, - Spit.Publishing.PluginHost host) { - base(service, host, "https://picasaweb.google.com/data/"); - - this.publishing_parameters = new PublishingParameters(); - load_parameters_from_configuration_system(publishing_parameters); - - Spit.Publishing.Publisher.MediaType media_type = Spit.Publishing.Publisher.MediaType.NONE; - foreach(Spit.Publishing.Publishable p in host.get_publishables()) - media_type |= p.get_media_type(); - publishing_parameters.set_media_type(media_type); - - this.progress_reporter = null; - } - - private Album[] extract_albums_helper(Xml.Node* document_root) - throws Spit.Publishing.PublishingError { - Album[] result = new Album[0]; - - Xml.Node* doc_node_iter = null; - if (document_root->name == "feed") - doc_node_iter = document_root->children; - else if (document_root->name == "entry") - doc_node_iter = document_root; - else - throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE("response root node " + - "isn't a or "); - - // Add album that will push to the default feed for all the new users - result += new Album(_("Default album"), DEFAULT_ALBUM_FEED_URL); - - for ( ; doc_node_iter != null; doc_node_iter = doc_node_iter->next) { - if (doc_node_iter->name != "entry") - continue; - - string name_val = null; - string url_val = null; - Xml.Node* album_node_iter = doc_node_iter->children; - for ( ; album_node_iter != null; album_node_iter = album_node_iter->next) { - if (album_node_iter->name == "title") { - name_val = album_node_iter->get_content(); - } else if (album_node_iter->name == "id") { - // we only want nodes in the default namespace -- the feed that we get back - // from Google also defines child nodes named in the gphoto and - // media namespaces - if (album_node_iter->ns->prefix != null) - continue; - url_val = album_node_iter->get_content(); - } - } - - // If default album is present in the result list, just skip it because we added it on top anyway - if (url_val == DEFAULT_ALBUM_FEED_URL) { - continue; - } - - result += new Album(name_val, url_val); - } - - return result; - } - - private void load_parameters_from_configuration_system(PublishingParameters parameters) { - parameters.set_major_axis_size_selection_id(get_host().get_config_int("default-size", 0)); - parameters.set_strip_metadata(get_host().get_config_bool("strip-metadata", false)); - parameters.set_target_album_name(get_host().get_config_string("last-album", null)); - } - - private void save_parameters_to_configuration_system(PublishingParameters parameters) { - get_host().set_config_int("default-size", parameters.get_major_axis_size_selection_id()); - get_host().set_config_bool("strip_metadata", parameters.get_strip_metadata()); - get_host().set_config_string("last-album", parameters.get_target_album_name()); - } - - protected override void on_login_flow_complete() { - debug("EVENT: OAuth login flow complete."); - - - publishing_parameters.set_user_name(get_session().get_user_name()); - - do_fetch_account_information(); - } - - private void on_initial_album_fetch_complete(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_initial_album_fetch_complete); - txn.network_error.disconnect(on_initial_album_fetch_error); - - if (!is_running()) - return; - - debug("EVENT: finished fetching account and album information."); - - do_parse_and_display_account_information((AlbumDirectoryTransaction) txn); - } - - private void on_initial_album_fetch_error(Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err) { - bad_txn.completed.disconnect(on_initial_album_fetch_complete); - bad_txn.network_error.disconnect(on_initial_album_fetch_error); - - if (!is_running()) - return; - - debug("EVENT: fetching account and album information failed; response = '%s'.", - bad_txn.get_response()); - - if (bad_txn.get_status_code() == 403 || bad_txn.get_status_code() == 404) { - do_logout(); - } else { - // If we get any other kind of error, we can't recover, so just post it to the user - get_host().post_error(err); - } - } - - private void on_publishing_options_logout() { - if (!is_running()) - return; - - debug("EVENT: user clicked 'Logout' in the publishing options pane."); - - do_logout(); - } - - private void on_publishing_options_publish() { - if (!is_running()) - return; - - debug("EVENT: user clicked 'Publish' in the publishing options pane."); - - save_parameters_to_configuration_system(publishing_parameters); - - do_upload(); - } - - private void on_upload_status_updated(int file_number, double completed_fraction) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction); - - assert(progress_reporter != null); - - progress_reporter(file_number, completed_fraction); - } - - private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, - int num_published) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload complete; %d items published.", num_published); - - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - do_show_success_pane(); - } - - private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, - Spit.Publishing.PublishingError err) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload error = '%s'.", err.message); - - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - get_host().post_error(err); - } - - private void do_fetch_account_information() { - debug("ACTION: fetching account and album information."); - - get_host().install_account_fetch_wait_pane(); - get_host().set_service_locked(true); - - AlbumDirectoryTransaction directory_trans = - new AlbumDirectoryTransaction(get_session()); - directory_trans.network_error.connect(on_initial_album_fetch_error); - directory_trans.completed.connect(on_initial_album_fetch_complete); - - try { - directory_trans.execute(); - } catch (Spit.Publishing.PublishingError err) { - // don't post the error here -- some errors are recoverable so let's let the error - // handler function sort out whether the error is recoverable or not. If the error - // isn't recoverable, the error handler will post the error to the host - on_initial_album_fetch_error(directory_trans, err); - } - } - - private void do_parse_and_display_account_information(AlbumDirectoryTransaction transaction) { - debug("ACTION: parsing account and album information from server response XML"); - - Publishing.RESTSupport.XmlDocument response_doc; - try { - response_doc = Publishing.RESTSupport.XmlDocument.parse_string( - transaction.get_response(), AlbumDirectoryTransaction.validate_xml); - } catch (Spit.Publishing.PublishingError err) { - get_host().post_error(err); - return; - } - - try { - publishing_parameters.set_albums(extract_albums_helper(response_doc.get_root_node())); - } catch (Spit.Publishing.PublishingError err) { - get_host().post_error(err); - return; - } - - do_show_publishing_options_pane(); - } - - private void do_show_publishing_options_pane() { - debug("ACTION: showing publishing options pane."); - Gtk.Builder builder = new Gtk.Builder(); - - try { - builder.add_from_resource(Resources.RESOURCE_PATH + "/" + "picasa_publishing_options_pane.ui"); - } catch (Error e) { - warning("Could not parse UI file! Error: %s.", e.message); - get_host().post_error( - new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( - _("A file required for publishing is unavailable. Publishing to Picasa can’t continue."))); - return; - } - - var opts_pane = new PublishingOptionsPane(builder, publishing_parameters, this.authenticator.can_logout()); - opts_pane.publish.connect(on_publishing_options_publish); - opts_pane.logout.connect(on_publishing_options_logout); - get_host().install_dialog_pane(opts_pane); - - get_host().set_service_locked(false); - } - - private void do_upload() { - debug("ACTION: uploading media items to remote server."); - - get_host().set_service_locked(true); - - progress_reporter = get_host().serialize_publishables( - publishing_parameters.get_major_axis_size_pixels(), - publishing_parameters.get_strip_metadata()); - - // Serialization is a long and potentially cancellable operation, so before we use - // the publishables, make sure that the publishing interaction is still running. If it - // isn't the publishing environment may be partially torn down so do a short-circuit - // return - if (!is_running()) - return; - - Spit.Publishing.Publishable[] publishables = get_host().get_publishables(); - Uploader uploader = new Uploader(get_session(), publishables, publishing_parameters); - - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); - - uploader.upload(on_upload_status_updated); - } - - private void do_show_success_pane() { - debug("ACTION: showing success pane."); - - get_host().set_service_locked(false); - get_host().install_success_pane(); - } - - protected override void do_logout() { - debug("ACTION: logging out user."); - get_session().deauthenticate(); - - if (this.authenticator.can_logout()) { - this.authenticator.logout(); - this.authenticator.authenticate(); - } - } - - public override bool is_running() { - return running; - } - - public override void start() { - debug("PicasaPublisher: start( ) invoked."); - - if (is_running()) - return; - - running = true; - - this.authenticator.authenticate(); - } - - public override void stop() { - debug("PicasaPublisher: stop( ) invoked."); - - get_session().stop_transactions(); - - running = false; - } - - protected override Spit.Publishing.Authenticator get_authenticator() { - if (this.authenticator == null) { - this.authenticator = Publishing.Authenticator.Factory.get_instance().create("picasa", get_host()); - } - - return this.authenticator; - } -} - -internal class Album { - public string name; - public string url; - - public Album(string name, string url) { - this.name = name; - this.url = url; - } -} - -internal class AlbumDirectoryTransaction : - Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { - private const string ENDPOINT_URL = "https://picasaweb.google.com/data/feed/api/user/" + - "default"; - - public AlbumDirectoryTransaction(Publishing.RESTSupport.GoogleSession session) { - base(session, ENDPOINT_URL, Publishing.RESTSupport.HttpMethod.GET); - } - - public static string? validate_xml(Publishing.RESTSupport.XmlDocument doc) { - Xml.Node* document_root = doc.get_root_node(); - if ((document_root->name == "feed") || (document_root->name == "entry")) - return null; - else - return "response root node isn't a or "; - } -} - -internal class UploadTransaction : - Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { - private PublishingParameters parameters; - private const string METADATA_TEMPLATE = " %s %s %s "; - private Publishing.RESTSupport.GoogleSession session; - private string mime_type; - private Spit.Publishing.Publishable publishable; - private MappedFile mapped_file; - - public UploadTransaction(Publishing.RESTSupport.GoogleSession session, - PublishingParameters parameters, Spit.Publishing.Publishable publishable) { - base(session, parameters.get_target_album_feed_url(), - Publishing.RESTSupport.HttpMethod.POST); - assert(session.is_authenticated()); - this.session = session; - this.parameters = parameters; - this.publishable = publishable; - if (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) { - try { - var info = this.publishable.get_serialized_file().query_info(FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE); - this.mime_type = ContentType.get_mime_type(info.get_content_type()); - } catch (Error err) { - this.mime_type = "video/mpeg"; - } - } else { - this.mime_type = "image/jpeg"; - } - } - - public override void execute() throws Spit.Publishing.PublishingError { - // create the multipart request container - Soup.Multipart message_parts = new Soup.Multipart("multipart/related"); - - string summary = ""; - if (publishable.get_publishing_name() != "") { - summary = "%s".printf( - Publishing.RESTSupport.decimal_entity_encode(publishable.get_publishing_name())); - } - - string[] keywords = publishable.get_publishing_keywords(); - string keywords_string = ""; - if (keywords.length > 0) { - for (int i = 0; i < keywords.length; i++) { - string[] tmp; - - if (keywords[i].has_prefix("/")) - tmp = keywords[i].substring(1).split("/"); - else - tmp = keywords[i].split("/"); - - if (keywords_string.length > 0) - keywords_string = string.join(", ", keywords_string, string.joinv(", ", tmp)); - else - keywords_string = string.joinv(", ", tmp); - } - - keywords_string = Publishing.RESTSupport.decimal_entity_encode(keywords_string); - keywords_string = "%s".printf(keywords_string); - } - - string metadata = METADATA_TEMPLATE.printf(Publishing.RESTSupport.decimal_entity_encode( - publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME)), - summary, keywords_string); - Soup.Buffer metadata_buffer = new Soup.Buffer(Soup.MemoryUse.COPY, metadata.data); - message_parts.append_form_file("", "", "application/atom+xml", metadata_buffer); - - // attempt to map the binary image data from disk into memory - try { - mapped_file = new MappedFile(publishable.get_serialized_file().get_path(), false); - } catch (FileError e) { - string msg = "Picasa: couldn't read data from %s: %s".printf( - publishable.get_serialized_file().get_path(), e.message); - warning("%s", msg); - - throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(msg); - } - unowned uint8[] photo_data = (uint8[]) mapped_file.get_contents(); - photo_data.length = (int) mapped_file.get_length(); - - // bind the binary image data read from disk into a Soup.Buffer object so that we - // can attach it to the multipart request, then actaully append the buffer - // to the multipart request. Then, set the MIME type for this part. - Soup.Buffer bindable_data = new Soup.Buffer(Soup.MemoryUse.TEMPORARY, photo_data); - - message_parts.append_form_file("", publishable.get_serialized_file().get_path(), mime_type, - bindable_data); - // create a message that can be sent over the wire whose payload is the multipart container - // that we've been building up - Soup.Message outbound_message = - Soup.Form.request_new_from_multipart(get_endpoint_url(), message_parts); - outbound_message.request_headers.append("Authorization", "Bearer " + - session.get_access_token()); - set_message(outbound_message); - - // send the message and get its response - set_is_executed(true); - send(); - } -} - -internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { - private class SizeDescription { - public string name; - public int major_axis_pixels; - - public SizeDescription(string name, int major_axis_pixels) { - this.name = name; - this.major_axis_pixels = major_axis_pixels; - } - } - - private const string DEFAULT_SIZE_CONFIG_KEY = "default_size"; - private const string LAST_ALBUM_CONFIG_KEY = "last_album"; - - private Gtk.Builder builder = null; - private Gtk.Box pane_widget = null; - private Gtk.Label login_identity_label = null; - private Gtk.Label publish_to_label = null; - private Gtk.ComboBoxText existing_albums_combo = null; - private Gtk.CheckButton public_check = null; - private Gtk.ComboBoxText size_combo = null; - private Gtk.CheckButton strip_metadata_check = null; - private Gtk.Button publish_button = null; - private Gtk.Button logout_button = null; - private SizeDescription[] size_descriptions; - private PublishingParameters parameters; - - public signal void publish(); - public signal void logout(); - - public PublishingOptionsPane(Gtk.Builder builder, PublishingParameters parameters, bool can_logout) { - size_descriptions = create_size_descriptions(); - - this.builder = builder; - assert(builder != null); - assert(builder.get_objects().length() > 0); - - this.parameters = parameters; - - // pull in all widgets from builder. - pane_widget = (Gtk.Box) builder.get_object("picasa_pane_widget"); - login_identity_label = (Gtk.Label) builder.get_object("login_identity_label"); - publish_to_label = (Gtk.Label) builder.get_object("publish_to_label"); - existing_albums_combo = (Gtk.ComboBoxText) builder.get_object("existing_albums_combo"); - public_check = (Gtk.CheckButton) builder.get_object("public_check"); - size_combo = (Gtk.ComboBoxText) builder.get_object("size_combo"); - strip_metadata_check = (Gtk.CheckButton) this.builder.get_object("strip_metadata_check"); - publish_button = (Gtk.Button) builder.get_object("publish_button"); - logout_button = (Gtk.Button) builder.get_object("logout_button"); - - if (!can_logout) { - logout_button.parent.remove(logout_button); - } - - // populate any widgets whose contents are programmatically-generated. - login_identity_label.set_label(_("You are logged into Picasa Web Albums as %s.").printf( - parameters.get_user_name())); - strip_metadata_check.set_active(parameters.get_strip_metadata()); - - - if((parameters.get_media_type() & Spit.Publishing.Publisher.MediaType.PHOTO) == 0) { - publish_to_label.set_label(_("Videos will appear in:")); - size_combo.set_visible(false); - size_combo.set_sensitive(false); - } - else { - publish_to_label.set_label(_("Photos will appear in:")); - foreach(SizeDescription desc in size_descriptions) { - size_combo.append_text(desc.name); - } - size_combo.set_visible(true); - size_combo.set_sensitive(true); - size_combo.set_active(parameters.get_major_axis_size_selection_id()); - } - - // connect all signals. - logout_button.clicked.connect(on_logout_clicked); - publish_button.clicked.connect(on_publish_clicked); - } - - private void on_publish_clicked() { - // size_combo won't have been set to anything useful if this is the first time we've - // published to Picasa, and/or we've only published video before, so it may be negative, - // indicating nothing was selected. Clamp it to a valid value... - int size_combo_last_active = (size_combo.get_active() >= 0) ? size_combo.get_active() : 0; - - parameters.set_major_axis_size_selection_id(size_combo_last_active); - parameters.set_major_axis_size_pixels( - size_descriptions[size_combo_last_active].major_axis_pixels); - parameters.set_strip_metadata(strip_metadata_check.get_active()); - - Album[] albums = parameters.get_albums(); - - parameters.set_target_album_name(albums[existing_albums_combo.get_active()].name); - parameters.set_target_album_entry_url(albums[existing_albums_combo.get_active()].url); - publish(); - } - - private void on_logout_clicked() { - logout(); - } - - private SizeDescription[] create_size_descriptions() { - SizeDescription[] result = new SizeDescription[0]; - - result += new SizeDescription(_("Small (640 × 480 pixels)"), 640); - result += new SizeDescription(_("Medium (1024 × 768 pixels)"), 1024); - result += new SizeDescription(_("Recommended (1600 × 1200 pixels)"), 1600); - result += new SizeDescription(_("Google+ (2048 × 1536 pixels)"), 2048); - result += new SizeDescription(_("Original Size"), PublishingParameters.ORIGINAL_SIZE); - - return result; - } - - public void installed() { - int default_album_id = -1; - string last_album = parameters.get_target_album_name(); - - Album[] albums = parameters.get_albums(); - - for (int i = 0; i < albums.length; i++) { - existing_albums_combo.append_text(albums[i].name); - // Activate last known album id. If none was chosen, either use the old default (Shotwell connect) - // or the new "Default album" album for Google Photos - if (albums[i].name == last_album || - ((albums[i].name == DEFAULT_ALBUM_NAME || albums[i].name == _("Default album")) && default_album_id == -1)) - default_album_id = i; - } - - if (default_album_id >= 0) { - existing_albums_combo.set_active(default_album_id); - } - } - - public Gtk.Widget get_widget() { - return pane_widget; - } - - public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { - return Spit.Publishing.DialogPane.GeometryOptions.NONE; - } - - public void on_pane_installed() { - installed(); - } - - public void on_pane_uninstalled() { - } -} - -internal class PublishingParameters { - public const int ORIGINAL_SIZE = -1; - - private string? target_album_name; - private string? target_album_url; - private bool album_public; - private bool strip_metadata; - private int major_axis_size_pixels; - private int major_axis_size_selection_id; - private string user_name; - private Album[] albums; - private Spit.Publishing.Publisher.MediaType media_type; - - public PublishingParameters() { - this.user_name = "[unknown]"; - this.target_album_name = null; - this.major_axis_size_selection_id = 0; - this.major_axis_size_pixels = ORIGINAL_SIZE; - this.target_album_url = null; - this.album_public = false; - this.albums = null; - this.strip_metadata = false; - this.media_type = Spit.Publishing.Publisher.MediaType.PHOTO; - } - - public string get_target_album_name() { - return target_album_name; - } - - public void set_target_album_name(string target_album_name) { - this.target_album_name = target_album_name; - } - - public void set_target_album_entry_url(string target_album_url) { - this.target_album_url = target_album_url; - } - - public string get_target_album_entry_url() { - return target_album_url; - } - - public string get_target_album_feed_url() { - string entry_url = get_target_album_entry_url(); - string feed_url = entry_url.replace("entry", "feed"); - - return feed_url; - } - - public string get_user_name() { - return user_name; - } - - public void set_user_name(string user_name) { - this.user_name = user_name; - } - - public Album[] get_albums() { - return albums; - } - - public void set_albums(Album[] albums) { - this.albums = albums; - } - - public void set_major_axis_size_pixels(int pixels) { - this.major_axis_size_pixels = pixels; - } - - public int get_major_axis_size_pixels() { - return major_axis_size_pixels; - } - - public void set_major_axis_size_selection_id(int selection_id) { - this.major_axis_size_selection_id = selection_id; - } - - public int get_major_axis_size_selection_id() { - return major_axis_size_selection_id; - } - - public void set_strip_metadata(bool strip_metadata) { - this.strip_metadata = strip_metadata; - } - - public bool get_strip_metadata() { - return strip_metadata; - } - - public void set_media_type(Spit.Publishing.Publisher.MediaType media_type) { - this.media_type = media_type; - } - - public Spit.Publishing.Publisher.MediaType get_media_type() { - return media_type; - } -} - -internal class Uploader : Publishing.RESTSupport.BatchUploader { - private PublishingParameters parameters; - - public Uploader(Publishing.RESTSupport.GoogleSession session, - Spit.Publishing.Publishable[] publishables, PublishingParameters parameters) { - base(session, publishables); - - this.parameters = parameters; - } - - protected override Publishing.RESTSupport.Transaction create_transaction( - Spit.Publishing.Publishable publishable) { - return new UploadTransaction((Publishing.RESTSupport.GoogleSession) get_session(), - parameters, get_current_publishable()); - } -} - -} - diff --git a/plugins/shotwell-publishing/flickr.png b/plugins/shotwell-publishing/flickr.png deleted file mode 100644 index b6cae3e..0000000 Binary files a/plugins/shotwell-publishing/flickr.png and /dev/null differ diff --git a/plugins/shotwell-publishing/google-photos.svg b/plugins/shotwell-publishing/google-photos.svg new file mode 100644 index 0000000..498de5a --- /dev/null +++ b/plugins/shotwell-publishing/google-photos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/shotwell-publishing/google_photos_publishing_options_pane.ui b/plugins/shotwell-publishing/google_photos_publishing_options_pane.ui new file mode 100644 index 0000000..8685b96 --- /dev/null +++ b/plugins/shotwell-publishing/google_photos_publishing_options_pane.ui @@ -0,0 +1,221 @@ + + + + + + + + + + + + diff --git a/plugins/shotwell-publishing/meson.build b/plugins/shotwell-publishing/meson.build index 45058b8..eeac177 100644 --- a/plugins/shotwell-publishing/meson.build +++ b/plugins/shotwell-publishing/meson.build @@ -1,11 +1,15 @@ shotwell_publishing_sources = [ 'shotwell-publishing.vala', 'FacebookPublishing.vala', - 'PicasaPublishing.vala', 'FlickrPublishing.vala', 'TumblrPublishing.vala', 'YouTubePublishing.vala', - 'PiwigoPublishing.vala'] + 'PiwigoPublishing.vala', + 'PhotosPublisher.vala', + 'PhotosService.vala', + 'PhotosPublishingPane.vala', + 'PhotosUploader.vala' +] shotwell_publishing_resources = gnome.compile_resources('publishing-resource', 'org.gnome.Shotwell.Publishing.gresource.xml', @@ -16,9 +20,6 @@ shared_module('shotwell-publishing', dependencies : [gtk, soup, gexiv2, gee, sw_plugin, json_glib, webkit, sw_plugin_common_dep, xml, gdata, gcr, gcr_ui, authenticator_dep], - vala_args : [ - '--gresources', 'org.gnome.Shotwell.Publishing.gresource.xml' - ], c_args : ['-DPLUGIN_RESOURCE_PATH="/org/gnome/Shotwell/Publishing"', '-DGCR_API_SUBJECT_TO_CHANGE'], install: true, diff --git a/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml b/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml index 5e8ce1c..436ea9e 100644 --- a/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml +++ b/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml @@ -2,14 +2,13 @@ facebook.png - flickr.png - picasa.png piwigo.png youtube.png tumblr.png + google-photos.svg facebook_publishing_options_pane.ui flickr_publishing_options_pane.ui - picasa_publishing_options_pane.ui + google_photos_publishing_options_pane.ui piwigo_authentication_pane.ui piwigo_publishing_options_pane.ui piwigo_ssl_failure_pane.ui diff --git a/plugins/shotwell-publishing/picasa_publishing_options_pane.ui b/plugins/shotwell-publishing/picasa_publishing_options_pane.ui deleted file mode 100644 index 11e79af..0000000 --- a/plugins/shotwell-publishing/picasa_publishing_options_pane.ui +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - True - False - vertical - 1 - - - - - - True - False - 12 - 32 - 'you are logged in as $name' -(populated in the application code) - - - False - True - 4 - 1 - - - - - True - False - 32 - 32 - vertical - - - - - - True - False - 24 - 24 - 8 - True - - - True - False - - - 1 - 0 - - - - - _Remove location, camera, and other identifying information before uploading - True - True - False - start - 16 - True - True - True - - - 0 - 2 - 2 - - - - - True - False - start - $mediatype will appear in -(populated in code) - - - 0 - 0 - - - - - True - False - start - Photo _size preset: - True - size_combo - - - 0 - 1 - - - - - True - False - - - 1 - 1 - - - - - False - True - 4 - 1 - - - - - False - True - 2 - - - - - - - - True - False - 112 - 112 - 48 - 24 - 128 - True - - - _Logout - True - True - True - True - - - False - True - 0 - - - - - _Publish - True - True - True - True - - - False - True - 1 - - - - - False - True - 4 - - - - diff --git a/plugins/shotwell-publishing/shotwell-publishing.vala b/plugins/shotwell-publishing/shotwell-publishing.vala index 3515f5f..52c82ec 100644 --- a/plugins/shotwell-publishing/shotwell-publishing.vala +++ b/plugins/shotwell-publishing/shotwell-publishing.vala @@ -6,7 +6,7 @@ extern const string _VERSION; -// "core services" are: Facebook, Flickr, Picasa Web Albums, Piwigo and YouTube +// "core services" are: Flickr, Google Photos, Piwigo, Tumblr and YouTube private class ShotwellPublishingCoreServices : Object, Spit.Module { private Spit.Pluggable[] pluggables = new Spit.Pluggable[0]; @@ -28,9 +28,9 @@ private class ShotwellPublishingCoreServices : Object, Spit.Module { } #endif -#if HAVE_PICASA - if (authenicators.contains("picasa")) { - pluggables += new PicasaService(resource_directory); +#if HAVE_GOOGLEPHOTOS + if (authenicators.contains("google-photos")) { + pluggables += new Publishing.GooglePhotos.Service(resource_directory); } #endif -- cgit v1.2.3