#!/usr/bin/env python # # This program 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. # # This program 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 . # # Contact nikosapi@gmail.com with any problems or bug reports import os import string import getopt import sys class archosSync: def __init__(self, from_dir, to_dir, dvd_plugin, podcast_plugin): if from_dir and os.path.isdir(from_dir): self.from_dir = from_dir else: self.from_dir = os.path.expanduser('~') + '/Movies/Miro/' if not os.access(to_dir, os.W_OK) and os.path.isdir(to_dir): print 'DEST_DIR not writable or doesn\'t exist.' os.exit(2) self.device_root = to_dir self.device_sync_list = self.device_root + '/System/sync-data.txt' self.device_video_root = self.device_root + '/Video/' self.local_filelist = [] self.device_filelist = [] self.to_be_copied = [] self.to_be_deleted =[] self.log = log().log self.log_class = log() # These are various codecs _used_ by mplayer, if your video isn't copying # try running: mplayer -frames 0 -identify [video file] 2>&1 | grep _CODEC # then make sure the codecs are in the following lists standard_codecs = [ # Video Codecs [ 'ffodivx', 'xvid', 'divx', 'divxds', 'mpeg4', 'wmv8', 'wmv8', 'wmvadmo', 'wmvvc1dmo', 'divx4vfw', 'ffdivx', 'ffmp42','ffmp41', 'wmv9dmo' ], # Audio Codecs [ 'wma9dmo', 'wmadmo', 'divx', 'mp3', 'ffwmav1', 'ffwmav2', 'ffmp3', 'ffmp3adu', 'mad' ] ] dvd_plugin_codecs = [ # Video Codecs ['mpeg12'], # Audio Codecs ['a52'], ] podcast_plugin_codecs = [ # Video Codecs ['ffh264'], # Audio Codecs ['faad'] ] self.allowed_video_codecs = standard_codecs[0] self.allowed_audio_codecs = standard_codecs[1] if dvd_plugin: self.allowed_video_codecs += dvd_plugin_codecs[0] self.allowed_audio_codecs += dvd_plugin_codecs[1] if podcast_plugin: self.allowed_video_codecs += podcast_plugin_codecs[0] self.allowed_audio_codecs += podcast_plugin_codecs[1] def progress_indicator(self, completed = False): pass def create_filelist(self, convert_audio = False): for root_dir, dirs, files in os.walk(self.from_dir): for file in files: full_file = [ root_dir + '/', file ] if Utils().is_playable(full_file[0] + full_file[1], self.allowed_audio_codecs, self.allowed_video_codecs)[0] == 2: self.local_filelist.append(full_file) def read_device_filelist(self): if os.path.isfile(self.device_sync_list): sync_file = open(self.device_sync_list, 'r') self.device_filelist = sync_file.read().split('\n') sync_file.close() def write_device_filelist(self): if os.access(string.join(self.device_sync_list.split('/')[:-1], '/'), os.W_OK): sync_file = open(self.device_sync_list, 'w') for video in self.device_filelist: if video != "": sync_file.write(video + '\n') sync_file.close() def compare_filelists(self, copy_deleted_files = False): # create the list of files to be copied to the device for local_file in self.local_filelist: if not local_file[1] in self.device_filelist: self.to_be_copied.append(local_file) elif copy_deleted_files and not os.path.isfile(self.device_video_root + local_file[1]): self.to_be_copied.append(local_file) # create the list of files that no longer exist locally but still # exist on the device for device_file in self.device_filelist: exists_local = False for local_file in self.local_filelist: if device_file == local_file[1]: exists_local = True if not exists_local and device_file != "": self.to_be_deleted.append(device_file) def remove_files(self): for file in self.to_be_deleted: video = self.device_video_root + '/' + file if os.path.isfile(video): try: os.remove(video) self.device_filelist.remove(file) self.log('Deleted: %s' % os.path.abspath(video)) except: self.log('Error deleting %s' % os.path.abspath(video)) else: self.device_filelist.remove(file) self.log('File no longer on device: %s' % os.path.abspath(video)) self.write_device_filelist() def copy_file(self, from_file, to_file): progress = self.log_class.progress_indicator copy_buffer = 1024 * 1024 # taken from the gPodder "gPodder_FSSync" class (copy_file_progress function) try: out_file = open( to_file, 'wb') except IOError, ioerror: print 'Error opening %s: %s' % (ioerror.filename, ioerror.strerror) return False try: in_file = open( from_file, 'rb') except IOError, ioerror: print 'Error opening %s: %s' % (ioerror.filename, ioerror.strerror) return False in_file.seek( 0, 2) bytes = in_file.tell() in_file.seek( 0) s = in_file.read(copy_buffer) self.log_class.progress_total_byes_copied = 0 self.log_class.progress_total_size = bytes self.log_class.progress_filename = os.path.basename(from_file) while s: try: out_file.write(s) except IOError, ioerror: self.log(ioerror.strerror, 1) try: out_file.close() except: pass try: self.log( '\nTrying to remove partially copied file: %s' % ( to_file )) os.unlink( to_file) self.log( '\nYeah! Unlinked %s at least..' % ( to_file )) except: self.log( '\nError while trying to unlink %s. OH MY!' % ( to_file )) return False progress(len(s), False) s = in_file.read(copy_buffer) progress(0, True) out_file.close() in_file.close() return True def copy_to_device(self): transfer_errors = False for file in self.to_be_copied: local_file = file[0] + file[1] device_file = self.device_video_root + '/' + file[1] self.log('Copying %s to %s.' % (file[1], os.path.abspath(self.device_video_root))) if os.path.isfile(device_file) and os.path.getsize(local_file) == os.path.getsize(device_file): self.device_filelist.append(file[1]) self.log(' \'--> File already on device: %s' % (file[1])) elif self.copy_file(file[0] + file[1], self.device_video_root + '/' + file[1]): if not file[1] in self.device_filelist: self.device_filelist.append(file[1]) self.log('Copied %s successfully.' % (file[0] + file[1]), 5) else: self.log('Error copying %s to %s' % (file[0] + file[1], self.device_video_root + '/' + file[1])) transfer_errors = True if not transfer_errors: self.write_device_filelist() def do_sync(self, test_run, copy_only, delete_only, sync, quiet, force_copy): self.create_filelist() self.read_device_filelist() self.compare_filelists(force_copy) if test_run: if len(self.to_be_copied) == 0: self.log('COPY: nothing.') elif len(self.to_be_copied) == 1: self.log('COPY: 1 file.') self.log(' \'--> %s' % self.to_be_copied[0][1]) else: self.log('COPY: %s files.' % len(self.to_be_copied)) for file in self.to_be_copied: self.log(' \'--> %s' % file[1]) if len(self.to_be_deleted) == 0: self.log('DELETE: nothing') elif len(self.to_be_deleted) == 1: self.log('DELETE: 1 file.') self.log(' \'--> %s' % self.to_be_deleted[0]) else: self.log('DELETE: %s files.' % len(self.to_be_deleted)) for file in self.to_be_deleted: self.log(' \'--> %s' % file) if copy_only: self.copy_to_device() if delete_only: self.remove_files() if sync: self.copy_to_device() self.remove_files() class Utils: def is_playable(self, file, allowed_audio_codecs, allowed_video_codecs): command = 'mplayer -msglevel all=-1 -identify -vo null -ao null -frames 0 "%s" 2>/dev/null' mplayer_output = os.popen(command % file).read().split('\n') audio_codec = "" video_codec = "" for line in mplayer_output: if line[:14] == 'ID_AUDIO_CODEC': audio_codec = line[15:] if line[:14] == 'ID_VIDEO_CODEC': video_codec = line[15:] good_audio = False good_video = False for acodec in allowed_audio_codecs: if audio_codec == acodec: good_audio = True for vcodec in allowed_video_codecs: if video_codec == vcodec: good_video = True return [ good_audio + good_video, good_audio, good_video ] class log(): def __init__(self): self.progress_total_size = 0 self.progress_total_byes_copied = 0 self.progress_filename = "" self.progress_number_of_stars = 50 def log(self, log_output, message_level = 3, newline = True): log_level = 4 # levels: 5 = for non-essential messages (debug output) # 3 = for normal cli messages (output for user) # 1 = for essential messages (file io errors) if message_level <= log_level: if newline: print log_output if not newline: sys.stdout.write(log_output) sys.stdout.flush() def progress_indicator(self, bytes_copied, finished): self.progress_total_byes_copied += bytes_copied percent = 100 * float(self.progress_total_byes_copied) / float(self.progress_total_size) percent_per_star = 100.0 / float(self.progress_number_of_stars) stars = percent // percent_per_star self.log('\r', 4, False) self.log(' \'--> Copying: [' + ('*' * int(stars)) + (' ' * int(self.progress_number_of_stars - stars)) + '] %d%%' % int(percent), 4, False) if finished: self.log('', 4) class commandLine(): def usage(self): print '' print 'Usage: archos-sync [-h] [-f] [-q] [-t|-c|-d|-s] [-m VIDEOS_DIR] DEVICE_MOUNT_POINT' print '' print 'Options:' print ' -h Help Print this message.' print ' -t Test Run Show what files would be copied and deleted.' print ' -c Copy Only Only copy new files to the device, don\'t delete.' print ' -d Delete Only Only delete files off the device.' print ' -s Sync Do a full sync to the device.' print ' -f Force Copy Videos to the device even if they have been deleted.' print ' -q Quiet Be less verbose [Not Yet Implemented].' print ' -D DVD Copy files that work with the DVD plugin.' print ' -P Podcast Copy files that work with the Podcast Plugin.' print ' -m VIDEOS_DIR The directory where videos are kept.' print '' print 'By default VIDEOS_DIR is ~/Movies/Miro/' print '' sys.exit(2) def get_command_line_opts(self): try: switches, args = getopt.getopt(sys.argv[1:], "htcdsqfDPm:") except getopt.GetoptError: self.usage() test_run, copy_only, delete_only, sync, quiet = False, False, False, False, False force_copy, dvd_plugin, podcast_plugin = False, False, False source_dir, dest_dir = '', '' for switch in switches: if switch[0] == '-h': self.usage() if switch[0] == '-t': test_run = True if switch[0] == '-c': copy_only = True if switch[0] == '-d': delete_only = True if switch[0] == '-s': sync = True if switch[0] == '-q': quiet = True if switch[0] == '-f': force_copy = True if switch[0] == '-m': source_dir = switch[1] if switch[0] == '-D': dvd_plugin = True if switch[0] == '-P': podcast_plugin = True if args: dest_dir = args[0] else: self.usage() if test_run + copy_only + delete_only + sync != 1: self.usage() else: return test_run, copy_only, delete_only, sync, quiet, force_copy, source_dir, dest_dir, dvd_plugin, podcast_plugin if __name__ == '__main__': test_run, copy_only, delete_only, sync, quiet, force_copy, source_dir, dest_dir, dvd_plugin, podcast_plugin = commandLine().get_command_line_opts() archosSync(source_dir, dest_dir, dvd_plugin, podcast_plugin).do_sync(test_run, copy_only, delete_only, sync, quiet, force_copy)