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 --- 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 +- 12 files changed, 1097 insertions(+), 958 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/shotwell-publishing') 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