ecasa: add download functionality and basic slideshow.
[enigma2-plugins.git] / ecasa / src / PicasaApi.py
1 from __future__ import print_function
2
3 #pragma mark - Picasa API
4
5 import gdata.photos.service
6 import gdata.media
7 import gdata.geo
8 import os
9 import shutil
10
11 from twisted.internet import reactor
12 from twisted.internet.defer import Deferred
13 from twisted.web.client import downloadPage
14
15 def list_recursive(dirname):
16         for file in os.listdir(dirname):
17                 fn = os.path.join(dirname, file)
18                 if os.path.isfile(fn):
19                         yield fn
20                 elif os.path.isdir(fn):
21                         for f in list_recursive(fn):
22                                 yield f
23
24 def remove_empty(dirname):
25         files = os.listdir(dirname)
26         if files:
27                 for file in os.listdir(dirname):
28                         fn = os.path.join(dirname, file)
29                         if os.path.isdir(fn):
30                                 remove_empty(fn)
31         else:
32                 try:
33                         os.rmdir(dirname)
34                 except OSError as ose:
35                         print("Unable to remove directory", dirname + ":", ose)
36
37 #_PicasaApi__returnPhotos = lambda photos: [(photo.title.text, photo) for photo in photos.entry]
38 _PicasaApi__returnPhotos = lambda photos: photos.entry
39
40 class PicasaApi:
41         """Wrapper around gdata/picasa API to make our life a little easier."""
42         def __init__(self, email=None, password=None, cache='/tmp/ecasa'):
43                 """Initialize API, login to google servers"""
44                 gd_client = gdata.photos.service.PhotosService()
45                 gd_client.source = 'enigma2-plugin-extensions-ecasa'
46                 if email and password:
47                         gd_client.email = email
48                         gd_client.password = password
49                         # NOTE: this might fail
50                         gd_client.ProgrammaticLogin()
51
52                 self.gd_client = gd_client
53                 self.cache = cache
54
55         def setCredentials(self, email, password):
56                 # TODO: check if this is sane
57                 gd_client = self.gd_client
58                 gd_client.email = email
59                 gd_client.password = password
60                 if email and password:
61                         # NOTE: this might fail
62                         gd_client.ProgrammaticLogin()
63
64         def getAlbums(self, user='default'):
65                 albums = self.gd_client.GetUserFeed(user=user)
66                 return [(album.title.text, album.numphotos.text, album) for album in albums.entry]
67
68         def getSearch(self, query, limit='10'):
69                 photos = self.gd_client.SearchCommunityPhotos(query, limit=str(limit))
70                 return __returnPhotos(photos)
71
72         def getAlbum(self, album):
73                 photos = self.gd_client.GetFeed(album.GetPhotosUri())
74                 return __returnPhotos(photos)
75
76         def getTags(self, feed):
77                 tags = self.gd_client.GetFeed(feed.GetTagsUri())
78                 return [(tag.summary.text, tag) for tag in tags.entry]
79
80         def getComments(self, feed):
81                 comments = self.gd_client.GetCommentFeed(feed.GetCommentsUri())
82                 return [(comment.summary.text, comment) for comment in comments.entry]
83
84         def getFeatured(self):
85                 featured = self.gd_client.GetFeed('/data/feed/base/featured')
86                 return __returnPhotos(featured)
87
88         def downloadPhoto(self, photo, thumbnail=False):
89                 if not photo: return
90
91                 cache = os.path.join(self.cache, 'thumb', photo.albumid.text) if thumbnail else os.path.join(self.cache, photo.albumid.text)
92                 try: os.makedirs(cache)
93                 except OSError: pass
94
95                 url = photo.media.thumbnail[0].url if thumbnail else photo.media.content[0].url
96                 filename = url.split('/')[-1]
97                 fullname = os.path.join(cache, filename)
98                 d = Deferred()
99                 # file exists, assume it's valid...
100                 if os.path.exists(fullname):
101                         reactor.callLater(0, d.callback, (fullname, photo))
102                 else:
103                         downloadPage(url, fullname).addCallbacks(
104                                 lambda value:d.callback((fullname, photo)),
105                                 lambda error:d.errback((error, photo)))
106                 return d
107
108         def downloadThumbnail(self, photo):
109                 return self.downloadPhoto(photo, thumbnail=True)
110
111         def copyPhoto(self, photo, target, recursive=True):
112                 """Attempt to copy photo from cache to given destination.
113
114                 Arguments:
115                 photo: photo object to download.
116                 target: target filename
117                 recursive (optional): attempt to download picture if it does not exist yet
118
119                 Returns:
120                 True if image was copied successfully,
121                 False if image did not exist and download was initiated,
122                 otherwise None.
123
124                 Raises:
125                 shutil.Error if an error occured during moving the file.
126                 """
127                 if not photo: return
128
129                 cache = os.path.join(self.cache, photo.albumid.text)
130                 filename = photo.media.content[0].url.split('/')[-1]
131                 fullname = os.path.join(cache, filename)
132
133                 # file exists, assume it's valid...
134                 if os.path.exists(fullname):
135                         shutil.copy(fullname, target)
136                         return True
137                 else:
138                         print("[PicasaApi] Photo does not exist in cache, trying to download with deferred copy operation")
139                         self.downloadPhoto(photo).addCallback(
140                                 lambda value:self.copyPhoto(photo, target, recursive=False)
141                         )
142                         return False
143
144         def cleanupCache(self, maxSize):
145                 """Housekeeping for our download cache.
146
147                 Removes pictures and thumbnails (oldest to newest) until the cache is smaller than maxSize MB.
148
149                 Arguments:
150                 maxSize: maximum size of cache im MB.
151                 """
152                 stat = os.stat
153                 maxSize *= 1048576 # input size is assumed to be in mb, but we work with bytes internally
154
155                 files = [(f, stat(f)) for f in list_recursive(self.cache)]
156                 curSize = sum(map(lambda x: x[1].st_size, files))
157                 if curSize > maxSize:
158                         files.sort(key=lambda x: x[1].st_mtime)
159                         while curSize > maxSize:
160                                 file, stat = files.pop(0)
161                                 try:
162                                         os.unlink(file)
163                                 except Exception as e:
164                                         print("[PicasaApi] Unable to unlink file", file + ":", e)
165                                 else:
166                                         curSize -= stat.st_size
167                         remove_empty(self.cache)
168
169 __all__ = ['PicasaApi']