[shotwell/wip/libsoup3] plugins: Add back Youtube publishing



commit e80358bf0b8d613489e522208373463ba6e160db
Author: Jens Georg <mail jensge org>
Date:   Mon Jul 18 21:02:48 2022 +0200

    plugins: Add back Youtube publishing

 plugins/shotwell-publishing/YouTubePublishing.vala | 143 +++------------------
 plugins/shotwell-publishing/YoutubeUploader.vala   | 133 +++++++++++++++++++
 plugins/shotwell-publishing/meson.build            |   3 +-
 .../shotwell-publishing/shotwell-publishing.vala   |   3 +-
 4 files changed, 156 insertions(+), 126 deletions(-)
---
diff --git a/plugins/shotwell-publishing/YouTubePublishing.vala 
b/plugins/shotwell-publishing/YouTubePublishing.vala
index 91927bd0..4b5908a4 100644
--- a/plugins/shotwell-publishing/YouTubePublishing.vala
+++ b/plugins/shotwell-publishing/YouTubePublishing.vala
@@ -60,10 +60,23 @@ private const string DEVELOPER_KEY =
 private enum PrivacySetting {
     PUBLIC,
     UNLISTED,
-    PRIVATE
+    PRIVATE;
+
+    public string to_string() {
+        switch (this) {
+            case PUBLIC:
+                return "public";
+            case UNLISTED:
+                return "unlisted";
+            case PRIVATE:
+                return "private";
+            default:
+                assert_not_reached();
+        }
+    }
 }
 
-private class PublishingParameters {
+internal class PublishingParameters {
     private PrivacySetting privacy;
     private string? user_name;
 
@@ -89,44 +102,14 @@ private class PublishingParameters {
     }
 }
 
-internal class YouTubeAuthorizer : GData.Authorizer, Object {
-    private RESTSupport.GoogleSession session;
-    private Spit.Publishing.Authenticator authenticator;
-
-    public YouTubeAuthorizer(RESTSupport.GoogleSession session, Spit.Publishing.Authenticator authenticator) 
{
-        this.session = session;
-        this.authenticator = authenticator;
-    }
-
-    public bool is_authorized_for_domain(GData.AuthorizationDomain domain) {
-        return domain.scope.has_suffix ("auth/youtube");
-    }
-
-    public void process_request(GData.AuthorizationDomain? domain,
-                                Soup.Message message) {
-        if (domain == null) {
-            return;
-        }
-
-        var header = "Bearer %s".printf(session.get_access_token());
-        message.request_headers.replace("Authorization", header);
-    }
-
-    public bool refresh_authorization (GLib.Cancellable? cancellable = null) throws GLib.Error {
-        this.authenticator.refresh();
-        return true;
-    }
-}
-
 public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher {
     private bool running;
     private PublishingParameters publishing_parameters;
     private Spit.Publishing.ProgressCallback? progress_reporter;
     private Spit.Publishing.Authenticator authenticator;
-    private GData.YouTubeService youtube_service;
 
     public YouTubePublisher(Spit.Publishing.Service service, Spit.Publishing.PluginHost host) {
-        base(service, host, "https://gdata.youtube.com/";);
+        base(service, host, "https://www.googleapis.com/upload/youtube/v3/videos";);
 
         this.running = false;
         this.publishing_parameters = new PublishingParameters();
@@ -161,8 +144,6 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher {
         
         publishing_parameters.set_user_name(get_session().get_user_name());
         
-        this.youtube_service = new GData.YouTubeService(DEVELOPER_KEY,
-                new YouTubeAuthorizer(get_session(), this.authenticator));
         do_show_publishing_options_pane();
     }
 
@@ -261,7 +242,7 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher {
             return;
 
         Spit.Publishing.Publishable[] publishables = get_host().get_publishables();
-        Uploader uploader = new Uploader(this.youtube_service, get_session(), publishables, 
publishing_parameters);
+        Uploader uploader = new Uploader(get_session(), publishables, publishing_parameters);
 
         uploader.upload_complete.connect(on_upload_complete);
         uploader.upload_error.connect(on_upload_error);
@@ -397,104 +378,20 @@ internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object {
     }
 }
 
-internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction {
-    private const string ENDPOINT_URL = "https://uploads.gdata.youtube.com/feeds/api/users/default/uploads";;
-    private PublishingParameters parameters;
-    private Publishing.RESTSupport.GoogleSession session;
-    private Spit.Publishing.Publishable publishable;
-    private GData.YouTubeService youtube_service;
-
-    public UploadTransaction(GData.YouTubeService youtube_service, Publishing.RESTSupport.GoogleSession 
session,
-        PublishingParameters parameters, Spit.Publishing.Publishable publishable) {
-        base(session, ENDPOINT_URL, Publishing.RESTSupport.HttpMethod.POST);
-        assert(session.is_authenticated());
-        this.session = session;
-        this.parameters = parameters;
-        this.publishable = publishable;
-        this.youtube_service = youtube_service;
-    }
-
-    public override void execute() throws Spit.Publishing.PublishingError {
-        var video = new GData.YouTubeVideo(null);
-
-        var slug = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME);
-        // Set title to publishing name, but if that's empty default to filename.
-        string title = publishable.get_publishing_name();
-        if (title == "") {
-            title = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME);
-        }
-        video.title = title;
-
-        video.is_private = (parameters.get_privacy() == PrivacySetting.PRIVATE);
-
-        if (parameters.get_privacy() == PrivacySetting.UNLISTED) {
-            video.set_access_control("list", GData.YouTubePermission.DENIED);
-        } else if (!video.is_private) {
-            video.set_access_control("list", GData.YouTubePermission.ALLOWED);
-        }
-
-        var file = publishable.get_serialized_file();
-
-        try {
-            var info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE + "," +
-                                       FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE);
-            var upload_stream = this.youtube_service.upload_video(video, slug,
-                    info.get_content_type());
-            var input_stream = file.read();
-
-            // Yuck...
-            var loop = new MainLoop(null, false);
-            this.splice_with_progress.begin(info, input_stream, upload_stream, (obj, res) => {
-                try {
-                    this.splice_with_progress.end(res);
-                } catch (Error error) {
-                    critical("Failed to upload: %s", error.message);
-                }
-                loop.quit();
-            });
-            loop.run();
-            video = this.youtube_service.finish_video_upload(upload_stream);
-        } catch (Error error) {
-            critical("Upload failed: %s", error.message);
-        }
-    }
-
-    private async void splice_with_progress(GLib.FileInfo info, GLib.InputStream input, GLib.OutputStream 
output) throws Error {
-        var total_bytes = info.get_size();
-        var bytes_to_write = total_bytes;
-        uint8 buffer[8192];
-
-        while (bytes_to_write > 0) {
-            var bytes_read = yield input.read_async(buffer);
-            if (bytes_read == 0)
-                break;
-
-            var bytes_written = yield output.write_async(buffer[0:bytes_read]);
-            bytes_to_write -= bytes_written;
-            chunk_transmitted((int)(total_bytes - bytes_to_write), (int) total_bytes);
-        }
-
-        yield output.close_async();
-        yield input.close_async();
-    }
-}
-
 internal class Uploader : Publishing.RESTSupport.BatchUploader {
     private PublishingParameters parameters;
-    private GData.YouTubeService youtube_service;
 
-    public Uploader(GData.YouTubeService youtube_service, Publishing.RESTSupport.GoogleSession session,
+    public Uploader(Publishing.RESTSupport.GoogleSession session,
         Spit.Publishing.Publishable[] publishables, PublishingParameters parameters) {
         base(session, publishables);
 
         this.parameters = parameters;
-        this.youtube_service = youtube_service;
     }
 
     protected override Publishing.RESTSupport.Transaction create_transaction(
         Spit.Publishing.Publishable publishable) {
-        return new UploadTransaction(this.youtube_service, (Publishing.RESTSupport.GoogleSession) 
get_session(),
-            parameters, get_current_publishable());
+        return new UploadTransaction((Publishing.RESTSupport.GoogleSession) get_session(),
+                                         parameters, get_current_publishable());
     }
 }
 
diff --git a/plugins/shotwell-publishing/YoutubeUploader.vala 
b/plugins/shotwell-publishing/YoutubeUploader.vala
new file mode 100644
index 00000000..f98bf15c
--- /dev/null
+++ b/plugins/shotwell-publishing/YoutubeUploader.vala
@@ -0,0 +1,133 @@
+using Spit;
+
+namespace Publishing.YouTube {
+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://www.googleapis.com/upload/youtube/v3/videos";,
+             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 {
+        // Collect parameters
+
+        var slug = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME);
+        // Set title to publishing name, but if that's empty default to filename.
+        string title = publishable.get_publishing_name();
+        if (title == "") {
+            title = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME);
+        }
+
+        var builder = new Json.Builder();
+        builder.begin_object();
+        builder.set_member_name("snippet");
+        builder.begin_object();
+        builder.set_member_name("description");
+        builder.add_string_value(slug);
+        builder.set_member_name("title");
+        builder.add_string_value(title);
+        builder.end_object();
+        builder.set_member_name("status");
+        builder.begin_object();
+        builder.set_member_name("privacyStatus");
+        builder.add_string_value(parameters.get_privacy().to_string());
+        builder.end_object();
+        builder.end_object();
+
+        var meta_data = Json.to_string (builder.get_root(), false);
+        debug ("Parameters: %s", meta_data);
+        var message_parts = new Soup.Multipart("multipart/related");
+        var headers = new Soup.MessageHeaders(Soup.MessageHeadersType.MULTIPART);
+        var encoding = new GLib.HashTable<string, string>(str_hash, str_equal);
+        encoding.insert("encoding", "UTF-8");
+        headers.set_content_type ("application/json", encoding);
+
+        message_parts.append_part (headers, new Bytes (meta_data.data));
+        headers = new Soup.MessageHeaders(Soup.MessageHeadersType.MULTIPART);
+        headers.set_content_type ("application/octet-stream", null);
+        headers.append("Content-Transfer-Encoding", "binary");
+
+        MappedFile? mapped_file = null;
+        try {
+            mapped_file = new MappedFile(publishable.get_serialized_file().get_path(), false);
+        } catch (Error e) {
+            throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(
+                _("A temporary file needed for publishing is unavailable"));
+        }
+
+
+        message_parts.append_part (headers, mapped_file.get_bytes());
+
+        var outbound_message = new Soup.Message.from_multipart (get_endpoint_url() + "?part=" + 
GLib.Uri.escape_string ("snippet,status"), message_parts);
+        outbound_message.get_request_headers().append("Authorization", "Bearer " +
+                                                session.get_access_token());
+
+        set_message(outbound_message, mapped_file.get_length() + meta_data.length);
+        set_is_executed(true);
+        send();
+
+#if 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+        var basename = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME);
+        int64 mapped_file_size = -1;
+
+        // attempt to map the binary image data from disk into memory
+        try {
+            mapped_file = publishable.get_serialized_file().read(null);
+            var info = ((FileInputStream)mapped_file).query_info("standard::size", null);
+            mapped_file_size = info.get_size();
+        } catch (Error 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);
+        }
+
+        // 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.set_request_body(null, mapped_file, (ssize_t)mapped_file_size);
+        set_message(outbound_message, (ulong)mapped_file_size);
+
+        // send the message and get its response
+        set_is_executed(true);
+        send();
+        #endif
+    }
+
+}
+}
diff --git a/plugins/shotwell-publishing/meson.build b/plugins/shotwell-publishing/meson.build
index 4d495438..3096ee56 100644
--- a/plugins/shotwell-publishing/meson.build
+++ b/plugins/shotwell-publishing/meson.build
@@ -2,7 +2,8 @@ shotwell_publishing_sources = [
     'shotwell-publishing.vala',
     'FlickrPublishing.vala',
     'TumblrPublishing.vala',
-#    'YouTubePublishing.vala',
+    'YouTubePublishing.vala',
+    'YoutubeUploader.vala',
     'PiwigoPublishing.vala',
     'PhotosPublisher.vala',
     'PhotosService.vala',
diff --git a/plugins/shotwell-publishing/shotwell-publishing.vala 
b/plugins/shotwell-publishing/shotwell-publishing.vala
index da6c87d5..5cebf0e3 100644
--- a/plugins/shotwell-publishing/shotwell-publishing.vala
+++ b/plugins/shotwell-publishing/shotwell-publishing.vala
@@ -34,8 +34,7 @@ private class ShotwellPublishingCoreServices : Object, Spit.Module {
         }
 #endif
 
-#if 0
-//HAVE_YOUTUBE
+#if HAVE_YOUTUBE
         if (authenicators.contains("youtube")) {
             pluggables += new YouTubeService(resource_directory);
         }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]