;;; g-photo.el --- Google Picasa Client ;;;$Id$ ;;; $Author: raman $ ;;; Description: Client For Accessing Picasa (Photo Albums) ;;; Keywords: Google Atom API ;;{{{ LCD Archive entry: ;;; LCD Archive Entry: ;;; gphoto| T. V. Raman |raman@cs.cornell.edu ;;; An emacs interface to Reader| ;;; $Date: 2006/09/28 17:47:44 $ | ;;; $Revision: 1.30 $ | ;;; Location undetermined ;;; License: GPL ;;; ;;}}} ;;{{{ Copyright: ;;; Copyright (c) 2006 and later, Google Inc. ;;; All rights reserved. ;;; Redistribution and use in source and binary forms, with or without modification, ;;; are permitted provided that the following conditions are met: ;;; * Redistributions of source code must retain the above copyright notice, ;;; this list of conditions and the following disclaimer. ;;; * Redistributions in binary form must reproduce the above copyright notice, ;;; this list of conditions and the following disclaimer in the documentation ;;; and/or other materials provided with the distribution. ;;; * The name of the author may not be used to endorse or promote products ;;; derived from this software without specific prior written permission. ;;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" ;;; AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ;;; IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ;;; ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE ;;; LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ;;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE ;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ;;; HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, ;;; STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY ;;; WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF ;;; SUCH DAMAGE. ;;}}} ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Commentary: ;;{{{ introduction ;;; Emacs client for accessing Picasa --- Google Photo Albums ;;; See http://code.google.com/apis/picasaweb/overview.html ;;}}} ;;{{{ Required modules (require 'cl) (declaim (optimize (safety 0) (speed 3))) (require 'g-utils) (require 'g-auth) (require 'g-app) (require 'browse-url) ;;}}} ;;{{{ Customizations (defgroup gphoto nil "Google photo" :group 'g) (defcustom gphoto-user-email nil "Mail address that identifies Picasa user." :type '(choice (const :tag "none" nil) (string :tag "username@gmail.com" "")) :group 'gphoto) (defcustom gphoto-user-password nil "Password for authenticating to Picasa account." :type '(radio (const :tag "Prompt for password" nil) (string :tag "Save password in .emacs")) :group 'gphoto) (defcustom gphoto-album-default-access "public" "Default access mode for newly created albums." :type 'string :group 'gphoto) (defcustom gphoto-album-default-commenting-enabled"true" "Default commenting mode for newly created albums." :type '(choice (string "true") (string "false")) :group 'gphoto) ;;}}} ;;{{{ Constants (defconst gphoto-service-name "lh2" "Service name for accessing Google photo.") (defsubst gphoto-p (service) "Check if this is Picasa." (declare (special gphoto-service-name)) (string-equal service gphoto-service-name)) ;;}}} ;;{{{ photo Authenticate (defsubst make-gphoto-auth () "Make a new gphoto auth handle." (declare (special gphoto-service-name gphoto-user-email gphoto-user-password)) (make-g-auth :service gphoto-service-name :email gphoto-user-email :password gphoto-user-password)) (defvar gphoto-auth-handle (make-gphoto-auth) "G auth handle used for signing into Picasa.") (defun gphoto-authenticate () "Authenticate into Google Photo." (declare (special gphoto-auth-handle)) (g-authenticate gphoto-auth-handle)) ;;}}} ;;{{{ Feed of feeds: (defconst gphoto-base-url "http://picasaweb.google.com/data/feed/api/user" "Base URI for Picasa services.") (defconst gphoto-album-or-tag '(("album" . "album") ("tag" . "tag")) "Choices for albums or tags.") (defsubst gphoto-read-feed-kind ( prompt choices) "Prompt with prompt to collect choice from choices." (completing-read prompt choices nil 'require-matchs)) (defvar gphoto-album-or-tag-template-url (format "%s/%%s?kind=%%s&v=2" gphoto-base-url) "URL template for feed of albums or tags from Picasa.") (defsubst gphoto-album-or-tag-url (userid kind) "Return url for feed of albums or tags." (declare (special gphoto-album-or-tag-template-url)) (format gphoto-album-or-tag-template-url userid kind)) ;;;###autoload (defun gphoto-feeds (kind user) "Retrieve and display feed of albums or tags after authenticating." (interactive (list (gphoto-read-feed-kind "Album or Tag: " gphoto-album-or-tag) (or user (g-auth-email gphoto-auth-handle)))) (declare (special gphoto-auth-handle g-atom-view-xsl g-curl-program g-curl-common-options g-cookie-options)) (g-auth-ensure-token gphoto-auth-handle) (g-display-result (format "%s %s %s %s '%s' %s" g-curl-program g-curl-common-options g-cookie-options (g-authorization gphoto-auth-handle) (gphoto-album-or-tag-url (g-url-encode user) kind) (g-curl-debug)) g-atom-view-xsl)) ;;;###autoload (defun gphoto-albums(&optional prompt) "Display feed of albums. Interactive prefix arg prompts for userid whose albums we request." (interactive "P") (gphoto-feeds "album" (if prompt (read-from-minibuffer "UserId:") (g-auth-email gphoto-auth-handle)))) ;;;###autoload (defun gphoto-tags() "View feed of tags." (interactive) (gphoto-feeds "tags" (g-auth-email gphoto-auth-handle))) ;;;###autoload (defun gphoto-view (resource) "Retrieve and display resource after authenticating." (interactive "sResource: ") (declare (special gphoto-auth-handle g-atom-view-xsl g-curl-program g-curl-common-options g-cookie-options)) (g-auth-ensure-token gphoto-auth-handle) (g-display-result (format "%s %s %s %s '%s' %s" g-curl-program g-curl-common-options g-cookie-options (g-authorization gphoto-auth-handle) resource (g-curl-debug)) g-atom-view-xsl)) ;;;###autoload (defun gphoto-download (resource) "Download resource after authenticating." (interactive "sResource: ") (declare (special gphoto-auth-handle g-curl-program g-curl-common-options g-cookie-options)) (g-auth-ensure-token gphoto-auth-handle) (g-display-result (format "%s %s %s %s '%s' %s" g-curl-program g-curl-common-options g-cookie-options (g-authorization gphoto-auth-handle) resource (g-curl-debug)) nil)) ;;}}} ;;{{{ Community search: (defvar gphoto-community-search-url-template "http://picasaweb.google.com/data/feed/api/all?q=%s" "URL template for searching all public photos") ;;;###autoload (defun gphoto-community-search (query) "Search all public photos." (interactive "sSearch Public Photos: ") (declare (special gphoto-community-search-url-template)) (gphoto-view (format gphoto-community-search-url-template (g-url-encode query)))) (defvar gphoto-recent-url-template (format "%s/%%s?kind=%%s&max-results=25" gphoto-base-url) "URL template for feed of recent photos or comments.") ;;;###autoload (defun gphoto-recent (user kind) "Retrieve feed of recently uploaded photos or comments." (interactive (list (read-from-minibuffer "User: " (g-auth-email gphoto-auth-handle)) (gphoto-read-feed-kind "p Photos c Comments: " '(("photo" . "photo") ("comment" . "comment"))))) (declare (special gphoto-recent-url-template)) (gphoto-view (format gphoto-recent-url-template user kind))) (defvar gphoto-user-search-url-template (format "%s/%%s?kind=photo&q=%%s" gphoto-base-url) "URL template for feed to search a user's photos.") ;;;###autoload (defun gphoto-user-search (user query) "Retrieve feed of recently uploaded comments for specified user." (interactive (list (read-from-minibuffer "User: " (g-auth-email gphoto-auth-handle)) (read-from-minibuffer "Query: "))) (declare (special gphoto-user-search-url-template)) (gphoto-view (format gphoto-user-search-url-template user (g-url-encode query)))) (defvar gphoto-user-tagsearch-url-template (format "%s/%%s?kind=photo&tag=%%s" gphoto-base-url) "URL template for feed to tag search a user's photos.") ;;;###autoload (defun gphoto-user-tagsearch (user tag) "Retrieve feed o matches comments for specified user." (interactive (list (read-from-minibuffer "User: " (g-auth-email gphoto-auth-handle)) (read-from-minibuffer "Tag: "))) (declare (special gphoto-user-tagsearch-url-template)) (gphoto-view (format gphoto-user-tagsearch-url-template user (g-url-encode tag)))) ;;}}} ;;{{{ Adding an album: (defstruct gphoto-album title summary location (access gphoto-album-default-access) (commenting-enabled gphoto-album-default-commenting-enabled) ;timestamp keywords) (defvar gphoto-album-template " %s %s %s %s %s %s " "Template for new album.") (defun gphoto-read-album () "Prompt user and return specified album structure." (let ((album (make-gphoto-album))) (loop for slot in '(title summary location keywords) do (eval `(setf (,(intern (format "gphoto-album-%s" slot)) album) (read-from-minibuffer (format "%s: " slot))))) (setf (gphoto-album-access album) (read-from-minibuffer "access:" gphoto-album-default-access nil nil nil gphoto-album-default-access)) (setf (gphoto-album-commenting-enabled album) (read-from-minibuffer "Commenting Enabled?:" gphoto-album-default-commenting-enabled nil nil nil gphoto-album-default-commenting-enabled)) album)) (defun gphoto-album-as-xml (album) "Return Atom entry for album structure." (declare (special gphoto-album-template)) (format gphoto-album-template (gphoto-album-title album) (gphoto-album-summary album) (gphoto-album-location album) (gphoto-album-access album) (gphoto-album-commenting-enabled album) (gphoto-album-keywords album))) (defsubst gphoto-album-create-url (auth-handle) "URL to which new albums are posted." (declare (special gphoto-base-url)) (format "%s/%s" gphoto-base-url (g-url-encode (g-auth-email auth-handle)))) (defsubst gphoto-post-album (album location) "Post album to location and return HTTP response." (declare (special g-cookie-options gphoto-auth-handle g-curl-program g-curl-common-options g-curl-atom-header)) (g-using-scratch (insert (gphoto-album-as-xml album)) (let ((cl (format "-H 'Content-Length: %s'" (g-buffer-bytes))) (status nil)) (shell-command-on-region (point-min) (point-max) (format "%s %s %s %s %s %s -i -X POST --data-binary @- %s 2>/dev/null" g-curl-program g-curl-common-options g-curl-atom-header cl (g-authorization gphoto-auth-handle) g-cookie-options location) (current-buffer) 'replace) (list (g-http-headers (point-min) (point-max)) (g-http-body (point-min) (point-max)))))) ;;;###autoload (defun gphoto-album-create () "Create a new GPhoto album." (interactive) (declare (special gphoto-auth-handle)) (g-auth-ensure-token gphoto-auth-handle) (let ((album (gphoto-read-album)) (headers nil) (body nil) (response nil)) (setq response (gphoto-post-album album (gphoto-album-create-url gphoto-auth-handle))) (setq headers (first response) body (second response)) (when (or (string-equal "201" (g-http-header "Status" headers)) (string-equal "200" (g-http-header "Status" headers))) (and (> (length body)0) (g-display-xml-string body g-atom-view-xsl))))) ;;}}} ;;{{{ Adding a photo: (defstruct gphoto-photo title summary filepath) (defun gphoto-read-photo () "Prompt user and return specified photo structure." (let ((photo (make-gphoto-photo))) (setf (gphoto-photo-filepath photo) (read-file-name "File: ")) (setf (gphoto-photo-title photo) (file-name-nondirectory (gphoto-photo-filepath photo))) photo)) (defun gphoto-photo-as-xml (photo) "Return Atom entry for photo structure." (declare (special gphoto-photo-template)) (format gphoto-photo-template (gphoto-photo-title photo) (gphoto-photo-summary photo))) (defsubst gphoto-async-post-photo (photo location) "Post photo to location asynchronously." (declare (special gphoto-auth-handle g-curl-program g-curl-image-options)) (g-using-scratch (let ((status nil) (image (format g-curl-image-options (expand-file-name (gphoto-photo-filepath photo)) (file-name-nondirectory (gphoto-photo-filepath photo)))) (extra-options "--silent --include")) (shell-command (format "%s %s %s %s %s &" g-curl-program image extra-options (g-authorization gphoto-auth-handle) location) (format "*upload %s" (gphoto-photo-title photo))) (message "Posting photo asynchronously.")))) ;;;###autoload (defun gphoto-photo-add (album-name photo ) "Add a photo to an existing album." (interactive (list (read-from-minibuffer "Album Name: ") (gphoto-read-photo))) (declare (special gphoto-auth-handle gphoto-base-url)) (g-auth-ensure-token gphoto-auth-handle) (let ((location (format "%s/%s/album/%s?v=2" gphoto-base-url (g-url-encode (g-auth-email gphoto-auth-handle)) album-name))) (gphoto-async-post-photo photo location))) ;;;###autoload (defun gphoto-directory-add-photos (directory album-name) "Add all jpeg files in a directory to specified album." (interactive (list (read-from-minibuffer "Directory: " default-directory) (read-from-minibuffer "Album Name: "))) (let ((files (directory-files (expand-file-name directory) 'full "\\(jpg$\\)\\|\\(JPG$\\)\\|\\(jpeg\\|\\(JPEG\\)$\\)"))) (loop for file in files and i from 0 do (gphoto-photo-add album-name (make-gphoto-photo :filepath (shell-quote-argument file) :title (shell-quote-argument (file-name-nondirectory file)))) (when (zerop (% i 10)) (message "Throttling uploads") (sit-for 10))))) ;;}}} ;;{{{ Adding comments and tags: (defvar gphoto-tag-template " %s " "Atom entry for tags.") (defvar gphoto-comment-template " %s " "Atom entry for comments.") (defsubst gphoto-tag-as-xml (tag) "Return Atom Entry for tag." (declare (special gphoto-tag-template)) (format gphoto-tag-template tag)) (defsubst gphoto-comment-as-xml (comment) "Return Atom Entry for comment." (declare (special gphoto-comment-template)) (format gphoto-comment-template comment)) (defsubst gphoto-post-update (update location) "Post update to location and return HTTP response." (declare (special g-cookie-options gphoto-auth-handle g-curl-program g-curl-common-options g-curl-atom-header)) (g-using-scratch (insert update) (let ((cl (format "-H 'Content-Length: %s'" (g-buffer-bytes))) (status nil)) (shell-command-on-region (point-min) (point-max) (format "%s %s %s %s %s %s -i -X POST --data-binary @- %s 2>/dev/null" g-curl-program g-curl-common-options g-curl-atom-header cl (g-authorization gphoto-auth-handle) g-cookie-options location) (current-buffer) 'replace) (list (g-http-headers (point-min) (point-max)) (g-http-body (point-min) (point-max)))))) (defvar gphoto-update-types '(("comment" . "comment") ("tag" . "tag")) "Update types.") ;;;###autoload (defun gphoto-comment-or-tag (type resource) "Add comments or tags to an existing photo." (interactive (list (completing-read "Tag Or Comment: " gphoto-update-types) (read-from-minibuffer "PostURL: "))) (declare (special gphoto-auth-handle gphoto-update-types)) (g-auth-ensure-token gphoto-auth-handle) (let ((headers nil) (entry (cond ((string= type "tag") (gphoto-tag-as-xml (read-from-minibuffer "Tag: "))) ((string= type "comment") (gphoto-comment-as-xml (g-get-user-input))))) (body nil) (response nil)) (unless entry (error "Invalid update specified.")) (setq response (gphoto-post-update entry resource)) (setq headers (first response) body (second response)) (when (> (length body)0) (g-display-xml-string body g-atom-view-xsl)))) ;;}}} ;;{{{ Sign out: ;;;###autoload (defun gphoto-sign-out() "Resets client so you can start with a different userid." (interactive) (declare (special gphoto-auth-handle gphoto-user-email gphoto-user-password)) (message "Signing out %s from Picasa" (g-auth-email gphoto-auth-handle)) (setq gphoto-user-email nil gphoto-user-password nil) (setq gphoto-auth-handle (make-gphoto-auth))) ;;;###autoload (defun gphoto-sign-in() "Resets client so you can start with a different userid." (interactive) (declare (special gphoto-auth-handle gphoto-user-email )) (setq gphoto-user-email (read-from-minibuffer "User Email:")) (setq gphoto-auth-handle (make-gphoto-auth)) (g-authenticate gphoto-auth-handle)) ;;}}} ;;{{{ deleting tags, comments or photos: (defun gphoto-delete-item (url) "Delete specified item." (interactive (list (read-from-minibuffer "Entry URL:"))) (declare (special gphoto-auth-handle)) (g-app-delete-entry gphoto-auth-handle url)) ;;}}} ;;{{{ Editting MetaData: ;;;###autoload (defun gphoto-edit-entry (url) "Retrieve metadata for entry and prepare it for editting. The retrieved entry is placed in a buffer ready for editing. `url' is the URL of the entry." (interactive (list (read-from-minibuffer "Edit URL:"))) (declare (special gphoto-auth-handle)) (g-app-edit-entry gphoto-auth-handle url 'g-app-put-entry)) ;;}}} (provide 'gphoto) ;;{{{ end of file ;;; local variables: ;;; folded-file: t ;;; byte-compile-dynamic: t ;;; end: ;;}}}