Package conduit :: Package modules :: Package iPodModule :: Module iPodModule
[hide private]

Source Code for Module conduit.modules.iPodModule.iPodModule

  1  """ 
  2  Provides a number of dataproviders which are associated with 
  3  removable devices such as USB keys. 
  4   
  5  It also includes classes specific to the ipod.  
  6  This file is not dynamically loaded at runtime in the same 
  7  way as the other dataproviders as it needs to be loaded all the time in 
  8  order to listen to HAL events 
  9   
 10  Copyright: John Stowers, 2006 
 11  License: GPLv2 
 12  """ 
 13  import os 
 14  import pickle 
 15  import logging 
 16  log = logging.getLogger("modules.iPod") 
 17   
 18  import conduit 
 19  import conduit.dataproviders.DataProvider as DataProvider 
 20  import conduit.dataproviders.DataProviderCategory as DataProviderCategory 
 21  import conduit.dataproviders.VolumeFactory as VolumeFactory 
 22  import conduit.utils as Utils 
 23  import conduit.datatypes.Note as Note 
 24  import conduit.datatypes.Contact as Contact 
 25  import conduit.datatypes.Event as Event 
 26  import conduit.datatypes.File as File 
 27   
 28  MODULES = { 
 29          "iPodFactory" :         { "type":   "dataprovider-factory"  } 
 30  } 
 31   
 32  try: 
 33      import gpod 
 34      LIBGPOD_PHOTOS = gpod.version_info >= (0,6,0) 
 35      log.info("Module Information: %s" % Utils.get_module_information(gpod, 'version_info')) 
 36  except ImportError: 
 37      LIBGPOD_PHOTOS = False 
 38      log.info("iPod photo support disabled") 
 39   
40 -def _string_to_unqiue_file(txt, base_uri, prefix, postfix=''):
41 for i in range(1, 10000): 42 filename = prefix + str(i) + postfix 43 uri = os.path.join(base_uri, filename) 44 f = File.File(uri) 45 if not f.exists(): 46 break 47 48 temp = Utils.new_tempfile(txt) 49 temp.transfer(uri, True) 50 temp.set_UID(filename) 51 return temp.get_rid()
52
53 -class iPodFactory(VolumeFactory.VolumeFactory):
54 - def is_interesting(self, udi, props):
55 if props.has_key("info.parent") and props.has_key("info.parent")!="": 56 prop2 = self._get_properties(props["info.parent"]) 57 if prop2.has_key("storage.model") and prop2["storage.model"]=="iPod": 58 return True 59 return False
60
61 - def get_category(self, udi, **kwargs):
62 return DataProviderCategory.DataProviderCategory( 63 kwargs['label'], 64 "multimedia-player-ipod-video-white", 65 kwargs['mount'])
66
67 - def get_dataproviders(self, udi, **kwargs):
68 if LIBGPOD_PHOTOS: 69 #Read information about the ipod, like if it supports 70 #photos or not 71 d = gpod.itdb_device_new() 72 gpod.itdb_device_set_mountpoint(d,kwargs['mount']) 73 supportsPhotos = gpod.itdb_device_supports_photo(d) 74 gpod.itdb_device_free(d) 75 if supportsPhotos: 76 return [IPodNoteTwoWay, IPodContactsTwoWay, IPodCalendarTwoWay, IPodPhotoSink] 77 78 return [IPodNoteTwoWay, IPodContactsTwoWay, IPodCalendarTwoWay]
79 80
81 -class IPodBase(DataProvider.TwoWay):
82 - def __init__(self, *args):
83 DataProvider.TwoWay.__init__(self) 84 self.mountPoint = args[0] 85 self.uid = args[1] 86 self.objects = None
87
88 - def refresh(self):
89 DataProvider.TwoWay.refresh(self) 90 self.objects = [] 91 92 #Also checks directory exists 93 if not os.path.exists(self.dataDir): 94 os.mkdir(self.dataDir) 95 96 #When acting as a source, only notes in the Notes dir are 97 #considered 98 for f in os.listdir(self.dataDir): 99 fullpath = os.path.join(self.dataDir, f) 100 if os.path.isfile(fullpath): 101 self.objects.append(f)
102
103 - def get_all(self):
104 DataProvider.TwoWay.get_all(self) 105 return self.objects
106
107 - def delete(self, LUID):
108 obj = File.File(URI=os.path.join(self.dataDir, LUID)) 109 if obj.exists(): 110 obj.delete()
111
112 - def finish(self, aborted, error, conflict):
113 DataProvider.TwoWay.finish(self) 114 self.objects = None
115
116 - def get_UID(self):
117 return self.uid
118
119 - def _get_unique_filename(self, directory):
120 """ 121 Returns the name of a non-existant file on the 122 ipod within directory 123 124 @param directory: Name of the directory within the device root to make 125 the random file in 126 """ 127 done = False 128 while not done: 129 f = os.path.join(self.mountPoint,directory,Utils.random_string()) 130 if not os.path.exists(f): 131 done = True 132 return f
133
134 -class IPodNoteTwoWay(IPodBase):
135 """ 136 Stores Notes on the iPod. 137 Rather than requiring a perfect transform to and from notes to the 138 ipod note format I also store the original note data in a 139 .conduit directory in the root of the iPod. 140 141 Notes are saved as title.txt and a copy of the raw note is saved as 142 title.note 143 144 LUID is the note title 145 """ 146 147 _name_ = "Notes" 148 _description_ = "Sync your iPod notes" 149 _module_type_ = "twoway" 150 _in_type_ = "note" 151 _out_type_ = "note" 152 _icon_ = "tomboy" 153 154 # datatypes.Note doesn't care about encoding, 155 # lets be naive and assume that all notes are utf-8 156 ENCODING_DECLARATION = '<?xml encoding="utf-8"?>' 157
158 - def __init__(self, *args):
159 IPodBase.__init__(self, *args) 160 161 self.dataDir = os.path.join(self.mountPoint, 'Notes') 162 self.objects = []
163
164 - def _get_shadow_dir(self):
165 shadowDir = os.path.join(self.mountPoint, '.conduit') 166 if not os.path.exists(shadowDir): 167 os.mkdir(shadowDir) 168 return shadowDir
169
170 - def _get_note_from_ipod(self, uid):
171 """ 172 Gets a note from the ipod, If the pickled shadow copy exists 173 then return that 174 """ 175 rawNoteURI = os.path.join(self._get_shadow_dir(),uid) 176 if os.path.exists(rawNoteURI): 177 raw = open(rawNoteURI,'rb') 178 try: 179 n = pickle.load(raw) 180 raw.close() 181 return n 182 except: 183 raw.close() 184 185 noteURI = os.path.join(self.dataDir, uid) 186 noteFile = File.File(URI=noteURI) 187 #get the contents from the note, get the raw from the raw copy. 188 #the UID for notes from the ipod is the filename 189 n = Note.Note( 190 title=uid, 191 contents=noteFile.get_contents_as_text().replace( 192 self.ENCODING_DECLARATION, '', 1), 193 ) 194 n.set_UID(uid) 195 n.set_mtime(noteFile.get_mtime()) 196 n.set_open_URI(noteURI) 197 return n
198
199 - def _save_note_to_ipod(self, uid, note):
200 """ 201 Save a simple iPod note in /Notes 202 If the note has raw then also save that in shadowdir 203 uid is the note title. 204 """ 205 # the normal note viewed by the iPod 206 # inject an encoding declaration if it is missing. 207 contents = note.get_contents() 208 if not self.ENCODING_DECLARATION in contents: 209 contents = ''.join([self.ENCODING_DECLARATION, contents]) 210 ipodnote = Utils.new_tempfile(contents) 211 212 ipodnote.transfer(os.path.join(self.dataDir,uid), overwrite=True) 213 ipodnote.set_mtime(note.get_mtime()) 214 ipodnote.set_UID(uid) 215 216 #the raw pickled note for sync 217 raw = open(os.path.join(self._get_shadow_dir(),uid),'wb') 218 pickle.dump(note, raw, -1) 219 raw.close() 220 221 return ipodnote.get_rid()
222
223 - def _note_exists(self, uid):
224 #Check if both the shadow copy and the ipodified version exists 225 shadowDir = self._get_shadow_dir() 226 return os.path.exists(os.path.join(shadowDir,uid)) and os.path.exists(os.path.join(self.dataDir,uid))
227
228 - def get(self, LUID):
229 DataProvider.TwoWay.get(self, LUID) 230 return self._get_note_from_ipod(LUID)
231
232 - def put(self, note, overwrite, LUID=None):
233 """ 234 The LUID for a note in the iPod is the note title 235 """ 236 DataProvider.TwoWay.put(self, note, overwrite, LUID) 237 238 if LUID != None: 239 #Check if both the shadow copy and the ipodified version exists 240 if self._note_exists(LUID): 241 if overwrite == True: 242 #replace the note 243 log.debug("Replacing Note %s" % LUID) 244 return self._save_note_to_ipod(LUID, note) 245 else: 246 #only overwrite if newer 247 log.warn("OVERWRITE IF NEWER NOT IMPLEMENTED") 248 return self._save_note_to_ipod(LUID, note) 249 250 #make a new note 251 log.warn("CHECK IF EXISTS, COMPARE, SAVE") 252 return self._save_note_to_ipod(note.title, note)
253
254 - def delete(self, LUID):
255 IPodBase.delete(self, LUID) 256 257 raw = File.File(URI=os.path.join(self._get_shadow_dir(), LUID)) 258 if raw.exists(): 259 raw.delete()
260
261 -class IPodContactsTwoWay(IPodBase):
262 263 _name_ = "Contacts" 264 _description_ = "Sync your iPod contacts" 265 _module_type_ = "twoway" 266 _in_type_ = "contact" 267 _out_type_ = "contact" 268 _icon_ = "contact-new" 269
270 - def __init__(self, *args):
271 IPodBase.__init__(self, *args) 272 self.dataDir = os.path.join(self.mountPoint, 'Contacts')
273
274 - def get(self, LUID):
275 DataProvider.TwoWay.get(self, LUID) 276 fullpath = os.path.join(self.dataDir, LUID) 277 f = File.File(URI=fullpath) 278 279 contact = Contact.Contact() 280 contact.set_from_vcard_string(f.get_contents_as_text()) 281 contact.set_open_URI(fullpath) 282 contact.set_mtime(f.get_mtime()) 283 contact.set_UID(LUID) 284 return contact
285
286 - def put(self, contact, overwrite, LUID=None):
287 DataProvider.TwoWay.put(self, contact, overwrite, LUID) 288 289 if LUID != None: 290 f = Utils.new_tempfile(contact.get_vcard_string()) 291 f.transfer(os.path.join(self.dataDir, LUID), overwrite=True) 292 f.set_UID(LUID) 293 return f.get_rid() 294 295 return _string_to_unqiue_file(contact.get_vcard_string(), self.dataDir, 'contact')
296
297 -class IPodCalendarTwoWay(IPodBase):
298 299 _name_ = "Calendar" 300 _description_ = "Sync your iPod calendar" 301 _module_type_ = "twoway" 302 _in_type_ = "event" 303 _out_type_ = "event" 304 _icon_ = "contact-new" 305
306 - def __init__(self, *args):
307 IPodBase.__init__(self, *args) 308 self.dataDir = os.path.join(self.mountPoint, 'Calendars')
309
310 - def get(self, LUID):
311 DataProvider.TwoWay.get(self, LUID) 312 fullpath = os.path.join(self.dataDir, LUID) 313 f = File.File(URI=fullpath) 314 315 event = Event.Event() 316 event.set_from_ical_string(f.get_contents_as_text()) 317 event.set_open_URI(fullpath) 318 event.set_mtime(f.get_mtime()) 319 event.set_UID(LUID) 320 return event
321
322 - def put(self, event, overwrite, LUID=None):
323 DataProvider.TwoWay.put(self, event, overwrite, LUID) 324 325 if LUID != None: 326 f = Utils.new_tempfile(event.get_ical_string()) 327 f.transfer(os.path.join(self.dataDir, LUID), overwrite=True) 328 f.set_UID(LUID) 329 return f.get_rid() 330 331 return _string_to_unqiue_file(event.get_ical_string(), self.dataDir, 'event')
332
333 -class IPodPhotoSink(IPodBase):
334 335 _name_ = "Photos" 336 _description_ = "Sync your iPod photos" 337 _module_type_ = "sink" 338 _in_type_ = "file/photo" 339 _out_type_ = "file/photo" 340 _icon_ = "image-x-generic" 341 _configurable_ = True 342 343 SAFE_PHOTO_ALBUM = "Photo Library" 344
345 - def __init__(self, *args):
346 IPodBase.__init__(self, *args) 347 self.db = gpod.PhotoDatabase(self.mountPoint) 348 self.albumName = "Conduit" 349 self.album = None
350
351 - def _set_sysinfo(self, modelnumstr, model):
352 gpod.itdb_device_set_sysinfo(self.db._itdb.device, modelnumstr, model)
353
354 - def _get_photo_album(self, albumName):
355 for album in self.db.PhotoAlbums: 356 if album.name == albumName: 357 log.debug("Found album: %s" % albumName) 358 return album 359 360 log.debug("Creating album: %s" % albumName) 361 return self._create_photo_album(albumName)
362
363 - def _create_photo_album(self, albumName):
364 if albumName in [a.name for a in self.db.PhotoAlbums]: 365 log.warn("Album already exists: %s" % albumName) 366 album = self._get_photo_album(albumName) 367 else: 368 album = self.db.new_PhotoAlbum(title=albumName) 369 return album
370
371 - def _get_photo_by_id(self, id):
372 for album in self.db.PhotoAlbums: 373 for photo in album: 374 if str(photo['id']) == str(id): 375 return photo 376 return None
377
378 - def _delete_album(self, albumName):
379 if albumName == self.SAFE_PHOTO_ALBUM: 380 log.warn("Cannot delete album: %s" % self.SAFE_PHOTO_ALBUM) 381 else: 382 album = self._get_photo_album(albumName) 383 for photo in album[:]: 384 album.remove(photo) 385 self.db.remove(album)
386
387 - def _empty_all_photos(self):
388 for photo in self.db.PhotoAlbums[0][:]: 389 self.db.remove(photo)
390
391 - def _get_photo_albums(self):
392 i = [] 393 for album in self.db.PhotoAlbums: 394 i.append(album.name) 395 return i
396
397 - def refresh(self):
398 DataProvider.TwoWay.refresh(self) 399 self.album = self._get_photo_album(self.albumName)
400
401 - def get_all(self):
402 uids = [] 403 for photo in self.album: 404 uids.append(str(photo['id'])) 405 return uids
406
407 - def put(self, f, overwrite, LUID=None):
408 photo = self.db.new_Photo(filename=f.get_local_uri()) 409 self.album.add(photo) 410 gpod.itdb_photodb_write(self.db._itdb, None) 411 return conduit.datatypes.Rid(str(photo['id']), None, hash(None))
412
413 - def delete(self, LUID):
414 photo = self._get_photo_by_id(LUID) 415 if photo != None: 416 self.db.remove(photo) 417 gpod.itdb_photodb_write(self.db._itdb, None)
418
419 - def configure(self, window):
420 import gobject 421 import gtk 422 def build_album_model(albumCombo): 423 self.album_store.clear() 424 album_count = 0 425 album_iter = None 426 for name in self._get_photo_albums(): 427 iter = self.album_store.append((name,)) 428 if name == self.albumName: 429 album_iter = iter 430 album_count += 1 431 432 if album_iter: 433 albumCombo.set_active_iter(album_iter) 434 elif self.albumName: 435 albumCombo.child.set_text(self.albumName) 436 elif album_count: 437 albumCombo.set_active(0)
438 439 def delete_click(sender, albumCombo): 440 albumName = albumCombo.get_active_text() 441 if albumName: 442 self._delete_album(albumName) 443 build_album_model(albumCombo) 444 445 #get a whole bunch of widgets 446 tree = Utils.dataprovider_glade_get_widget( 447 __file__, 448 "config.glade", 449 "PhotoConfigDialog") 450 albumCombo = tree.get_widget("album_combobox") 451 delete_button = tree.get_widget("delete_button") 452 453 #setup album store 454 self.album_store = gtk.ListStore(gobject.TYPE_STRING) 455 albumCombo.set_model(self.album_store) 456 cell = gtk.CellRendererText() 457 albumCombo.pack_start(cell, True) 458 albumCombo.set_text_column(0) 459 460 #setup widgets 461 build_album_model(albumCombo) 462 delete_button.connect('clicked', delete_click, albumCombo) 463 464 # run dialog 465 dlg = tree.get_widget("PhotoConfigDialog") 466 response = Utils.run_dialog(dlg, window) 467 468 if response == True: 469 #get the values from the widgets 470 self.albumName = albumCombo.get_active_text() 471 dlg.destroy() 472 473 del self.album_store 474
475 - def is_configured (self, isSource, isTwoWay):
476 return len(self.albumName) > 0
477
478 - def uninitialize(self):
479 self.db.close()
480