2012-07-02 05:53:33 +02:00
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
2012-07-09 21:08:40 +02:00
# Copyright (c) 2005-2012 Thomas Perl and the gPodder Team
2012-07-02 05:53:33 +02:00
#
# gPodder is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# gPodder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# sync.py -- Device synchronization
# Thomas Perl <thp@perli.net> 2007-12-06
# based on libipodsync.py (2006-04-05 Thomas Perl)
2012-07-09 21:08:40 +02:00
# Ported to gPodder 3 by Joseph Wickremasinghe in June 2012
2012-07-02 05:53:33 +02:00
import gpodder
from gpodder import util
from gpodder import services
2012-07-09 21:08:40 +02:00
from gpodder import download
2012-07-02 05:53:33 +02:00
import logging
logger = logging . getLogger ( __name__ )
import calendar
_ = gpodder . gettext
2012-07-09 21:08:40 +02:00
#
# TODO: Re-enable iPod and MTP sync support
#
2012-07-02 05:53:33 +02:00
2012-07-09 21:08:40 +02:00
pymtp_available = False
gpod_available = False
#gpod_available = True
#try:
# import gpod
#except:
# gpod_available = False
# logger.warning('Could not find gpod')
#
#pymtp_available = True
#try:
# import gpodder.gpopymtp as pymtp
#except:
# pymtp_available = False
# logger.warning('Could not load gpopymtp (libmtp not installed?).')
2012-07-02 05:53:33 +02:00
try :
import eyeD3
except :
2012-07-09 21:08:40 +02:00
logger . warning ( ' Could not find eyeD3 ' )
2012-07-02 05:53:33 +02:00
import os . path
import glob
import time
if pymtp_available :
class MTP ( pymtp . MTP ) :
sep = os . path . sep
def __init__ ( self ) :
pymtp . MTP . __init__ ( self )
self . folders = { }
def connect ( self ) :
pymtp . MTP . connect ( self )
self . folders = self . unfold ( self . mtp . LIBMTP_Get_Folder_List ( self . device ) )
def get_folder_list ( self ) :
return self . folders
def unfold ( self , folder , path = ' ' ) :
result = { }
while folder :
folder = folder . contents
name = self . sep . join ( [ path , folder . name ] ) . lstrip ( self . sep )
result [ name ] = folder . folder_id
if folder . child :
result . update ( self . unfold ( folder . child , name ) )
folder = folder . sibling
return result
def mkdir ( self , path ) :
folder_id = 0
prefix = [ ]
parts = path . split ( self . sep )
while parts :
prefix . append ( parts [ 0 ] )
tmpath = self . sep . join ( prefix )
if self . folders . has_key ( tmpath ) :
folder_id = self . folders [ tmpath ]
else :
folder_id = self . create_folder ( parts [ 0 ] , parent = folder_id )
# logger.info('Creating subfolder %s in %s (id=%u)' % (parts[0], self.sep.join(prefix), folder_id))
tmpath = self . sep . join ( prefix + [ parts [ 0 ] ] )
self . folders [ tmpath ] = folder_id
# logger.info(">>> %s = %s" % (tmpath, folder_id))
del parts [ 0 ]
# logger.info('MTP.mkdir: %s = %u' % (path, folder_id))
return folder_id
2012-07-09 21:08:40 +02:00
def open_device ( gui ) :
config = gui . _config
device_type = gui . _config . device_sync . device_type
2012-07-02 05:53:33 +02:00
if device_type == ' filesystem ' :
2012-07-09 21:08:40 +02:00
return MP3PlayerDevice ( config ,
gui . download_status_model ,
gui . download_queue_manager )
return None
2012-07-02 05:53:33 +02:00
def get_track_length ( filename ) :
if util . find_command ( ' mplayer ' ) is not None :
try :
mplayer_output = os . popen ( ' mplayer -msglevel all=-1 -identify -vo null -ao null -frames 0 " %s " 2>/dev/null ' % filename ) . read ( )
return int ( float ( mplayer_output [ mplayer_output . index ( ' ID_LENGTH ' ) : ] . splitlines ( ) [ 0 ] [ 10 : ] ) * 1000 )
except :
pass
else :
logger . info ( ' Please install MPlayer for track length detection. ' )
try :
eyed3_info = eyeD3 . Mp3AudioFile ( filename )
return int ( eyed3_info . getPlayTime ( ) * 1000 )
except :
pass
return int ( 60 * 60 * 1000 * 3 ) # Default is three hours (to be on the safe side)
class SyncTrack ( object ) :
"""
This represents a track that is on a device . You need
2012-07-09 21:08:40 +02:00
to specify at least the following keyword arguments ,
2012-07-02 05:53:33 +02:00
because these will be used to display the track in the
GUI . All other keyword arguments are optional and can
be used to reference internal objects , etc . . . See the
iPod synchronization code for examples .
Keyword arguments needed :
playcount ( How often has the track been played ? )
podcast ( Which podcast is this track from ? Or : Folder name )
released ( The release date of the episode )
If any of these fields is unknown , it should not be
passed to the function ( the values will default to None
for all required fields ) .
"""
def __init__ ( self , title , length , modified , * * kwargs ) :
self . title = title
self . length = length
self . filesize = util . format_filesize ( length )
self . modified = modified
# Set some (possible) keyword arguments to default values
self . playcount = 0
self . podcast = None
self . released = None
# Convert keyword arguments to object attributes
self . __dict__ . update ( kwargs )
@property
def playcount_str ( self ) :
return str ( self . playcount )
class Device ( services . ObservableService ) :
def __init__ ( self , config ) :
self . _config = config
self . cancelled = False
self . allowed_types = [ ' audio ' , ' video ' ]
self . errors = [ ]
self . tracks_list = [ ]
signals = [ ' progress ' , ' sub-progress ' , ' status ' , ' done ' , ' post-done ' ]
services . ObservableService . __init__ ( self , signals )
def open ( self ) :
pass
def cancel ( self ) :
self . cancelled = True
self . notify ( ' status ' , _ ( ' Cancelled by user ' ) )
def close ( self ) :
self . notify ( ' status ' , _ ( ' Writing data to disk ' ) )
2012-11-18 19:26:02 +01:00
if self . _config . device_sync . after_sync . sync_disks and not gpodder . ui . win32 :
2012-07-02 05:53:33 +02:00
os . system ( ' sync ' )
else :
logger . warning ( ' Not syncing disks. Unmount your device before unplugging. ' )
return True
2012-07-07 23:41:56 +02:00
def add_sync_tasks ( self , tracklist , force_played = False ) :
2012-07-02 05:53:33 +02:00
for track in list ( tracklist ) :
# Filter tracks that are not meant to be synchronized
does_not_exist = not track . was_downloaded ( and_exists = True )
2012-07-09 21:08:40 +02:00
exclude_played = ( not track . is_new and
2012-07-02 05:53:33 +02:00
self . _config . device_sync . skip_played_episodes )
wrong_type = track . file_type ( ) not in self . allowed_types
if does_not_exist or exclude_played or wrong_type :
logger . info ( ' Excluding %s from sync ' , track . title )
tracklist . remove ( track )
2012-07-09 21:08:40 +02:00
for track in sorted ( tracklist , key = lambda e : e . pubdate_prop ) :
2012-07-02 05:53:33 +02:00
if self . cancelled :
return False
2012-07-09 21:08:40 +02:00
# XXX: need to check if track is added properly?
2012-07-02 05:53:33 +02:00
sync_task = SyncTask ( track )
sync_task . status = sync_task . QUEUED
sync_task . device = self
self . download_status_model . register_task ( sync_task )
self . download_queue_manager . add_task ( sync_task )
return True
2012-07-09 21:08:40 +02:00
def remove_tracks ( self , tracklist ) :
for idx , track in enumerate ( tracklist ) :
2012-07-02 05:53:33 +02:00
if self . cancelled :
return False
2012-07-09 21:08:40 +02:00
self . notify ( ' progress ' , idx , len ( tracklist ) )
2012-07-02 05:53:33 +02:00
self . remove_track ( track )
2012-07-09 21:08:40 +02:00
2012-07-02 05:53:33 +02:00
return True
def get_all_tracks ( self ) :
pass
def add_track ( self , track , reporthook = None ) :
pass
def remove_track ( self , track ) :
pass
def get_free_space ( self ) :
pass
def episode_on_device ( self , episode ) :
return self . _track_on_device ( episode . title )
def _track_on_device ( self , track_name ) :
for t in self . tracks_list :
title = t . title
if track_name == title :
return t
return None
class iPodDevice ( Device ) :
def __init__ ( self , config ) :
Device . __init__ ( self , config )
self . mountpoint = str ( self . _config . ipod_mount )
self . itdb = None
self . podcast_playlist = None
def get_free_space ( self ) :
# Reserve 10 MiB for iTunesDB writing (to be on the safe side)
RESERVED_FOR_ITDB = 1024 * 1024 * 10
return util . get_free_disk_space ( self . mountpoint ) - RESERVED_FOR_ITDB
def open ( self ) :
Device . open ( self )
if not gpod_available or not os . path . isdir ( self . mountpoint ) :
return False
self . notify ( ' status ' , _ ( ' Opening iPod database ' ) )
self . itdb = gpod . itdb_parse ( self . mountpoint , None )
if self . itdb is None :
return False
self . itdb . mountpoint = self . mountpoint
self . podcasts_playlist = gpod . itdb_playlist_podcasts ( self . itdb )
self . master_playlist = gpod . itdb_playlist_mpl ( self . itdb )
if self . podcasts_playlist :
self . notify ( ' status ' , _ ( ' iPod opened ' ) )
# build the initial tracks_list
self . tracks_list = self . get_all_tracks ( )
return True
else :
return False
def close ( self ) :
if self . itdb is not None :
self . notify ( ' status ' , _ ( ' Saving iPod database ' ) )
gpod . itdb_write ( self . itdb , None )
self . itdb = None
2012-07-09 21:08:40 +02:00
2012-07-02 05:53:33 +02:00
if self . _config . ipod_write_gtkpod_extended :
self . notify ( ' status ' , _ ( ' Writing extended gtkpod database ' ) )
2012-07-09 21:08:40 +02:00
itunes_folder = os . path . join ( self . mountpoint , ' iPod_Control ' , ' iTunes ' )
ext_filename = os . path . join ( itunes_folder , ' iTunesDB.ext ' )
idb_filename = os . path . join ( itunes_folder , ' iTunesDB ' )
2012-07-02 05:53:33 +02:00
if os . path . exists ( ext_filename ) and os . path . exists ( idb_filename ) :
try :
db = gpod . ipod . Database ( self . mountpoint )
gpod . gtkpod . parse ( ext_filename , db , idb_filename )
gpod . gtkpod . write ( ext_filename , db , idb_filename )
db . close ( )
except :
2012-07-09 21:08:40 +02:00
logger . error ( ' Error writing iTunesDB.ext ' )
2012-07-02 05:53:33 +02:00
else :
2012-07-09 21:08:40 +02:00
logger . warning ( ' Could not find %s or %s . ' ,
ext_filename , idb_filename )
2012-07-02 05:53:33 +02:00
Device . close ( self )
return True
def update_played_or_delete ( self , channel , episodes , delete_from_db ) :
"""
Check whether episodes on ipod are played and update as played
and delete if required .
"""
for episode in episodes :
track = self . episode_on_device ( episode )
if track :
gtrack = track . libgpodtrack
if gtrack . playcount > 0 :
if delete_from_db and not gtrack . rating :
2012-07-09 21:08:40 +02:00
logger . info ( ' Deleting episode from db %s ' , gtrack . title )
2012-07-02 05:53:33 +02:00
channel . delete_episode ( episode )
else :
2012-07-09 21:08:40 +02:00
logger . info ( ' Marking episode as played %s ' , gtrack . title )
2012-07-02 05:53:33 +02:00
def purge ( self ) :
for track in gpod . sw_get_playlist_tracks ( self . podcasts_playlist ) :
if gpod . itdb_filename_on_ipod ( track ) is None :
2012-07-09 21:08:40 +02:00
logger . info ( ' Episode has no file: %s ' , track . title )
2012-07-02 05:53:33 +02:00
# self.remove_track_gpod(track)
elif track . playcount > 0 and not track . rating :
2012-07-09 21:08:40 +02:00
logger . info ( ' Purging episode: %s ' , track . title )
2012-07-02 05:53:33 +02:00
self . remove_track_gpod ( track )
def get_all_tracks ( self ) :
tracks = [ ]
for track in gpod . sw_get_playlist_tracks ( self . podcasts_playlist ) :
filename = gpod . itdb_filename_on_ipod ( track )
if filename is None :
# This can happen if the episode is deleted on the device
2012-07-09 21:08:40 +02:00
logger . info ( ' Episode has no file: %s ' , track . title )
2012-07-02 05:53:33 +02:00
self . remove_track_gpod ( track )
continue
length = util . calculate_size ( filename )
timestamp = util . file_modification_timestamp ( filename )
modified = util . format_date ( timestamp )
try :
released = gpod . itdb_time_mac_to_host ( track . time_released )
released = util . format_date ( released )
except ValueError , ve :
# timestamp out of range for platform time_t (bug 418)
2012-07-09 21:08:40 +02:00
logger . info ( ' Cannot convert track time: %s ' , ve )
2012-07-02 05:53:33 +02:00
released = 0
2012-07-09 21:08:40 +02:00
t = SyncTrack ( track . title , length , modified ,
modified_sort = timestamp ,
libgpodtrack = track ,
playcount = track . playcount ,
released = released ,
podcast = track . artist )
2012-07-02 05:53:33 +02:00
tracks . append ( t )
2012-07-09 21:08:40 +02:00
return tracks
2012-07-02 05:53:33 +02:00
def remove_track ( self , track ) :
self . notify ( ' status ' , _ ( ' Removing %s ' ) % track . title )
self . remove_track_gpod ( track . libgpodtrack )
def remove_track_gpod ( self , track ) :
filename = gpod . itdb_filename_on_ipod ( track )
try :
gpod . itdb_playlist_remove_track ( self . podcasts_playlist , track )
except :
2012-07-09 21:08:40 +02:00
logger . info ( ' Track %s not in playlist ' , track . title )
2012-07-02 05:53:33 +02:00
gpod . itdb_track_unlink ( track )
util . delete_file ( filename )
def add_track ( self , episode ) :
self . notify ( ' status ' , _ ( ' Adding %s ' ) % episode . title )
for track in gpod . sw_get_playlist_tracks ( self . podcasts_playlist ) :
if episode . url == track . podcasturl :
# Mark as played on iPod if played locally (and set podcast flags)
self . set_podcast_flags ( track , episode )
return True
original_filename = episode . local_filename ( create = False )
# The file has to exist, if we ought to transfer it, and therefore,
# local_filename(create=False) must never return None as filename
assert original_filename is not None
local_filename = original_filename
if util . calculate_size ( original_filename ) > self . get_free_space ( ) :
2012-07-09 21:08:40 +02:00
logger . error ( ' Not enough space on %s , sync aborted... ' , self . mountpoint )
2012-07-02 05:53:33 +02:00
d = { ' episode ' : episode . title , ' mountpoint ' : self . mountpoint }
message = _ ( ' Error copying %(episode)s : Not enough free space on %(mountpoint)s ' )
self . errors . append ( message % d )
self . cancelled = True
return False
local_filename = self . convert_track ( episode )
( fn , extension ) = os . path . splitext ( local_filename )
if extension . lower ( ) . endswith ( ' ogg ' ) :
2012-07-09 21:08:40 +02:00
logger . error ( ' Cannot copy .ogg files to iPod. ' )
2012-07-02 05:53:33 +02:00
return False
track = gpod . itdb_track_new ( )
2012-07-09 21:08:40 +02:00
2012-07-02 05:53:33 +02:00
# Add release time to track if pubdate has a valid value
if episode . pubdate > 0 :
try :
# libgpod>= 0.5.x uses a new timestamp format
track . time_released = gpod . itdb_time_host_to_mac ( int ( episode . pubdate ) )
except :
# old (pre-0.5.x) libgpod versions expect mactime, so
# we're going to manually build a good mactime timestamp here :)
#
# + 2082844800 for unixtime => mactime (1970 => 1904)
track . time_released = int ( episode . pubdate + 2082844800 )
2012-07-09 21:08:40 +02:00
2012-07-02 05:53:33 +02:00
track . title = str ( episode . title )
track . album = str ( episode . channel . title )
track . artist = str ( episode . channel . title )
track . description = str ( util . remove_html_tags ( episode . description ) )
track . podcasturl = str ( episode . url )
track . podcastrss = str ( episode . channel . url )
track . tracklen = get_track_length ( local_filename )
track . size = os . path . getsize ( local_filename )
if episode . file_type ( ) == ' audio ' :
track . filetype = ' mp3 '
track . mediatype = 0x00000004
elif episode . file_type ( ) == ' video ' :
track . filetype = ' m4v '
track . mediatype = 0x00000006
self . set_podcast_flags ( track , episode )
gpod . itdb_track_add ( self . itdb , track , - 1 )
gpod . itdb_playlist_add_track ( self . master_playlist , track , - 1 )
gpod . itdb_playlist_add_track ( self . podcasts_playlist , track , - 1 )
copied = gpod . itdb_cp_track_to_ipod ( track , str ( local_filename ) , None )
if copied and gpodder . user_hooks is not None :
gpodder . user_hooks . on_file_copied_to_ipod ( self , local_filename )
# If the file has been converted, delete the temporary file here
if local_filename != original_filename :
util . delete_file ( local_filename )
return True
def set_podcast_flags ( self , track , episode ) :
try :
# Set several flags for to podcast values
track . remember_playback_position = 0x01
track . flag1 = 0x02
track . flag2 = 0x01
track . flag3 = 0x01
track . flag4 = 0x01
except :
2012-07-09 21:08:40 +02:00
logger . warning ( ' Seems like your python-gpod is out-of-date. ' )
2012-07-02 05:53:33 +02:00
2012-07-09 21:08:40 +02:00
class MP3PlayerDevice ( Device ) :
def __init__ ( self , config ,
download_status_model ,
download_queue_manager ) :
2012-07-02 05:53:33 +02:00
Device . __init__ ( self , config )
2012-08-13 22:28:52 +02:00
self . destination = util . sanitize_encoding ( self . _config . device_sync . device_folder )
2012-07-02 05:53:33 +02:00
self . buffer_size = 1024 * 1024 # 1 MiB
2012-07-09 21:08:40 +02:00
self . download_status_model = download_status_model
self . download_queue_manager = download_queue_manager
2012-07-02 05:53:33 +02:00
def get_free_space ( self ) :
return util . get_free_disk_space ( self . destination )
def open ( self ) :
Device . open ( self )
self . notify ( ' status ' , _ ( ' Opening MP3 player ' ) )
2012-07-09 21:08:40 +02:00
2012-07-02 05:53:33 +02:00
if util . directory_is_writable ( self . destination ) :
self . notify ( ' status ' , _ ( ' MP3 player opened ' ) )
self . tracks_list = self . get_all_tracks ( )
return True
2012-07-09 21:08:40 +02:00
return False
2012-07-02 05:53:33 +02:00
def add_track ( self , episode , reporthook = None ) :
self . notify ( ' status ' , _ ( ' Adding %s ' ) % episode . title . decode ( ' utf-8 ' , ' ignore ' ) )
2012-07-07 23:41:56 +02:00
if self . _config . device_sync . one_folder_per_podcast :
2012-07-02 05:53:33 +02:00
# Add channel title as subfolder
folder = episode . channel . title
# Clean up the folder name for use on limited devices
2012-07-09 21:08:40 +02:00
folder = util . sanitize_filename ( folder ,
self . _config . device_sync . max_filename_length )
2012-07-02 05:53:33 +02:00
folder = os . path . join ( self . destination , folder )
else :
folder = self . destination
folder = util . sanitize_encoding ( folder )
2012-07-09 21:08:40 +02:00
filename = episode . local_filename ( create = False )
2012-07-02 05:53:33 +02:00
# The file has to exist, if we ought to transfer it, and therefore,
# local_filename(create=False) must never return None as filename
2012-07-09 21:08:40 +02:00
assert filename is not None
2012-07-02 05:53:33 +02:00
2012-07-09 21:08:40 +02:00
from_file = util . sanitize_encoding ( filename )
filename_base = util . sanitize_filename ( episode . sync_filename (
self . _config . device_sync . custom_sync_name_enabled ,
self . _config . device_sync . custom_sync_name ) ,
self . _config . device_sync . max_filename_length )
2012-07-02 05:53:33 +02:00
to_file = filename_base + os . path . splitext ( from_file ) [ 1 ] . lower ( )
# dirty workaround: on bad (empty) episode titles,
# we simply use the from_file basename
# (please, podcast authors, FIX YOUR RSS FEEDS!)
if os . path . splitext ( to_file ) [ 0 ] == ' ' :
to_file = os . path . basename ( from_file )
to_file = util . sanitize_encoding ( os . path . join ( folder , to_file ) )
if not os . path . exists ( folder ) :
try :
os . makedirs ( folder )
except :
logger . error ( ' Cannot create folder on MP3 player: %s ' , folder )
return False
if not os . path . exists ( to_file ) :
2012-07-09 21:08:40 +02:00
logger . info ( ' Copying %s => %s ' ,
os . path . basename ( from_file ) ,
to_file . decode ( util . encoding ) )
self . copy_file_progress ( from_file , to_file , reporthook )
2012-07-02 05:53:33 +02:00
return True
def copy_file_progress ( self , from_file , to_file , reporthook = None ) :
try :
out_file = open ( to_file , ' wb ' )
except IOError , ioerror :
d = { ' filename ' : ioerror . filename , ' message ' : ioerror . strerror }
self . errors . append ( _ ( ' Error opening %(filename)s : %(message)s ' ) % d )
self . cancel ( )
return False
try :
in_file = open ( from_file , ' rb ' )
except IOError , ioerror :
d = { ' filename ' : ioerror . filename , ' message ' : ioerror . strerror }
self . errors . append ( _ ( ' Error opening %(filename)s : %(message)s ' ) % d )
self . cancel ( )
return False
2012-07-09 21:08:40 +02:00
in_file . seek ( 0 , os . SEEK_END )
total_bytes = in_file . tell ( )
2012-07-02 05:53:33 +02:00
in_file . seek ( 0 )
bytes_read = 0
s = in_file . read ( self . buffer_size )
while s :
bytes_read + = len ( s )
try :
out_file . write ( s )
except IOError , ioerror :
self . errors . append ( ioerror . strerror )
try :
out_file . close ( )
except :
pass
try :
logger . info ( ' Trying to remove partially copied file: %s ' % to_file )
os . unlink ( to_file )
logger . info ( ' Yeah! Unlinked %s at least.. ' % to_file )
except :
logger . error ( ' Error while trying to unlink %s . OH MY! ' % to_file )
self . cancel ( )
return False
2012-07-09 21:08:40 +02:00
reporthook ( bytes_read , 1 , total_bytes )
2012-07-02 05:53:33 +02:00
s = in_file . read ( self . buffer_size )
out_file . close ( )
in_file . close ( )
return True
2012-07-09 21:08:40 +02:00
2012-07-02 05:53:33 +02:00
def get_all_tracks ( self ) :
tracks = [ ]
2012-07-07 23:41:56 +02:00
if self . _config . one_folder_per_podcast :
2012-07-02 05:53:33 +02:00
files = glob . glob ( os . path . join ( self . destination , ' * ' , ' * ' ) )
else :
files = glob . glob ( os . path . join ( self . destination , ' * ' ) )
for filename in files :
( title , extension ) = os . path . splitext ( os . path . basename ( filename ) )
length = util . calculate_size ( filename )
timestamp = util . file_modification_timestamp ( filename )
modified = util . format_date ( timestamp )
2012-07-07 23:41:56 +02:00
if self . _config . one_folder_per_podcast :
2012-07-02 05:53:33 +02:00
podcast_name = os . path . basename ( os . path . dirname ( filename ) )
else :
podcast_name = None
2012-07-09 21:08:40 +02:00
t = SyncTrack ( title , length , modified ,
modified_sort = timestamp ,
filename = filename ,
podcast = podcast_name )
2012-07-02 05:53:33 +02:00
tracks . append ( t )
return tracks
def episode_on_device ( self , episode ) :
2012-07-09 21:08:40 +02:00
e = util . sanitize_filename ( episode . sync_filename (
self . _config . device_sync . custom_sync_name_enabled ,
self . _config . device_sync . custom_sync_name ) ,
self . _config . device_sync . max_filename_length )
2012-07-02 05:53:33 +02:00
return self . _track_on_device ( e )
def remove_track ( self , track ) :
self . notify ( ' status ' , _ ( ' Removing %s ' ) % track . title )
util . delete_file ( track . filename )
directory = os . path . dirname ( track . filename )
2012-07-07 23:41:56 +02:00
if self . directory_is_empty ( directory ) and self . _config . one_folder_per_podcast :
2012-07-02 05:53:33 +02:00
try :
os . rmdir ( directory )
except :
logger . error ( ' Cannot remove %s ' , directory )
def directory_is_empty ( self , directory ) :
files = glob . glob ( os . path . join ( directory , ' * ' ) )
dotfiles = glob . glob ( os . path . join ( directory , ' .* ' ) )
return len ( files + dotfiles ) == 0
class MTPDevice ( Device ) :
def __init__ ( self , config ) :
Device . __init__ ( self , config )
self . __model_name = None
try :
self . __MTPDevice = MTP ( )
except NameError , e :
# pymtp not available / not installed (see bug 924)
logger . error ( ' pymtp not found: %s ' , str ( e ) )
self . __MTPDevice = None
def __callback ( self , sent , total ) :
if self . cancelled :
return - 1
percentage = round ( float ( sent ) / float ( total ) * 100 )
text = ( ' %i %% ' % percentage )
self . notify ( ' progress ' , sent , total , text )
def __date_to_mtp ( self , date ) :
"""
this function format the given date and time to a string representation
according to MTP specifications : YYYYMMDDThhmmss . s
return
the string representation od the given date
"""
if not date :
return " "
try :
d = time . gmtime ( date )
return time . strftime ( " % Y % m %d - % H % M % S.0Z " , d )
except Exception , exc :
2012-07-09 21:08:40 +02:00
logger . error ( ' ERROR: An error has happend while trying to convert date to an mtp string ' )
2012-07-02 05:53:33 +02:00
return None
def __mtp_to_date ( self , mtp ) :
"""
this parse the mtp ' s string representation for date
according to specifications ( YYYYMMDDThhmmss . s ) to
a python time object
"""
if not mtp :
return None
try :
mtp = mtp . replace ( " " , " 0 " ) # replace blank with 0 to fix some invalid string
d = time . strptime ( mtp [ : 8 ] + mtp [ 9 : 13 ] , " % Y % m %d % H % M % S " )
_date = calendar . timegm ( d )
if len ( mtp ) == 20 :
# TIME ZONE SHIFTING: the string contains a hour/min shift relative to a time zone
try :
shift_direction = mtp [ 15 ]
hour_shift = int ( mtp [ 16 : 18 ] )
minute_shift = int ( mtp [ 18 : 20 ] )
shift_in_sec = hour_shift * 3600 + minute_shift * 60
if shift_direction == " + " :
_date + = shift_in_sec
elif shift_direction == " - " :
_date - = shift_in_sec
else :
raise ValueError ( " Expected + or - " )
except Exception , exc :
logger . warning ( ' WARNING: ignoring invalid time zone information for %s ( %s ) ' )
return max ( 0 , _date )
except Exception , exc :
logger . warning ( ' WARNING: the mtp date " %s " can not be parsed against mtp specification ( %s ) ' )
return None
def get_name ( self ) :
"""
this function try to find a nice name for the device .
First , it tries to find a friendly ( user assigned ) name
( this name can be set by other application and is stored on the device ) .
if no friendly name was assign , it tries to get the model name ( given by the vendor ) .
If no name is found at all , a generic one is returned .
Once found , the name is cached internaly to prevent reading again the device
return
the name of the device
"""
if self . __model_name :
return self . __model_name
if self . __MTPDevice is None :
return _ ( ' MTP device ' )
self . __model_name = self . __MTPDevice . get_devicename ( ) # actually libmtp.Get_Friendlyname
if not self . __model_name or self . __model_name == " ????? " :
self . __model_name = self . __MTPDevice . get_modelname ( )
if not self . __model_name :
self . __model_name = _ ( ' MTP device ' )
return self . __model_name
def open ( self ) :
Device . open ( self )
logger . info ( " opening the MTP device " )
self . notify ( ' status ' , _ ( ' Opening the MTP device ' ) , )
try :
self . __MTPDevice . connect ( )
# build the initial tracks_list
self . tracks_list = self . get_all_tracks ( )
except Exception , exc :
logger . error ( ' unable to find an MTP device ( %s ) ' )
return False
self . notify ( ' status ' , _ ( ' %s opened ' ) % self . get_name ( ) )
return True
def close ( self ) :
logger . info ( " closing %s " , self . get_name ( ) )
self . notify ( ' status ' , _ ( ' Closing %s ' ) % self . get_name ( ) )
try :
self . __MTPDevice . disconnect ( )
except Exception , exc :
logger . error ( ' unable to close %s ( %s ) ' , self . get_name ( ) )
return False
self . notify ( ' status ' , _ ( ' %s closed ' ) % self . get_name ( ) )
Device . close ( self )
return True
def add_track ( self , episode ) :
self . notify ( ' status ' , _ ( ' Adding %s ... ' ) % episode . title )
filename = str ( self . convert_track ( episode ) )
logger . info ( " sending %s ( %s ). " , filename , episode . title )
try :
# verify free space
needed = util . calculate_size ( filename )
free = self . get_free_space ( )
if needed > free :
2012-07-09 21:08:40 +02:00
logger . error ( ' Not enough space on device %s : %s available, but need at least %s ' , self . get_name ( ) , util . format_filesize ( free ) , util . format_filesize ( needed ) )
2012-07-02 05:53:33 +02:00
self . cancelled = True
return False
# fill metadata
metadata = pymtp . LIBMTP_Track ( )
metadata . title = str ( episode . title )
metadata . artist = str ( episode . channel . title )
metadata . album = str ( episode . channel . title )
metadata . genre = " podcast "
metadata . date = self . __date_to_mtp ( episode . pubdate )
metadata . duration = get_track_length ( str ( filename ) )
folder_name = ' '
if episode . mimetype . startswith ( ' audio/ ' ) and self . _config . mtp_audio_folder :
folder_name = self . _config . mtp_audio_folder
if episode . mimetype . startswith ( ' video/ ' ) and self . _config . mtp_video_folder :
folder_name = self . _config . mtp_video_folder
if episode . mimetype . startswith ( ' image/ ' ) and self . _config . mtp_image_folder :
folder_name = self . _config . mtp_image_folder
if folder_name != ' ' and self . _config . mtp_podcast_folders :
folder_name + = os . path . sep + str ( episode . channel . title )
# log('Target MTP folder: %s' % folder_name)
if folder_name == ' ' :
folder_id = 0
else :
folder_id = self . __MTPDevice . mkdir ( folder_name )
# send the file
to_file = util . sanitize_filename ( metadata . title ) + episode . extension ( )
self . __MTPDevice . send_track_from_file ( filename , to_file ,
metadata , folder_id , callback = self . __callback )
if gpodder . user_hooks is not None :
gpodder . user_hooks . on_file_copied_to_mtp ( self , filename , to_file )
except :
logger . error ( ' unable to add episode %s ' , episode . title )
return False
return True
def remove_track ( self , sync_track ) :
self . notify ( ' status ' , _ ( ' Removing %s ' ) % sync_track . mtptrack . title )
logger . info ( " removing %s " , sync_track . mtptrack . title )
try :
self . __MTPDevice . delete_object ( sync_track . mtptrack . item_id )
except Exception , exc :
logger . error ( ' unable remove file %s ( %s ) ' , sync_track . mtptrack . filename )
logger . info ( ' %s removed ' , sync_track . mtptrack . title )
def get_all_tracks ( self ) :
try :
listing = self . __MTPDevice . get_tracklisting ( callback = self . __callback )
except Exception , exc :
logger . error ( ' unable to get file listing %s ( %s ) ' )
tracks = [ ]
for track in listing :
title = track . title
if not title or title == " " : title = track . filename
if len ( title ) > 50 : title = title [ 0 : 49 ] + ' ... '
artist = track . artist
if artist and len ( artist ) > 50 : artist = artist [ 0 : 49 ] + ' ... '
length = track . filesize
age_in_days = 0
date = self . __mtp_to_date ( track . date )
if not date :
modified = track . date # not a valid mtp date. Display what mtp gave anyway
modified_sort = - 1 # no idea how to sort invalid date
else :
modified = util . format_date ( date )
modified_sort = date
t = SyncTrack ( title , length , modified , modified_sort = modified_sort , mtptrack = track , podcast = artist )
tracks . append ( t )
return tracks
def get_free_space ( self ) :
if self . __MTPDevice is not None :
return self . __MTPDevice . get_freespace ( )
else :
return 0
2012-07-09 21:08:40 +02:00
class SyncCancelledException ( Exception ) : pass
2012-07-02 05:53:33 +02:00
2012-07-09 21:08:40 +02:00
class SyncTask ( download . DownloadTask ) :
# An object representing the synchronization task of an episode
2012-07-02 05:53:33 +02:00
# Possible states this sync task can be in
STATUS_MESSAGE = ( _ ( ' Added ' ) , _ ( ' Queued ' ) , _ ( ' Synchronizing ' ) ,
_ ( ' Finished ' ) , _ ( ' Failed ' ) , _ ( ' Cancelled ' ) , _ ( ' Paused ' ) )
( INIT , QUEUED , DOWNLOADING , DONE , FAILED , CANCELLED , PAUSED ) = range ( 7 )
2012-07-07 23:41:56 +02:00
2012-07-02 05:53:33 +02:00
def __str__ ( self ) :
return self . __episode . title
def __get_status ( self ) :
return self . __status
def __set_status ( self , status ) :
if status != self . __status :
self . __status_changed = True
self . __status = status
status = property ( fget = __get_status , fset = __set_status )
2012-07-09 21:08:40 +02:00
2012-07-02 05:53:33 +02:00
def __get_device ( self ) :
return self . __device
def __set_device ( self , device ) :
self . __device = device
2012-07-09 21:08:40 +02:00
device = property ( fget = __get_device , fset = __set_device )
2012-07-02 05:53:33 +02:00
def __get_status_changed ( self ) :
if self . __status_changed :
self . __status_changed = False
return True
else :
return False
status_changed = property ( fget = __get_status_changed )
2012-07-09 21:08:40 +02:00
2012-07-07 23:41:56 +02:00
def __get_activity ( self ) :
return self . __activity
def __set_activity ( self , activity ) :
self . __activity = activity
activity = property ( fget = __get_activity , fset = __set_activity )
2012-07-02 05:53:33 +02:00
2012-07-09 21:08:40 +02:00
def __get_empty_string ( self ) :
return ' '
2012-07-02 05:53:33 +02:00
2012-07-09 21:08:40 +02:00
url = property ( fget = __get_empty_string )
podcast_url = property ( fget = __get_empty_string )
2012-07-02 05:53:33 +02:00
def __get_episode ( self ) :
return self . __episode
episode = property ( fget = __get_episode )
def cancel ( self ) :
if self . status in ( self . DOWNLOADING , self . QUEUED ) :
self . status = self . CANCELLED
def removed_from_list ( self ) :
2012-07-09 21:08:40 +02:00
# XXX: Should we delete temporary/incomplete files here?
pass
2012-07-02 05:53:33 +02:00
2012-07-09 21:08:40 +02:00
def __init__ ( self , episode ) :
2012-07-02 05:53:33 +02:00
self . __status = SyncTask . INIT
2012-07-09 21:08:40 +02:00
self . __activity = SyncTask . ACTIVITY_SYNCHRONIZE
2012-07-02 05:53:33 +02:00
self . __status_changed = True
self . __episode = episode
# Create the target filename and save it in the database
self . filename = self . __episode . local_filename ( create = False )
self . tempname = self . filename + ' .partial '
2012-07-09 21:08:40 +02:00
2012-07-02 05:53:33 +02:00
self . total_size = self . __episode . file_size
self . speed = 0.0
self . progress = 0.0
self . error_message = None
# Have we already shown this task in a notification?
self . _notification_shown = False
# Variables for speed limit and speed calculation
self . __start_time = 0
self . __start_blocks = 0
self . __limit_rate_value = 999
self . __limit_rate = 999
# Callbacks
self . _progress_updated = lambda x : None
def notify_as_finished ( self ) :
if self . status == SyncTask . DONE :
if self . _notification_shown :
return False
else :
self . _notification_shown = True
return True
return False
def notify_as_failed ( self ) :
if self . status == SyncTask . FAILED :
if self . _notification_shown :
return False
else :
self . _notification_shown = True
return True
return False
def add_progress_callback ( self , callback ) :
self . _progress_updated = callback
def status_updated ( self , count , blockSize , totalSize ) :
# We see a different "total size" while downloading,
# so correct the total size variable in the thread
if totalSize != self . total_size and totalSize > 0 :
self . total_size = float ( totalSize )
if self . total_size > 0 :
2012-07-09 21:08:40 +02:00
self . progress = max ( 0.0 , min ( 1.0 , float ( count * blockSize ) / self . total_size ) )
2012-07-02 05:53:33 +02:00
self . _progress_updated ( self . progress )
if self . status == SyncTask . CANCELLED :
raise SyncCancelledException ( )
if self . status == SyncTask . PAUSED :
raise SyncCancelledException ( )
def recycle ( self ) :
self . episode . download_task = None
def run ( self ) :
# Speed calculation (re-)starts here
self . __start_time = 0
self . __start_blocks = 0
# If the download has already been cancelled, skip it
if self . status == SyncTask . CANCELLED :
util . delete_file ( self . tempname )
self . progress = 0.0
self . speed = 0.0
return False
# We only start this download if its status is "queued"
if self . status != SyncTask . QUEUED :
return False
# We are synching this file right now
self . status = SyncTask . DOWNLOADING
self . _notification_shown = False
try :
2012-07-09 21:08:40 +02:00
logger . info ( ' Starting SyncTask ' )
self . device . add_track ( self . episode , reporthook = self . status_updated )
2012-07-02 05:53:33 +02:00
except Exception , e :
self . status = SyncTask . FAILED
logger . error ( ' Download failed: %s ' , str ( e ) , exc_info = True )
self . error_message = _ ( ' Error: %s ' ) % ( str ( e ) , )
if self . status == SyncTask . DOWNLOADING :
# Everything went well - we're done
self . status = SyncTask . DONE
if self . total_size < = 0 :
self . total_size = util . calculate_size ( self . filename )
logger . info ( ' Total size updated to %d ' , self . total_size )
self . progress = 1.0
gpodder . user_extensions . on_episode_downloaded ( self . __episode )
return True
2012-07-09 21:08:40 +02:00
2012-07-02 05:53:33 +02:00
self . speed = 0.0
# We finished, but not successfully (at least not really)
return False