/gdata/photos/service.py

http://radioappz.googlecode.com/ · Python · 680 lines · 497 code · 53 blank · 130 comment · 42 complexity · 6ac8258b43b46a835fd77db4efdbc619 MD5 · raw file

  1. #!/usr/bin/env python
  2. # -*-*- encoding: utf-8 -*-*-
  3. #
  4. # This is the service file for the Google Photo python client.
  5. # It is used for higher level operations.
  6. #
  7. # $Id: service.py 144 2007-10-25 21:03:34Z havard.gulldahl $
  8. #
  9. # Copyright 2007 H?vard Gulldahl
  10. #
  11. # Licensed under the Apache License, Version 2.0 (the "License");
  12. # you may not use this file except in compliance with the License.
  13. # You may obtain a copy of the License at
  14. #
  15. # http://www.apache.org/licenses/LICENSE-2.0
  16. #
  17. # Unless required by applicable law or agreed to in writing, software
  18. # distributed under the License is distributed on an "AS IS" BASIS,
  19. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  20. # See the License for the specific language governing permissions and
  21. # limitations under the License.
  22. """Google PhotoService provides a human-friendly interface to
  23. Google Photo (a.k.a Picasa Web) services[1].
  24. It extends gdata.service.GDataService and as such hides all the
  25. nasty details about authenticating, parsing and communicating with
  26. Google Photos.
  27. [1]: http://code.google.com/apis/picasaweb/gdata.html
  28. Example:
  29. import gdata.photos, gdata.photos.service
  30. pws = gdata.photos.service.PhotosService()
  31. pws.ClientLogin(username, password)
  32. #Get all albums
  33. albums = pws.GetUserFeed().entry
  34. # Get all photos in second album
  35. photos = pws.GetFeed(albums[1].GetPhotosUri()).entry
  36. # Get all tags for photos in second album and print them
  37. tags = pws.GetFeed(albums[1].GetTagsUri()).entry
  38. print [ tag.summary.text for tag in tags ]
  39. # Get all comments for the first photos in list and print them
  40. comments = pws.GetCommentFeed(photos[0].GetCommentsUri()).entry
  41. print [ c.summary.text for c in comments ]
  42. # Get a photo to work with
  43. photo = photos[0]
  44. # Update metadata
  45. # Attributes from the <gphoto:*> namespace
  46. photo.summary.text = u'A nice view from my veranda'
  47. photo.title.text = u'Verandaview.jpg'
  48. # Attributes from the <media:*> namespace
  49. photo.media.keywords.text = u'Home, Long-exposure, Sunset' # Comma-separated
  50. # Adding attributes to media object
  51. # Rotate 90 degrees clockwise
  52. photo.rotation = gdata.photos.Rotation(text='90')
  53. # Submit modified photo object
  54. photo = pws.UpdatePhotoMetadata(photo)
  55. # Make sure you only modify the newly returned object, else you'll get
  56. # versioning errors. See Optimistic-concurrency
  57. # Add comment to a picture
  58. comment = pws.InsertComment(photo, u'I wish the water always was this warm')
  59. # Remove comment because it was silly
  60. print "*blush*"
  61. pws.Delete(comment.GetEditLink().href)
  62. """
  63. __author__ = u'havard@gulldahl.no'# (H?vard Gulldahl)' #BUG: pydoc chokes on non-ascii chars in __author__
  64. __license__ = 'Apache License v2'
  65. __version__ = '$Revision: 176 $'[11:-2]
  66. import sys, os.path, StringIO
  67. import time
  68. import gdata.service
  69. import gdata
  70. import atom.service
  71. import atom
  72. import gdata.photos
  73. SUPPORTED_UPLOAD_TYPES = ('bmp', 'jpeg', 'jpg', 'gif', 'png')
  74. UNKOWN_ERROR=1000
  75. GPHOTOS_BAD_REQUEST=400
  76. GPHOTOS_CONFLICT=409
  77. GPHOTOS_INTERNAL_SERVER_ERROR=500
  78. GPHOTOS_INVALID_ARGUMENT=601
  79. GPHOTOS_INVALID_CONTENT_TYPE=602
  80. GPHOTOS_NOT_AN_IMAGE=603
  81. GPHOTOS_INVALID_KIND=604
  82. class GooglePhotosException(Exception):
  83. def __init__(self, response):
  84. self.error_code = response['status']
  85. self.reason = response['reason'].strip()
  86. if '<html>' in str(response['body']): #general html message, discard it
  87. response['body'] = ""
  88. self.body = response['body'].strip()
  89. self.message = "(%(status)s) %(body)s -- %(reason)s" % response
  90. #return explicit error codes
  91. error_map = { '(12) Not an image':GPHOTOS_NOT_AN_IMAGE,
  92. 'kind: That is not one of the acceptable values':
  93. GPHOTOS_INVALID_KIND,
  94. }
  95. for msg, code in error_map.iteritems():
  96. if self.body == msg:
  97. self.error_code = code
  98. break
  99. self.args = [self.error_code, self.reason, self.body]
  100. class PhotosService(gdata.service.GDataService):
  101. userUri = '/data/feed/api/user/%s'
  102. def __init__(self, email=None, password=None, source=None,
  103. server='picasaweb.google.com', additional_headers=None,
  104. **kwargs):
  105. """Creates a client for the Google Photos service.
  106. Args:
  107. email: string (optional) The user's email address, used for
  108. authentication.
  109. password: string (optional) The user's password.
  110. source: string (optional) The name of the user's application.
  111. server: string (optional) The name of the server to which a connection
  112. will be opened. Default value: 'picasaweb.google.com'.
  113. **kwargs: The other parameters to pass to gdata.service.GDataService
  114. constructor.
  115. """
  116. self.email = email
  117. self.client = source
  118. gdata.service.GDataService.__init__(
  119. self, email=email, password=password, service='lh2', source=source,
  120. server=server, additional_headers=additional_headers, **kwargs)
  121. def GetFeed(self, uri, limit=None, start_index=None):
  122. """Get a feed.
  123. The results are ordered by the values of their `updated' elements,
  124. with the most recently updated entry appearing first in the feed.
  125. Arguments:
  126. uri: the uri to fetch
  127. limit (optional): the maximum number of entries to return. Defaults to what
  128. the server returns.
  129. Returns:
  130. one of gdata.photos.AlbumFeed,
  131. gdata.photos.UserFeed,
  132. gdata.photos.PhotoFeed,
  133. gdata.photos.CommentFeed,
  134. gdata.photos.TagFeed,
  135. depending on the results of the query.
  136. Raises:
  137. GooglePhotosException
  138. See:
  139. http://code.google.com/apis/picasaweb/gdata.html#Get_Album_Feed_Manual
  140. """
  141. if limit is not None:
  142. uri += '&max-results=%s' % limit
  143. if start_index is not None:
  144. uri += '&start-index=%s' % start_index
  145. try:
  146. return self.Get(uri, converter=gdata.photos.AnyFeedFromString)
  147. except gdata.service.RequestError, e:
  148. raise GooglePhotosException(e.args[0])
  149. def GetEntry(self, uri, limit=None, start_index=None):
  150. """Get an Entry.
  151. Arguments:
  152. uri: the uri to the entry
  153. limit (optional): the maximum number of entries to return. Defaults to what
  154. the server returns.
  155. Returns:
  156. one of gdata.photos.AlbumEntry,
  157. gdata.photos.UserEntry,
  158. gdata.photos.PhotoEntry,
  159. gdata.photos.CommentEntry,
  160. gdata.photos.TagEntry,
  161. depending on the results of the query.
  162. Raises:
  163. GooglePhotosException
  164. """
  165. if limit is not None:
  166. uri += '&max-results=%s' % limit
  167. if start_index is not None:
  168. uri += '&start-index=%s' % start_index
  169. try:
  170. return self.Get(uri, converter=gdata.photos.AnyEntryFromString)
  171. except gdata.service.RequestError, e:
  172. raise GooglePhotosException(e.args[0])
  173. def GetUserFeed(self, kind='album', user='default', limit=None):
  174. """Get user-based feed, containing albums, photos, comments or tags;
  175. defaults to albums.
  176. The entries are ordered by the values of their `updated' elements,
  177. with the most recently updated entry appearing first in the feed.
  178. Arguments:
  179. kind: the kind of entries to get, either `album', `photo',
  180. `comment' or `tag', or a python list of these. Defaults to `album'.
  181. user (optional): whose albums we're querying. Defaults to current user.
  182. limit (optional): the maximum number of entries to return.
  183. Defaults to everything the server returns.
  184. Returns:
  185. gdata.photos.UserFeed, containing appropriate Entry elements
  186. See:
  187. http://code.google.com/apis/picasaweb/gdata.html#Get_Album_Feed_Manual
  188. http://googledataapis.blogspot.com/2007/07/picasa-web-albums-adds-new-api-features.html
  189. """
  190. if isinstance(kind, (list, tuple) ):
  191. kind = ",".join(kind)
  192. uri = '/data/feed/api/user/%s?kind=%s' % (user, kind)
  193. return self.GetFeed(uri, limit=limit)
  194. def GetTaggedPhotos(self, tag, user='default', limit=None):
  195. """Get all photos belonging to a specific user, tagged by the given keyword
  196. Arguments:
  197. tag: The tag you're looking for, e.g. `dog'
  198. user (optional): Whose images/videos you want to search, defaults
  199. to current user
  200. limit (optional): the maximum number of entries to return.
  201. Defaults to everything the server returns.
  202. Returns:
  203. gdata.photos.UserFeed containing PhotoEntry elements
  204. """
  205. # Lower-casing because of
  206. # http://code.google.com/p/gdata-issues/issues/detail?id=194
  207. uri = '/data/feed/api/user/%s?kind=photo&tag=%s' % (user, tag.lower())
  208. return self.GetFeed(uri, limit)
  209. def SearchUserPhotos(self, query, user='default', limit=100):
  210. """Search through all photos for a specific user and return a feed.
  211. This will look for matches in file names and image tags (a.k.a. keywords)
  212. Arguments:
  213. query: The string you're looking for, e.g. `vacation'
  214. user (optional): The username of whose photos you want to search, defaults
  215. to current user.
  216. limit (optional): Don't return more than `limit' hits, defaults to 100
  217. Only public photos are searched, unless you are authenticated and
  218. searching through your own photos.
  219. Returns:
  220. gdata.photos.UserFeed with PhotoEntry elements
  221. """
  222. uri = '/data/feed/api/user/%s?kind=photo&q=%s' % (user, query)
  223. return self.GetFeed(uri, limit=limit)
  224. def SearchCommunityPhotos(self, query, limit=100):
  225. """Search through all public photos and return a feed.
  226. This will look for matches in file names and image tags (a.k.a. keywords)
  227. Arguments:
  228. query: The string you're looking for, e.g. `vacation'
  229. limit (optional): Don't return more than `limit' hits, defaults to 100
  230. Returns:
  231. gdata.GDataFeed with PhotoEntry elements
  232. """
  233. uri='/data/feed/api/all?q=%s' % query
  234. return self.GetFeed(uri, limit=limit)
  235. def GetContacts(self, user='default', limit=None):
  236. """Retrieve a feed that contains a list of your contacts
  237. Arguments:
  238. user: Username of the user whose contacts you want
  239. Returns
  240. gdata.photos.UserFeed, with UserEntry entries
  241. See:
  242. http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38
  243. """
  244. uri = '/data/feed/api/user/%s/contacts?kind=user' % user
  245. return self.GetFeed(uri, limit=limit)
  246. def SearchContactsPhotos(self, user='default', search=None, limit=None):
  247. """Search over your contacts' photos and return a feed
  248. Arguments:
  249. user: Username of the user whose contacts you want
  250. search (optional): What to search for (photo title, description and keywords)
  251. Returns
  252. gdata.photos.UserFeed, with PhotoEntry elements
  253. See:
  254. http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38
  255. """
  256. uri = '/data/feed/api/user/%s/contacts?kind=photo&q=%s' % (user, search)
  257. return self.GetFeed(uri, limit=limit)
  258. def InsertAlbum(self, title, summary, location=None, access='public',
  259. commenting_enabled='true', timestamp=None):
  260. """Add an album.
  261. Needs authentication, see self.ClientLogin()
  262. Arguments:
  263. title: Album title
  264. summary: Album summary / description
  265. access (optional): `private' or `public'. Public albums are searchable
  266. by everyone on the internet. Defaults to `public'
  267. commenting_enabled (optional): `true' or `false'. Defaults to `true'.
  268. timestamp (optional): A date and time for the album, in milliseconds since
  269. Unix epoch[1] UTC. Defaults to now.
  270. Returns:
  271. The newly created gdata.photos.AlbumEntry
  272. See:
  273. http://code.google.com/apis/picasaweb/gdata.html#Add_Album_Manual_Installed
  274. [1]: http://en.wikipedia.org/wiki/Unix_epoch
  275. """
  276. album = gdata.photos.AlbumEntry()
  277. album.title = atom.Title(text=title, title_type='text')
  278. album.summary = atom.Summary(text=summary, summary_type='text')
  279. if location is not None:
  280. album.location = gdata.photos.Location(text=location)
  281. album.access = gdata.photos.Access(text=access)
  282. if commenting_enabled in ('true', 'false'):
  283. album.commentingEnabled = gdata.photos.CommentingEnabled(text=commenting_enabled)
  284. if timestamp is None:
  285. timestamp = '%i' % int(time.time() * 1000)
  286. album.timestamp = gdata.photos.Timestamp(text=timestamp)
  287. try:
  288. return self.Post(album, uri=self.userUri % self.email,
  289. converter=gdata.photos.AlbumEntryFromString)
  290. except gdata.service.RequestError, e:
  291. raise GooglePhotosException(e.args[0])
  292. def InsertPhoto(self, album_or_uri, photo, filename_or_handle,
  293. content_type='image/jpeg'):
  294. """Add a PhotoEntry
  295. Needs authentication, see self.ClientLogin()
  296. Arguments:
  297. album_or_uri: AlbumFeed or uri of the album where the photo should go
  298. photo: PhotoEntry to add
  299. filename_or_handle: A file-like object or file name where the image/video
  300. will be read from
  301. content_type (optional): Internet media type (a.k.a. mime type) of
  302. media object. Currently Google Photos supports these types:
  303. o image/bmp
  304. o image/gif
  305. o image/jpeg
  306. o image/png
  307. Images will be converted to jpeg on upload. Defaults to `image/jpeg'
  308. """
  309. try:
  310. assert(isinstance(photo, gdata.photos.PhotoEntry))
  311. except AssertionError:
  312. raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT,
  313. 'body':'`photo` must be a gdata.photos.PhotoEntry instance',
  314. 'reason':'Found %s, not PhotoEntry' % type(photo)
  315. })
  316. try:
  317. majtype, mintype = content_type.split('/')
  318. assert(mintype in SUPPORTED_UPLOAD_TYPES)
  319. except (ValueError, AssertionError):
  320. raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE,
  321. 'body':'This is not a valid content type: %s' % content_type,
  322. 'reason':'Accepted content types: %s' % \
  323. ['image/'+t for t in SUPPORTED_UPLOAD_TYPES]
  324. })
  325. if isinstance(filename_or_handle, (str, unicode)) and \
  326. os.path.exists(filename_or_handle): # it's a file name
  327. mediasource = gdata.MediaSource()
  328. mediasource.setFile(filename_or_handle, content_type)
  329. elif hasattr(filename_or_handle, 'read'):# it's a file-like resource
  330. if hasattr(filename_or_handle, 'seek'):
  331. filename_or_handle.seek(0) # rewind pointer to the start of the file
  332. # gdata.MediaSource needs the content length, so read the whole image
  333. file_handle = StringIO.StringIO(filename_or_handle.read())
  334. name = 'image'
  335. if hasattr(filename_or_handle, 'name'):
  336. name = filename_or_handle.name
  337. mediasource = gdata.MediaSource(file_handle, content_type,
  338. content_length=file_handle.len, file_name=name)
  339. else: #filename_or_handle is not valid
  340. raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT,
  341. 'body':'`filename_or_handle` must be a path name or a file-like object',
  342. 'reason':'Found %s, not path name or object with a .read() method' % \
  343. type(filename_or_handle)
  344. })
  345. if isinstance(album_or_uri, (str, unicode)): # it's a uri
  346. feed_uri = album_or_uri
  347. elif hasattr(album_or_uri, 'GetFeedLink'): # it's a AlbumFeed object
  348. feed_uri = album_or_uri.GetFeedLink().href
  349. try:
  350. return self.Post(photo, uri=feed_uri, media_source=mediasource,
  351. converter=gdata.photos.PhotoEntryFromString)
  352. except gdata.service.RequestError, e:
  353. raise GooglePhotosException(e.args[0])
  354. def InsertPhotoSimple(self, album_or_uri, title, summary, filename_or_handle,
  355. content_type='image/jpeg', keywords=None):
  356. """Add a photo without constructing a PhotoEntry.
  357. Needs authentication, see self.ClientLogin()
  358. Arguments:
  359. album_or_uri: AlbumFeed or uri of the album where the photo should go
  360. title: Photo title
  361. summary: Photo summary / description
  362. filename_or_handle: A file-like object or file name where the image/video
  363. will be read from
  364. content_type (optional): Internet media type (a.k.a. mime type) of
  365. media object. Currently Google Photos supports these types:
  366. o image/bmp
  367. o image/gif
  368. o image/jpeg
  369. o image/png
  370. Images will be converted to jpeg on upload. Defaults to `image/jpeg'
  371. keywords (optional): a 1) comma separated string or 2) a python list() of
  372. keywords (a.k.a. tags) to add to the image.
  373. E.g. 1) `dog, vacation, happy' 2) ['dog', 'happy', 'vacation']
  374. Returns:
  375. The newly created gdata.photos.PhotoEntry or GooglePhotosException on errors
  376. See:
  377. http://code.google.com/apis/picasaweb/gdata.html#Add_Album_Manual_Installed
  378. [1]: http://en.wikipedia.org/wiki/Unix_epoch
  379. """
  380. metadata = gdata.photos.PhotoEntry()
  381. metadata.title=atom.Title(text=title)
  382. metadata.summary = atom.Summary(text=summary, summary_type='text')
  383. if keywords is not None:
  384. if isinstance(keywords, list):
  385. keywords = ','.join(keywords)
  386. metadata.media.keywords = gdata.media.Keywords(text=keywords)
  387. return self.InsertPhoto(album_or_uri, metadata, filename_or_handle,
  388. content_type)
  389. def UpdatePhotoMetadata(self, photo):
  390. """Update a photo's metadata.
  391. Needs authentication, see self.ClientLogin()
  392. You can update any or all of the following metadata properties:
  393. * <title>
  394. * <media:description>
  395. * <gphoto:checksum>
  396. * <gphoto:client>
  397. * <gphoto:rotation>
  398. * <gphoto:timestamp>
  399. * <gphoto:commentingEnabled>
  400. Arguments:
  401. photo: a gdata.photos.PhotoEntry object with updated elements
  402. Returns:
  403. The modified gdata.photos.PhotoEntry
  404. Example:
  405. p = GetFeed(uri).entry[0]
  406. p.title.text = u'My new text'
  407. p.commentingEnabled.text = 'false'
  408. p = UpdatePhotoMetadata(p)
  409. It is important that you don't keep the old object around, once
  410. it has been updated. See
  411. http://code.google.com/apis/gdata/reference.html#Optimistic-concurrency
  412. """
  413. try:
  414. return self.Put(data=photo, uri=photo.GetEditLink().href,
  415. converter=gdata.photos.PhotoEntryFromString)
  416. except gdata.service.RequestError, e:
  417. raise GooglePhotosException(e.args[0])
  418. def UpdatePhotoBlob(self, photo_or_uri, filename_or_handle,
  419. content_type = 'image/jpeg'):
  420. """Update a photo's binary data.
  421. Needs authentication, see self.ClientLogin()
  422. Arguments:
  423. photo_or_uri: a gdata.photos.PhotoEntry that will be updated, or a
  424. `edit-media' uri pointing to it
  425. filename_or_handle: A file-like object or file name where the image/video
  426. will be read from
  427. content_type (optional): Internet media type (a.k.a. mime type) of
  428. media object. Currently Google Photos supports these types:
  429. o image/bmp
  430. o image/gif
  431. o image/jpeg
  432. o image/png
  433. Images will be converted to jpeg on upload. Defaults to `image/jpeg'
  434. Returns:
  435. The modified gdata.photos.PhotoEntry
  436. Example:
  437. p = GetFeed(PhotoUri)
  438. p = UpdatePhotoBlob(p, '/tmp/newPic.jpg')
  439. It is important that you don't keep the old object around, once
  440. it has been updated. See
  441. http://code.google.com/apis/gdata/reference.html#Optimistic-concurrency
  442. """
  443. try:
  444. majtype, mintype = content_type.split('/')
  445. assert(mintype in SUPPORTED_UPLOAD_TYPES)
  446. except (ValueError, AssertionError):
  447. raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE,
  448. 'body':'This is not a valid content type: %s' % content_type,
  449. 'reason':'Accepted content types: %s' % \
  450. ['image/'+t for t in SUPPORTED_UPLOAD_TYPES]
  451. })
  452. if isinstance(filename_or_handle, (str, unicode)) and \
  453. os.path.exists(filename_or_handle): # it's a file name
  454. photoblob = gdata.MediaSource()
  455. photoblob.setFile(filename_or_handle, content_type)
  456. elif hasattr(filename_or_handle, 'read'):# it's a file-like resource
  457. if hasattr(filename_or_handle, 'seek'):
  458. filename_or_handle.seek(0) # rewind pointer to the start of the file
  459. # gdata.MediaSource needs the content length, so read the whole image
  460. file_handle = StringIO.StringIO(filename_or_handle.read())
  461. name = 'image'
  462. if hasattr(filename_or_handle, 'name'):
  463. name = filename_or_handle.name
  464. mediasource = gdata.MediaSource(file_handle, content_type,
  465. content_length=file_handle.len, file_name=name)
  466. else: #filename_or_handle is not valid
  467. raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT,
  468. 'body':'`filename_or_handle` must be a path name or a file-like object',
  469. 'reason':'Found %s, not path name or an object with .read() method' % \
  470. type(filename_or_handle)
  471. })
  472. if isinstance(photo_or_uri, (str, unicode)):
  473. entry_uri = photo_or_uri # it's a uri
  474. elif hasattr(photo_or_uri, 'GetEditMediaLink'):
  475. entry_uri = photo_or_uri.GetEditMediaLink().href
  476. try:
  477. return self.Put(photoblob, entry_uri,
  478. converter=gdata.photos.PhotoEntryFromString)
  479. except gdata.service.RequestError, e:
  480. raise GooglePhotosException(e.args[0])
  481. def InsertTag(self, photo_or_uri, tag):
  482. """Add a tag (a.k.a. keyword) to a photo.
  483. Needs authentication, see self.ClientLogin()
  484. Arguments:
  485. photo_or_uri: a gdata.photos.PhotoEntry that will be tagged, or a
  486. `post' uri pointing to it
  487. (string) tag: The tag/keyword
  488. Returns:
  489. The new gdata.photos.TagEntry
  490. Example:
  491. p = GetFeed(PhotoUri)
  492. tag = InsertTag(p, 'Beautiful sunsets')
  493. """
  494. tag = gdata.photos.TagEntry(title=atom.Title(text=tag))
  495. if isinstance(photo_or_uri, (str, unicode)):
  496. post_uri = photo_or_uri # it's a uri
  497. elif hasattr(photo_or_uri, 'GetEditMediaLink'):
  498. post_uri = photo_or_uri.GetPostLink().href
  499. try:
  500. return self.Post(data=tag, uri=post_uri,
  501. converter=gdata.photos.TagEntryFromString)
  502. except gdata.service.RequestError, e:
  503. raise GooglePhotosException(e.args[0])
  504. def InsertComment(self, photo_or_uri, comment):
  505. """Add a comment to a photo.
  506. Needs authentication, see self.ClientLogin()
  507. Arguments:
  508. photo_or_uri: a gdata.photos.PhotoEntry that is about to be commented
  509. , or a `post' uri pointing to it
  510. (string) comment: The actual comment
  511. Returns:
  512. The new gdata.photos.CommentEntry
  513. Example:
  514. p = GetFeed(PhotoUri)
  515. tag = InsertComment(p, 'OOOH! I would have loved to be there.
  516. Who's that in the back?')
  517. """
  518. comment = gdata.photos.CommentEntry(content=atom.Content(text=comment))
  519. if isinstance(photo_or_uri, (str, unicode)):
  520. post_uri = photo_or_uri # it's a uri
  521. elif hasattr(photo_or_uri, 'GetEditMediaLink'):
  522. post_uri = photo_or_uri.GetPostLink().href
  523. try:
  524. return self.Post(data=comment, uri=post_uri,
  525. converter=gdata.photos.CommentEntryFromString)
  526. except gdata.service.RequestError, e:
  527. raise GooglePhotosException(e.args[0])
  528. def Delete(self, object_or_uri, *args, **kwargs):
  529. """Delete an object.
  530. Re-implementing the GDataService.Delete method, to add some
  531. convenience.
  532. Arguments:
  533. object_or_uri: Any object that has a GetEditLink() method that
  534. returns a link, or a uri to that object.
  535. Returns:
  536. ? or GooglePhotosException on errors
  537. """
  538. try:
  539. uri = object_or_uri.GetEditLink().href
  540. except AttributeError:
  541. uri = object_or_uri
  542. try:
  543. return gdata.service.GDataService.Delete(self, uri, *args, **kwargs)
  544. except gdata.service.RequestError, e:
  545. raise GooglePhotosException(e.args[0])
  546. def GetSmallestThumbnail(media_thumbnail_list):
  547. """Helper function to get the smallest thumbnail of a list of
  548. gdata.media.Thumbnail.
  549. Returns gdata.media.Thumbnail """
  550. r = {}
  551. for thumb in media_thumbnail_list:
  552. r[int(thumb.width)*int(thumb.height)] = thumb
  553. keys = r.keys()
  554. keys.sort()
  555. return r[keys[0]]
  556. def ConvertAtomTimestampToEpoch(timestamp):
  557. """Helper function to convert a timestamp string, for instance
  558. from atom:updated or atom:published, to milliseconds since Unix epoch
  559. (a.k.a. POSIX time).
  560. `2007-07-22T00:45:10.000Z' -> """
  561. return time.mktime(time.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.000Z'))
  562. ## TODO: Timezone aware