diff options
author | Jörg Frings-Fürst <debian@jff.email> | 2019-07-12 16:16:34 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff.email> | 2019-07-12 16:16:34 +0200 |
commit | e905afb102569e93e2f6ece9a9ab515ac1fd4f04 (patch) | |
tree | 56c764f3a3fe3848ef2aa161d143f33cb2eaeb8e /plugins/shotwell-publishing/PhotosPublisher.vala | |
parent | 71137cc5832110d91599d68790402cf196762ed3 (diff) | |
parent | cb774d1baf242adcc03a1df5eed6d100f7e3bd55 (diff) |
Merge branch 'release/debian/0.30.4-1'debian/0.30.4-1
Diffstat (limited to 'plugins/shotwell-publishing/PhotosPublisher.vala')
-rw-r--r-- | plugins/shotwell-publishing/PhotosPublisher.vala | 551 |
1 files changed, 551 insertions, 0 deletions
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 <mail@jensge.org> + * + * 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 |