1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 # sync.py -- Device synchronization
22 # Thomas Perl <thp@perli.net> 2007-12-06
23 # based on libipodsync.py (2006-04-05 Thomas Perl)
26 from gpodder
import util
27 from gpodder
import services
28 from gpodder
import libconverter
29 from gpodder
import libtagupdate
31 from gpodder
.liblogger
import log
32 from gpodder
.libgpodder
import gl
33 from gpodder
.dbsqlite
import db
42 gpod_available
= False
43 log('(gpodder.sync) Could not find gpod')
45 pymtp_available
= True
49 pymtp_available
= False
50 log('(gpodder.sync) Could not find pymtp.')
55 log('(gpodder.sync) Could not find pymad')
60 log( '(gpodder.sync) Could not find eyeD3')
65 log('(gpodder.sync) Could not find Python Imaging Library (PIL)')
79 device_type
= gl
.config
.device_type
80 if device_type
== 'ipod':
82 elif device_type
== 'filesystem':
83 return MP3PlayerDevice()
84 elif device_type
== 'mtp':
89 def get_track_length(filename
):
90 if util
.find_command('mplayer') is not None:
92 mplayer_output
= os
.popen('mplayer -msglevel all=-1 -identify -vo null -ao null -frames 0 "%s" 2>/dev/null' % filename
).read()
93 return int(float(mplayer_output
[mplayer_output
.index('ID_LENGTH'):].splitlines()[0][10:])*1000)
97 log('Please install MPlayer for track length detection.')
100 mad_info
= mad
.MadFile(filename
)
101 return int(mad_info
.total_time())
106 eyed3_info
= eyeD3
.Mp3AudioFile(filename
)
107 return int(eyed3_info
.getPlayTime()*1000)
111 return int(60*60*1000*3) # Default is three hours (to be on the safe side)
114 class SyncTrack(object):
116 This represents a track that is on a device. You need
117 to specify at least the following keyword arguments,
118 because these will be used to display the track in the
119 GUI. All other keyword arguments are optional and can
120 be used to reference internal objects, etc... See the
121 iPod synchronization code for examples.
123 Keyword arguments needed:
124 playcount (How often has the track been played?)
125 podcast (Which podcast is this track from? Or: Folder name)
126 released (The release date of the episode)
128 If any of these fields is unknown, it should not be
129 passed to the function (the values will default to None
130 for all required fields).
132 def __init__(self
, title
, length
, modified
, **kwargs
):
135 self
.filesize
= util
.format_filesize(length
, gl
.config
.use_si_units
)
136 self
.modified
= modified
138 # Set some (possible) keyword arguments to default values
139 self
.playcount
= None
143 # Convert keyword arguments to object attributes
144 self
.__dict
__.update(kwargs
)
147 class Device(services
.ObservableService
):
149 self
.cancelled
= False
150 self
.allowed_types
= ['audio', 'video']
152 self
.tracks_list
= []
153 signals
= ['progress', 'sub-progress', 'status', 'done', 'post-done']
154 services
.ObservableService
.__init
__(self
, signals
)
160 self
.cancelled
= True
161 self
.notify('status', _('Cancelled by user'))
164 self
.notify('status', _('Writing data to disk'))
165 successful_sync
= not os
.system('sync')
167 self
.notify('post-done', self
, successful_sync
)
170 def add_tracks(self
, tracklist
=[], force_played
=False):
171 for id, track
in enumerate(tracklist
):
175 self
.notify('progress', id+1, len(tracklist
))
177 if not track
.was_downloaded(and_exists
=True):
180 if track
.is_played
and gl
.config
.only_sync_not_played
and not force_played
:
183 if track
.file_type() not in self
.allowed_types
:
186 added
= self
.add_track(track
)
188 if gl
.config
.on_sync_mark_played
:
189 log('Marking as played on transfer: %s', track
.url
, sender
=self
)
190 db
.mark_episode(track
.url
, is_played
=True)
192 if added
and gl
.config
.on_sync_delete
:
193 log('Removing episode after transfer: %s', track
.url
, sender
=self
)
194 track
.delete_from_disk()
197 def remove_tracks(self
, tracklist
=[]):
198 for id, track
in enumerate(tracklist
):
201 self
.notify('progress', id, len(tracklist
))
202 self
.remove_track(track
)
205 def get_all_tracks(self
):
208 def add_track(self
, track
):
211 def remove_track(self
, track
):
214 def get_free_space(self
):
217 def episode_on_device(self
, episode
):
218 return self
._track
_on
_device
(episode
.title
)
220 def _track_on_device( self
, track_name
):
221 for t
in self
.tracks_list
:
222 if track_name
== t
.title
:
226 class iPodDevice(Device
):
228 Device
.__init
__(self
)
230 self
.mountpoint
= str(gl
.config
.ipod_mount
)
233 self
.podcast_playlist
= None
236 def get_free_space(self
):
237 # Reserve 10 MiB for iTunesDB writing (to be on the safe side)
238 RESERVED_FOR_ITDB
= 1024*1024*10
239 return util
.get_free_disk_space(self
.mountpoint
) - RESERVED_FOR_ITDB
243 if not gpod_available
or not os
.path
.isdir(self
.mountpoint
):
246 self
.notify('status', _('Opening iPod database'))
247 self
.itdb
= gpod
.itdb_parse(self
.mountpoint
, None)
248 if self
.itdb
is None:
251 self
.itdb
.mountpoint
= self
.mountpoint
252 self
.podcasts_playlist
= gpod
.itdb_playlist_podcasts(self
.itdb
)
254 if self
.podcasts_playlist
:
255 self
.notify('status', _('iPod opened'))
257 # build the initial tracks_list
258 self
.tracks_list
= self
.get_all_tracks()
265 if self
.itdb
is not None:
266 self
.notify('status', _('Saving iPod database'))
267 gpod
.itdb_write(self
.itdb
, None)
270 if gl
.config
.ipod_write_gtkpod_extended
:
271 # Fix up iTunesDB.ext (gtkpod extended database),
272 # so gtkpod will not complain about a wrong sha1sum
273 self
.notify('status', _('Writing extended gtkpod database'))
274 ext_filename
= os
.path
.join(self
.mountpoint
, 'iPod_Control', 'iTunes', 'iTunesDB.ext')
275 idb_filename
= os
.path
.join(self
.mountpoint
, 'iPod_Control', 'iTunes', 'iTunesDB')
276 if os
.path
.exists(ext_filename
) and os
.path
.exists(idb_filename
):
278 db
= gpod
.ipod
.Database(self
.mountpoint
)
279 gpod
.gtkpod
.parse(ext_filename
, db
, idb_filename
)
280 gpod
.gtkpod
.write(ext_filename
, db
, idb_filename
)
283 log('Error when writing iTunesDB.ext', sender
=self
, traceback
=True)
285 log('I could not find %s or %s. Will not update extended gtkpod DB.', ext_filename
, idb_filename
, sender
=self
)
287 log('Not writing extended gtkpod DB. Set "ipod_write_gpod_extended" to True if I should write it.', sender
=self
)
292 def get_all_tracks(self
):
294 for track
in gpod
.sw_get_playlist_tracks(self
.podcasts_playlist
):
295 filename
= gpod
.itdb_filename_on_ipod(track
)
296 length
= util
.calculate_size(filename
)
298 age_in_days
= util
.file_age_in_days(filename
)
299 modified
= util
.file_age_to_string(age_in_days
)
300 released
= gpod
.itdb_time_mac_to_host(track
.time_released
)
301 released
= util
.format_date(released
)
303 t
= SyncTrack(track
.title
, length
, modified
, libgpodtrack
=track
, playcount
=track
.playcount
, released
=released
, podcast
=track
.artist
)
307 def remove_track(self
, track
):
308 self
.notify('status', _('Removing %s') % track
.title
)
309 track
= track
.libgpodtrack
310 filename
= gpod
.itdb_filename_on_ipod(track
)
313 gpod
.itdb_playlist_remove_track(self
.podcasts_playlist
, track
)
315 log('Track %s not in playlist', track
.title
, sender
=self
)
317 gpod
.itdb_track_unlink(track
)
318 util
.delete_file(filename
)
320 def add_track(self
, episode
):
321 self
.notify('status', _('Adding %s') % episode
.title
)
322 for track
in gpod
.sw_get_playlist_tracks(self
.podcasts_playlist
):
323 if episode
.url
== track
.podcasturl
:
324 if track
.playcount
> 0:
325 db
.mark_episode(track
.podcasturl
, is_played
=True)
326 # Mark as played on iPod if played locally (and set podcast flags)
327 self
.set_podcast_flags(track
)
330 original_filename
= str(episode
.local_filename())
331 local_filename
= original_filename
333 if util
.calculate_size(original_filename
) > self
.get_free_space():
334 log('Not enough space on %s, sync aborted...', self
.mountpoint
, sender
= self
)
335 self
.errors
.append( _('Error copying %s: Not enough free disk space on %s') % (episode
.title
, self
.mountpoint
))
336 self
.cancelled
= True
339 (fn
, extension
) = os
.path
.splitext(original_filename
)
340 if libconverter
.converters
.has_converter(extension
):
341 log('Converting: %s', original_filename
, sender
=self
)
342 callback_status
= lambda percentage
: self
.notify('sub-progress', int(percentage
))
343 local_filename
= libconverter
.converters
.convert(original_filename
, callback
=callback_status
)
345 if not libtagupdate
.update_metadata_on_file(local_filename
, title
=episode
.title
, artist
=episode
.channel
.title
):
346 log('Could not set metadata on converted file %s', local_filename
, sender
=self
)
348 if local_filename
is None:
349 log('Cannot convert %s', original_filename
, sender
=self
)
352 local_filename
= str(local_filename
)
354 (fn
, extension
) = os
.path
.splitext(local_filename
)
355 if extension
.lower().endswith('ogg'):
356 log('Cannot copy .ogg files to iPod.', sender
=self
)
359 track
= gpod
.itdb_track_new()
361 # Add release time to track if pubDate has a valid value
362 if episode
.pubDate
> 0:
364 # libgpod>= 0.5.x uses a new timestamp format
365 track
.time_released
= gpod
.itdb_time_host_to_mac(int(episode
.pubDate
))
367 # old (pre-0.5.x) libgpod versions expect mactime, so
368 # we're going to manually build a good mactime timestamp here :)
370 # + 2082844800 for unixtime => mactime (1970 => 1904)
371 track
.time_released
= int(episode
.pubDate
+ 2082844800)
373 track
.title
= str(episode
.title
)
374 track
.album
= str(episode
.channel
.title
)
375 track
.artist
= str(episode
.channel
.title
)
376 track
.description
= str(util
.remove_html_tags(episode
.description
))
378 track
.podcasturl
= str(episode
.url
)
379 track
.podcastrss
= str(episode
.channel
.url
)
381 track
.tracklen
= get_track_length(local_filename
)
382 track
.size
= os
.path
.getsize(local_filename
)
384 if episode
.file_type() == 'audio':
385 track
.filetype
= 'mp3'
386 track
.mediatype
= 0x00000004
387 elif episode
.file_type() == 'video':
388 track
.filetype
= 'm4v'
389 track
.mediatype
= 0x00000006
391 self
.set_podcast_flags(track
)
392 self
.set_cover_art(track
, local_filename
)
394 gpod
.itdb_track_add(self
.itdb
, track
, -1)
395 gpod
.itdb_playlist_add_track(self
.podcasts_playlist
, track
, -1)
396 gpod
.itdb_cp_track_to_ipod( track
, local_filename
, None)
398 # If the file has been converted, delete the temporary file here
399 if local_filename
!= original_filename
:
400 util
.delete_file(local_filename
)
404 def set_podcast_flags(self
, track
):
406 # Set blue bullet for unplayed tracks on 5G iPods
407 episode
= db
.load_episode(track
.podcasturl
)
408 if episode
['is_played']:
409 track
.mark_unplayed
= 0x01
410 if track
.playcount
== 0:
413 track
.mark_unplayed
= 0x02
415 # Set several flags for to podcast values
416 track
.remember_playback_position
= 0x01
422 log('Seems like your python-gpod is out-of-date.', sender
=self
)
424 def set_cover_art(self
, track
, local_filename
):
427 if tag
.link(local_filename
):
428 if 'APIC' in tag
.frames
and len(tag
.frames
['APIC']) > 0:
429 apic
= tag
.frames
['APIC'][0]
432 if apic
.mimeType
== 'image/png':
434 cover_filename
= '%s.cover.%s' (local_filename
, extension
)
436 cover_file
= open(cover_filename
, 'w')
437 cover_file
.write(apic
.imageData
)
440 gpod
.itdb_track_set_thumbnails(track
, cover_filename
)
443 log('Error getting cover using eyeD3', sender
=self
)
446 cover_filename
= os
.path
.join(os
.path
.dirname(local_filename
), 'cover')
447 if os
.path
.isfile(cover_filename
):
448 gpod
.itdb_track_set_thumbnails(track
, cover_filename
)
451 log('Error getting cover using channel cover', sender
=self
)
456 class MP3PlayerDevice(Device
):
457 # if different players use other filenames besides
458 # .scrobbler.log, add them to this list
459 scrobbler_log_filenames
= ['.scrobbler.log']
461 # This is the maximum length of a file name that is
462 # created on the MP3 player, because FAT32 has a
463 # 255-character limit for the whole path
464 MAX_FILENAME_LENGTH
= gl
.config
.mp3_player_max_filename_length
467 Device
.__init
__(self
)
468 self
.destination
= gl
.config
.mp3_player_folder
469 self
.buffer_size
= 1024*1024 # 1 MiB
470 self
.scrobbler_log
= []
472 def get_free_space(self
):
473 return util
.get_free_disk_space(self
.destination
)
477 self
.notify('status', _('Opening MP3 player'))
478 if util
.directory_is_writable(self
.destination
):
479 self
.notify('status', _('MP3 player opened'))
480 # build the initial tracks_list
481 self
.tracks_list
= self
.get_all_tracks()
482 if gl
.config
.mp3_player_use_scrobbler_log
:
483 mp3_player_mount_point
= util
.find_mount_point(self
.destination
)
484 # If a moint point cannot be found look inside self.destination for scrobbler_log_filenames
485 # this prevents us from os.walk()'ing the entire / filesystem
486 if mp3_player_mount_point
== '/':
487 mp3_player_mount_point
= self
.destination
488 log_location
= self
.find_scrobbler_log(mp3_player_mount_point
)
489 if log_location
is not None and self
.load_audioscrobbler_log(log_location
):
490 log('Using Audioscrobbler log data to mark tracks as played', sender
=self
)
495 def add_track(self
, episode
):
496 self
.notify('status', _('Adding %s') % episode
.title
)
498 if gl
.config
.fssync_channel_subfolders
:
499 # Add channel title as subfolder
500 folder
= episode
.channel
.title
501 # Clean up the folder name for use on limited devices
502 folder
= util
.sanitize_filename(folder
, self
.MAX_FILENAME_LENGTH
)
503 folder
= os
.path
.join(self
.destination
, folder
)
505 folder
= self
.destination
507 from_file
= util
.sanitize_encoding(episode
.local_filename())
508 filename_base
= util
.sanitize_filename(episode
.sync_filename(), self
.MAX_FILENAME_LENGTH
)
510 to_file
= filename_base
+ os
.path
.splitext(from_file
)[1].lower()
512 # dirty workaround: on bad (empty) episode titles,
513 # we simply use the from_file basename
514 # (please, podcast authors, FIX YOUR RSS FEEDS!)
515 if os
.path
.splitext(to_file
)[0] == '':
516 to_file
= os
.path
.basename(from_file
)
518 to_file
= os
.path
.join(folder
, to_file
)
520 if not os
.path
.exists(folder
):
524 log('Cannot create folder on MP3 player: %s', folder
, sender
=self
)
527 if (gl
.config
.mp3_player_use_scrobbler_log
and not episode
.is_played
528 and [episode
.channel
.title
, episode
.title
] in self
.scrobbler_log
):
529 log('Marking "%s" from "%s" as played', episode
.title
, episode
.channel
.title
, sender
=self
)
530 db
.mark_episode(episode
.url
, is_played
=True)
532 if gl
.config
.rockbox_copy_coverart
and not os
.path
.exists(os
.path
.join(folder
, 'cover.bmp')):
533 log('Creating Rockbox album art for "%s"', episode
.channel
.title
, sender
=self
)
534 self
.copy_rockbox_cover_art(folder
, from_file
)
536 if not os
.path
.exists(to_file
):
537 log('Copying %s => %s', os
.path
.basename(from_file
), to_file
.decode(util
.encoding
), sender
=self
)
538 return self
.copy_file_progress(from_file
, to_file
)
542 def copy_file_progress(self
, from_file
, to_file
):
544 out_file
= open(to_file
, 'wb')
545 except IOError, ioerror
:
546 self
.errors
.append(_('Error opening %s: %s') % (ioerror
.filename
, ioerror
.strerror
))
551 in_file
= open(from_file
, 'rb')
552 except IOError, ioerror
:
553 self
.errors
.append(_('Error opening %s: %s') % (ioerror
.filename
, ioerror
.strerror
))
558 bytes
= in_file
.tell()
562 s
= in_file
.read(self
.buffer_size
)
567 except IOError, ioerror
:
568 self
.errors
.append(ioerror
.strerror
)
574 log('Trying to remove partially copied file: %s' % to_file
, sender
=self
)
576 log('Yeah! Unlinked %s at least..' % to_file
, sender
=self
)
578 log('Error while trying to unlink %s. OH MY!' % to_file
, sender
=self
)
581 self
.notify('sub-progress', int(min(100, 100*float(bytes_read
)/float(bytes
))))
582 s
= in_file
.read(self
.buffer_size
)
588 def get_all_tracks(self
):
591 if gl
.config
.fssync_channel_subfolders
:
592 files
= glob
.glob(os
.path
.join(self
.destination
, '*', '*'))
594 files
= glob
.glob(os
.path
.join(self
.destination
, '*'))
596 for filename
in files
:
597 (title
, extension
) = os
.path
.splitext(os
.path
.basename(filename
))
598 length
= util
.calculate_size(filename
)
600 age_in_days
= util
.file_age_in_days(filename
)
601 modified
= util
.file_age_to_string(age_in_days
)
602 if gl
.config
.fssync_channel_subfolders
:
603 podcast_name
= os
.path
.basename(os
.path
.dirname(filename
))
607 t
= SyncTrack(title
, length
, modified
, filename
=filename
, podcast
=podcast_name
)
611 def episode_on_device(self
, episode
):
612 e
= util
.sanitize_filename(episode
.sync_filename(), gl
.config
.mp3_player_max_filename_length
)
613 return self
._track
_on
_device
(e
)
615 def remove_track(self
, track
):
616 self
.notify('status', _('Removing %s') % track
.title
)
617 util
.delete_file(track
.filename
)
618 directory
= os
.path
.dirname(track
.filename
)
619 if self
.directory_is_empty(directory
) and gl
.config
.fssync_channel_subfolders
:
623 log('Cannot remove %s', directory
, sender
=self
)
625 def directory_is_empty(self
, directory
):
626 files
= glob
.glob(os
.path
.join(directory
, '*'))
627 dotfiles
= glob
.glob(os
.path
.join(directory
, '.*'))
628 return len(files
+dotfiles
) == 0
630 def find_scrobbler_log(self
, mount_point
):
631 """ find an audioscrobbler log file from log_filenames in the mount_point dir """
632 for dirpath
, dirnames
, filenames
in os
.walk(mount_point
):
633 for log_file
in self
.scrobbler_log_filenames
:
634 filename
= os
.path
.join(dirpath
, log_file
)
635 if os
.path
.isfile(filename
):
638 # No scrobbler log on that device
641 def copy_rockbox_cover_art(self
, destination
, local_filename
):
643 Try to copy the channel cover to "cover.bmp" in the podcast
644 folder on the MP3 player. This makes Rockbox (rockbox.org) display
645 the cover art in its interface.
647 You need the Python Imaging Library (PIL) installed to be able to
648 convert the cover file to a Bitmap file, which Rockbox needs.
651 cover_loc
= os
.path
.join(os
.path
.dirname(local_filename
), 'cover')
652 cover_dst
= os
.path
.join(destination
, 'cover.bmp')
653 if os
.path
.isfile(cover_loc
):
654 size
= (gl
.config
.rockbox_coverart_size
, gl
.config
.rockbox_coverart_size
)
656 cover
= Image
.open(cover_loc
)
657 cover
.thumbnail(size
)
658 cover
.save(cover_dst
, 'BMP')
660 log('Cannot create %s (PIL?)', cover_dst
, traceback
=True, sender
=self
)
663 log('No cover available to set as Rockbox cover', sender
=self
)
666 log('Error getting cover using channel cover', sender
=self
)
670 def load_audioscrobbler_log(self
, log_file
):
671 """ Retrive track title and artist info for all the entries
672 in an audioscrobbler portable player format logfile
673 http://www.audioscrobbler.net/wiki/Portable_Player_Logging """
675 log('Opening "%s" as AudioScrobbler log.', log_file
, sender
=self
)
676 f
= open(log_file
, 'r')
677 entries
= f
.readlines()
679 except IOError, ioerror
:
680 log('Error: "%s" cannot be read.', log_file
, sender
=self
)
684 # regex that can be used to get all the data from a scrobbler.log entry
685 entry_re
= re
.compile('^(.*)\t(.*)\t(.*)\t(.*)\t(.*)\t(.*)\t(.*)$')
686 for entry
in entries
:
687 match_obj
= re
.match(entry_re
, entry
)
688 # L means at least 50% of the track was listened to (S means < 50%)
689 if match_obj
and match_obj
.group(6).strip().lower() == 'l':
690 # append [artist_name, track_name]
691 self
.scrobbler_log
.append([match_obj
.group(1), match_obj
.group(3)])
693 log('Error while parsing "%s".', log_file
, sender
=self
)
697 class MTPDevice(Device
):
699 Device
.__init
__(self
)
700 self
.__model
_name
= None
701 self
.__MTPDevice
= pymtp
.MTP()
703 def __callback(self
, sent
, total
):
706 percentage
= round(float(sent
)/float(total
)*100)
707 text
= ('%i%%' % percentage
)
708 self
.notify('progress', sent
, total
, text
)
710 def __date_to_mtp(self
, date
):
712 this function format the given date and time to a string representation
713 according to MTP specifications: YYYYMMDDThhmmss.s
716 the string representation od the given date
721 d
= time
.gmtime(date
)
722 return time
.strftime("%Y%m%d-%H%M%S.0Z", d
)
723 except Exception, exc
:
724 log('ERROR: An error has happend while trying to convert date to an mtp string (%s)', exc
, sender
=self
)
727 def __mtp_to_date(self
, mtp
):
729 this parse the mtp's string representation for date
730 according to specifications (YYYYMMDDThhmmss.s) to
738 mtp
= mtp
.replace(" ", "0") # replace blank with 0 to fix some invalid string
739 d
= time
.strptime(mtp
[:8] + mtp
[9:13],"%Y%m%d%H%M%S")
740 _date
= calendar
.timegm(d
)
742 # TIME ZONE SHIFTING: the string contains a hour/min shift relative to a time zone
744 shift_direction
=mtp
[15]
745 hour_shift
= int(mtp
[16:18])
746 minute_shift
= int(mtp
[18:20])
747 shift_in_sec
= hour_shift
* 3600 + minute_shift
* 60
748 if shift_direction
== "+":
749 _date
+= shift_in_sec
750 elif shift_direction
== "-":
751 _date
-= shift_in_sec
753 raise ValueError("Expected + or -")
754 except Exception, exc
:
755 log('WARNING: ignoring invalid time zone information for %s (%s)', mtp
, exc
, sender
=self
)
756 return max( 0, _date
)
757 except Exception, exc
:
758 log('WARNING: the mtp date "%s" can not be parsed against mtp specification (%s)', mtp
, exc
, sender
=self
)
763 this function try to find a nice name for the device.
764 First, it tries to find a friendly (user assigned) name
765 (this name can be set by other application and is stored on the device).
766 if no friendly name was assign, it tries to get the model name (given by the vendor).
767 If no name is found at all, a generic one is returned.
769 Once found, the name is cached internaly to prevent reading again the device
772 the name of the device
775 if self
.__model
_name
:
776 return self
.__model
_name
778 self
.__model
_name
= self
.__MTPDevice
.get_devicename() # actually libmtp.Get_Friendlyname
779 if not self
.__model
_name
or self
.__model
_name
== "?????":
780 self
.__model
_name
= self
.__MTPDevice
.get_modelname()
781 if not self
.__model
_name
:
782 self
.__model
_name
= "MTP device"
784 return self
.__model
_name
788 log("opening the MTP device", sender
=self
)
789 self
.notify('status', _('Opening the MTP device'), )
792 self
.__MTPDevice
.connect()
793 # build the initial tracks_list
794 self
.tracks_list
= self
.get_all_tracks()
795 except Exception, exc
:
796 log('unable to find an MTP device (%s)', exc
, sender
=self
, traceback
=True)
799 self
.notify('status', _('%s opened') % self
.get_name())
803 log("closing %s", self
.get_name(), sender
=self
)
804 self
.notify('status', _('Closing %s') % self
.get_name())
807 self
.__MTPDevice
.disconnect()
808 except Exception, exc
:
809 log('unable to close %s (%s)', self
.get_name(), exc
, sender
=self
)
812 self
.notify('status', _('%s closed') % self
.get_name())
816 def add_track(self
, episode
):
817 self
.notify('status', _('Adding %s...') % episode
.title
)
818 log("sending " + str(episode
.local_filename()) + " (" + episode
.title
+ ").", sender
=self
)
822 needed
= util
.calculate_size(episode
.local_filename())
823 free
= self
.get_free_space()
825 log('Not enough space on device %s: %s available, but need at least %s', self
.get_name(), util
.format_filesize(free
), util
.format_filesize(needed
), sender
=self
)
826 self
.cancelled
= True
830 metadata
= pymtp
.LIBMTP_Track()
831 metadata
.title
= str(episode
.title
)
832 metadata
.artist
= str(episode
.channel
.title
)
833 metadata
.album
= str(episode
.channel
.title
)
834 metadata
.genre
= "podcast"
835 metadata
.date
= self
.__date
_to
_mtp
(episode
.pubDate
)
836 metadata
.duration
= get_track_length(str(episode
.local_filename()))
839 self
.__MTPDevice
.send_track_from_file( str(episode
.local_filename()), episode
.basename
, metadata
, 0, callback
=self
.__callback
)
841 log('unable to add episode %s', episode
.title
, sender
=self
, traceback
=True)
846 def remove_track(self
, sync_track
):
847 self
.notify('status', _('Removing %s') % sync_track
.mtptrack
.title
)
848 log("removing %s", sync_track
.mtptrack
.title
, sender
=self
)
851 self
.__MTPDevice
.delete_object(sync_track
.mtptrack
.item_id
)
852 except Exception, exc
:
853 log('unable remove file %s (%s)', sync_track
.mtptrack
.filename
, exc
, sender
=self
)
855 log('%s removed', sync_track
.mtptrack
.title
, sender
=self
)
857 def get_all_tracks(self
):
859 listing
= self
.__MTPDevice
.get_tracklisting(callback
=self
.__callback
)
860 except Exception, exc
:
861 log('unable to get file listing %s (%s)', exc
, sender
=self
)
864 for track
in listing
:
866 if not title
or title
=="": title
=track
.filename
867 if len(title
) > 50: title
= title
[0:49] + '...'
868 artist
= track
.artist
869 if artist
and len(artist
) > 50: artist
= artist
[0:49] + '...'
870 length
= track
.filesize
872 date
= self
.__mtp
_to
_date
(track
.date
)
874 modified
= track
.date
# not a valid mtp date. Display what mtp gave anyway
876 modified
= util
.format_date(date
)
878 t
= SyncTrack(title
, length
, modified
, mtptrack
=track
, podcast
=artist
)
882 def get_free_space(self
):
883 return self
.__MTPDevice
.get_freespace()