utility to sync an itunes playlist to a directory (like a mounted sd card/phone)

restrict pathnames to avoid indexing problems on some players/filesystems

use rsync's --progress flag

make sure the destination volume can take the amount of data being
synced

+32 -17
+32 -17
itunes-rsync.rb
··· 14 14 # Redistribution and use in source and binary forms, with or without 15 15 # modification, are permitted provided that the following conditions 16 16 # are met: 17 - # 17 + # 18 18 # 1. Redistributions of source code must retain the above copyright 19 19 # notice, this list of conditions and the following disclaimer. 20 20 # 2. Redistributions in binary form must reproduce the above copyright ··· 22 22 # documentation and/or other materials provided with the distribution. 23 23 # 3. The name of the author may not be used to endorse or promote products 24 24 # derived from this software without specific prior written permission. 25 - # 25 + # 26 26 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR 27 27 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 28 28 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ··· 55 55 end 56 56 else 57 57 puts "error: directory \"#{destdir}\" does not exist, exiting" 58 - exit 58 + exit 1 59 59 end 60 60 61 - print "querying itunes for playlist \"#{playlist}\"... " 61 + puts "querying itunes for playlist \"#{playlist}\"..." 62 62 63 63 itunes = Appscript.app.by_name("iTunes", ITunesFix) 64 + begin 65 + if !(itpl = itunes.playlists[playlist]) 66 + raise "no such playlist with that name" 67 + end 64 68 65 - itpl = itunes.playlists[playlist] 69 + tracks = itpl.file_tracks.get.map{|t| t.location.get.path } 70 + rescue => e 71 + puts "could not find playlist #{playlist.inspect}, exiting" 72 + exit 1 73 + end 66 74 67 - if !itpl 68 - puts "could not locate, exiting" 69 - exit 70 - end 75 + # figure out how much space we're going to take 76 + bytes = 0 77 + tracks.each{|t| bytes += File.size(t) } 78 + mbytes = bytes.to_f / (1024 * 1024) 71 79 72 - tracks = itpl.file_tracks.get.map{|t| t.location.get.path } 80 + puts "found #{tracks.length} track#{tracks.length == 1 ? "" : "s"} with " + 81 + "size #{sprintf("%0.2fMb", mbytes)}" 73 82 74 - puts "found #{tracks.length} track#{tracks.length == 1 ? '' : 's'}." 83 + # make sure the destination can take it 84 + df_m = `df -m #{destdir}`.split("\n").last.split(" ")[1].to_i 85 + if mbytes > df_m 86 + puts "error: #{destdir} has size of #{df_m}Mb, need #{mbytes.ceil}Mb to sync" 87 + exit 1 88 + end 75 89 76 - # figure out where all of them are stored by checking for the greatest common 90 + # figure out where all tracks are stored by checking for the greatest common 77 91 # directory of every track 78 92 gcd = "" 79 93 (1 .. tracks.map{|t| t.length }.max).each do |s| ··· 93 107 end 94 108 end 95 109 96 - # setup work dir 97 110 td = `mktemp -d /tmp/itunes-rsync.XXXXX`.strip 98 111 99 - # mirror directory structure and create symlinks 112 + # link each track into the workspace 100 113 print "linking files under #{td}/... " 101 - 102 114 tracks.each do |t| 103 115 shortpath = t[gcd.length .. t.length - 1] 104 116 tmppath = "#{td}/#{shortpath}" 105 117 118 + # restrict pathnames to avoid indexing problems on some players/filesystems 119 + tmppath.gsub!(/[^A-Za-z0-9\.\/,' -]/, "_") 120 + 106 121 if !Dir[File.dirname(tmppath)].any? 107 - # i'm too lazy to emulate -p with Dir.mkdir 108 122 system("mkdir", "-p", File.dirname(tmppath)) 109 123 end 110 124 ··· 115 129 116 130 # times don't ever seem to match up, so only check size 117 131 puts "rsyncing to #{destdir}... " 118 - system("rsync", "-Lrv", "--size-only", "--delete", "#{td}/", destdir) 132 + system("rsync", "-Lrv", "--size-only", "--progress", "--delete", 133 + "#{td}/", destdir) 119 134 120 135 print "cleaning up... " 121 136 system("rm", "-rf", td)