/src/googlecl/picasa/__init__.py
Python | 315 lines | 249 code | 24 blank | 42 comment | 4 complexity | 0bfdbf07fed889977deabf3653d47083 MD5 | raw file
1# Copyright (C) 2010 Google Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14import datetime 15import googlecl 16import googlecl.base 17import logging 18 19 20service_name = __name__.split('.')[-1] 21LOGGER_NAME = __name__ 22SECTION_HEADER = service_name.upper() 23 24LOG = logging.getLogger(LOGGER_NAME) 25 26 27def make_download_url(url): 28 """Makes the given URL for a picasa image point to the download.""" 29 return url[:url.rfind('/')+1]+'d'+url[url.rfind('/'):] 30 31 32def _map_access_string(access_string, default_value='private'): 33 if not access_string: 34 return default_value 35 # It seems to me that 'private' is less private than 'protected' 36 # but I'm going with what Picasa seems to be using. 37 access_string_mappings = {'public': 'public', 38 'private': 'protected', 39 'protected': 'private', 40 'draft': 'private', 41 'hidden': 'private', 42 'link': 'private'} 43 try: 44 return access_string_mappings[access_string] 45 except KeyError: 46 import re 47 if access_string.find('link') != -1: 48 return 'private' 49 return default_value 50 51 52class PhotoEntryToStringWrapper(googlecl.base.BaseEntryToStringWrapper): 53 caption = googlecl.base.BaseEntryToStringWrapper.summary 54 55 @property 56 def distance(self): 57 """The distance to the subject.""" 58 return self.entry.exif.distance.text 59 60 @property 61 def ev(self): 62 """Exposure value, if possible to calculate""" 63 try: 64 # Using the equation for EV I found on Wikipedia... 65 N = float(self.fstop) 66 t = float(self.exposure) 67 import math # import math if fstop and exposure work 68 # str() actually "rounds" floats. Try repr(3.3) and print 3.3 69 ev_long_str = str(math.log(math.pow(N,2)/t, 2)) 70 dec_point = ev_long_str.find('.') 71 # In the very rare case that there is no decimal point: 72 if dec_point == -1: 73 # Technically this can return something like 10000, violating 74 # our desired precision. But not likely. 75 return ev_long_str 76 else: 77 # return value to 1 decimal place 78 return ev_long_str[0:dec_point+2] 79 except Exception: 80 # Don't really care what goes wrong -- result is the same. 81 return None 82 83 @property 84 def exposure(self): 85 """The exposure time used.""" 86 return self.entry.exif.exposure.text 87 shutter = exposure 88 speed = exposure 89 90 @property 91 def flash(self): 92 """Boolean value indicating whether the flash was used.""" 93 return self.entry.exif.flash.text 94 95 @property 96 def focallength(self): 97 """The focal length used.""" 98 return self.entry.exif.focallength.text 99 100 @property 101 def fstop(self): 102 """The fstop value used.""" 103 return self.entry.exif.fstop.text 104 105 @property 106 def imageUniqueID(self): 107 """The unique image ID for the photo.""" 108 return self.entry.exif.imageUniqueID.text 109 id = imageUniqueID 110 111 @property 112 def iso(self): 113 """The iso equivalent value used.""" 114 return self.entry.exif.iso.text 115 116 @property 117 def make(self): 118 """The make of the camera used.""" 119 return self.entry.exif.make.text 120 121 @property 122 def model(self): 123 """The model of the camera used.""" 124 return self.entry.exif.model.text 125 126 @property 127 def tags(self): 128 """Tags / keywords or labels.""" 129 tags_text = self.entry.media.keywords.text 130 tags_text = tags_text.replace(', ', ',') 131 tags_list = tags_text.split(',') 132 return self.intra_property_delimiter.join(tags_list) 133 labels = tags 134 keywords = tags 135 136 @property 137 def time(self): 138 """The date/time the photo was taken. 139 140 Represented as the number of milliseconds since January 1st, 1970. 141 Note: The value of this element should always be identical to the value of 142 the <gphoto:timestamp>. 143 """ 144 return self.entry.exif.time.text 145 when = time 146 147 # Overload from base.EntryToStringWrapper to use make_download_url 148 @property 149 def url_download(self): 150 """URL to the original uploaded image, suitable for downloading from.""" 151 return make_download_url(self.url_direct) 152 153 154class AlbumEntryToStringWrapper(googlecl.base.BaseEntryToStringWrapper): 155 @property 156 def access(self): 157 """Access level of the album, one of "public", "private", or "unlisted".""" 158 # Convert values to ones the user selects on the web 159 txt = self.entry.access.text 160 if txt == 'protected': 161 return 'private' 162 if txt == 'private': 163 return 'anyone with link' 164 return txt 165 visibility = access 166 167 @property 168 def location(self): 169 """Location of the album (where pictures were taken).""" 170 return self.entry.location.text 171 where = location 172 173 @property 174 def published(self): 175 """When the album was published/uploaded in local time.""" 176 date = datetime.datetime.strptime(self.entry.published.text, 177 googlecl.calendar.date.QUERY_DATE_FORMAT) 178 date = date - googlecl.calendar.date.get_utc_timedelta() 179 return date.strftime('%Y-%m-%dT%H:%M:%S') 180 when = published 181 182 183#=============================================================================== 184# Each of the following _run_* functions execute a particular task. 185# 186# Keyword arguments: 187# client: Client to the service being used. 188# options: Contains all attributes required to perform the task 189# args: Additional arguments passed in on the command line, may or may not be 190# required 191#=============================================================================== 192def _run_create(client, options, args): 193 # Paths to media might be in options.src, args, both, or neither. 194 # But both are guaranteed to be lists. 195 media_list = options.src + args 196 197 album = client.create_album(title=options.title, summary=options.summary, 198 access=options.access, date=options.date) 199 if media_list: 200 client.InsertMediaList(album, media_list=media_list, 201 tags=options.tags) 202 LOG.info('Created album: %s' % album.GetHtmlLink().href) 203 204 205def _run_delete(client, options, args): 206 if options.query or options.photo: 207 entry_type = 'media' 208 search_string = options.query 209 else: 210 entry_type = 'album' 211 search_string = options.title 212 213 titles_list = googlecl.build_titles_list(options.title, args) 214 entries = client.build_entry_list(titles=titles_list, 215 query=options.query, 216 photo_title=options.photo) 217 if not entries: 218 LOG.info('No %ss matching %s' % (entry_type, search_string)) 219 else: 220 client.DeleteEntryList(entries, entry_type, options.prompt) 221 222 223def _run_list(client, options, args): 224 titles_list = googlecl.build_titles_list(options.title, args) 225 entries = client.build_entry_list(user=options.owner or options.user, 226 titles=titles_list, 227 query=options.query, 228 force_photos=True, 229 photo_title=options.photo) 230 for entry in entries: 231 print googlecl.base.compile_entry_string(PhotoEntryToStringWrapper(entry), 232 options.fields.split(','), 233 delimiter=options.delimiter) 234 235 236def _run_list_albums(client, options, args): 237 titles_list = googlecl.build_titles_list(options.title, args) 238 entries = client.build_entry_list(user=options.owner or options.user, 239 titles=titles_list, 240 force_photos=False) 241 for entry in entries: 242 print googlecl.base.compile_entry_string(AlbumEntryToStringWrapper(entry), 243 options.fields.split(','), 244 delimiter=options.delimiter) 245 246 247def _run_post(client, options, args): 248 media_list = options.src + args 249 if not media_list: 250 LOG.error('Must provide paths to media to post!') 251 album = client.GetSingleAlbum(user=options.owner or options.user, 252 title=options.title) 253 if album: 254 client.InsertMediaList(album, media_list, tags=options.tags, 255 user=options.owner or options.user, 256 photo_name=options.photo, caption=options.summary) 257 else: 258 LOG.error('No albums found that match ' + options.title) 259 260 261def _run_get(client, options, args): 262 if not options.dest: 263 LOG.error('Must provide destination of album(s)!') 264 return 265 266 titles_list = googlecl.build_titles_list(options.title, args) 267 client.DownloadAlbum(options.dest, 268 user=options.owner or options.user, 269 video_format=options.format or 'mp4', 270 titles=titles_list, 271 photo_title=options.photo) 272 273 274def _run_tag(client, options, args): 275 titles_list = googlecl.build_titles_list(options.title, args) 276 entries = client.build_entry_list(user=options.owner or options.user, 277 query=options.query, 278 titles=titles_list, 279 force_photos=True, 280 photo_title=options.photo) 281 if entries: 282 client.TagPhotos(entries, options.tags, options.summary) 283 else: 284 LOG.error('No matches for the title and/or query you gave.') 285 286 287TASKS = {'create': googlecl.base.Task('Create an album', 288 callback=_run_create, 289 required='title', 290 optional=['src', 'date', 291 'summary', 'tags', 'access']), 292 'post': googlecl.base.Task('Post photos to an album', 293 callback=_run_post, 294 required=['title', 'src'], 295 optional=['tags', 'owner', 'photo', 296 'summary']), 297 'delete': googlecl.base.Task('Delete photos or albums', 298 callback=_run_delete, 299 required=[['title', 'query']], 300 optional='photo'), 301 'list': googlecl.base.Task('List photos', callback=_run_list, 302 required=['fields', 'delimiter'], 303 optional=['title', 'query', 304 'owner', 'photo']), 305 'list-albums': googlecl.base.Task('List albums', 306 callback=_run_list_albums, 307 required=['fields', 'delimiter'], 308 optional=['title', 'owner']), 309 'get': googlecl.base.Task('Download albums', callback=_run_get, 310 required=['title', 'dest'], 311 optional=['owner', 'format', 'photo']), 312 'tag': googlecl.base.Task('Tag/caption photos', callback=_run_tag, 313 required=[['title', 'query'], 314 ['tags', 'summary']], 315 optional=['owner', 'photo'])}