···11-rsync the files of an itunes playlist with another directory, most likely a usb
22-music device.
11+A couple scripts for maintaining a directory of music files (probably located
22+on a USB music player or SD card) from an iTunes playlist.
33+44+Requires the `appscript` gem.
55+66+###itunes-rsync.rb
77+Query a given iTunes playlist, build a scratch directory symlinking all files
88+of that playlist into it, then run `rsync` from the scratch directory to the
99+path given.
1010+1111+**Note:** `rsync`'s `--delete` option is used, so it will delete any files in
1212+the given path that are not found in the playlist.
1313+1414+Usage: `ruby itunes-rsync.rb "ipod (auto)" /Volumes/music/iTunes/`
1515+1616+###playlist_album_shuffle.rb
1717+Query a given iTunes playlist, organize all tracks into albums, shuffle the
1818+albums, and then print the filename of each track in each album (in its
1919+original track order) to STDOUT, relative to the path given.
2020+2121+Mimics the album shuffle functionality of iTunes and older iPods and can be
2222+used to create a few album-shuffled `.pls` playlists of a given list of tracks
2323+for players that don't natively support album shuffle.
2424+2525+**Note:** The path for the second argument is the path relative to all media as
2626+it would be mounted by the player. Nothing is saved to that path and the
2727+output is all done to STDOUT.
2828+2929+Usage: `ruby playlist_album_shuffle.rb "ipod (auto)" /mnt/SD1/iTunes/ > /Volumes/AK100/album\ shuffle\ 1.pls`
3030+3131+###License
33244-creates symlinks in a scratch directory pointing to the real destination
55-directory, then uses rsync to actually copy them out to the destination.
3333+Copyright (c) 2009, 2012, 2013 joshua stein <jcs@jcs.org>
3434+3535+Redistribution and use in source and binary forms, with or without
3636+modification, are permitted provided that the following conditions
3737+are met:
63877-requires the `appscript` gem.
3939+1. Redistributions of source code must retain the above copyright
4040+ notice, this list of conditions and the following disclaimer.
4141+2. Redistributions in binary form must reproduce the above copyright
4242+ notice, this list of conditions and the following disclaimer in the
4343+ documentation and/or other materials provided with the distribution.
4444+3. The name of the author may not be used to endorse or promote products
4545+ derived from this software without specific prior written permission.
84699-usage: `ruby itunes-rsync.rb "ipod (auto)" /Volumes/music/`
4747+THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
4848+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
4949+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
5050+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
5151+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
5252+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
5353+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
5454+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
5555+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
5656+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
···11#!/usr/bin/ruby
22-# $Id: itunes-rsync.rb,v 1.5 2009/01/27 09:11:14 jcs Exp $
33-#
44-# rsync the files of an itunes playlist with another directory, most likely a
55-# usb music device.
66-#
77-# creates symlinks in a scratch directory pointing to the real destination
88-# directory, then uses rsync to actually copy them out to the destination.
99-#
1010-# requires the appscript gem
1111-#
1212-# Copyright (c) 2009, 2012 joshua stein <jcs@jcs.org>
22+# Copyright (c) 2009, 2012, 2013 joshua stein <jcs@jcs.org>
133#
144# Redistribution and use in source and binary forms, with or without
155# modification, are permitted provided that the following conditions
···3525# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3626#
37273838-require "rubygems"
3939-require "appscript"
4040-$: << File.dirname(__FILE__)
4141-require "appscript_itunes_fix"
2828+require File.dirname(__FILE__) << "/lib/itunes"
42294330if !ARGV[1]
4431 puts "usage: #{$0} <itunes playlist> <destination directory>"
···60476148puts "querying itunes for playlist \"#{playlist}\"..."
62496363-itunes = Appscript.app.by_name("iTunes", ITunesFix)
6464-begin
6565- if !(itpl = itunes.playlists[playlist])
6666- raise "no such playlist with that name"
6767- end
5050+it = ITunes.new
5151+pl = it.playlist(playlist)
68526969- tracks = itpl.file_tracks.get.map{|t| t.location.get.path }
7070-rescue => e
7171- puts "could not find playlist #{playlist.inspect}, exiting"
7272- exit 1
7373-end
5353+mbytes = pl.total_bytes.to_f / (1024 * 1024)
74547575-# figure out how much space we're going to take
7676-bytes = 0
7777-tracks.each{|t| bytes += File.size(t) }
7878-mbytes = bytes.to_f / (1024 * 1024)
7979-8080-puts "found #{tracks.length} track#{tracks.length == 1 ? "" : "s"} with " +
8181- "size #{sprintf("%0.2fMb", mbytes)}"
5555+puts "found #{pl.tracks.length} track#{pl.tracks.length == 1 ? "" : "s"} " <<
5656+ "with size " << sprintf("%0.2fMb", mbytes)
82578383-# make sure the destination can take it
5858+# make sure the destination can hold this playlist
8459df_m = `df -m #{destdir}`.split("\n").last.split(" ")[1].to_i
8560if mbytes > df_m
8661 puts "error: #{destdir} has size of #{df_m}Mb, need #{mbytes.ceil}Mb to sync"
8762 exit 1
8863end
89649090-# figure out where all tracks are stored by checking for the greatest common
9191-# directory of every track
9292-gcd = ""
9393-(1 .. tracks.map{|t| t.length }.max).each do |s|
9494- piece = tracks[0][0 .. s - 1]
9595-9696- ok = true
9797- tracks.each do |t|
9898- if t[0 .. s - 1] != piece
9999- ok = false
100100- end
101101- end
102102-103103- if ok
104104- gcd = piece
105105- else
106106- break
107107- end
108108-end
109109-11065td = `mktemp -d /tmp/itunes-rsync.XXXXX`.strip
1116611267# link each track into the workspace
11368print "linking files under #{td}/... "
114114-tracks.each do |t|
115115- shortpath = t[gcd.length .. t.length - 1]
116116- tmppath = "#{td}/#{shortpath}"
117117-118118- # restrict pathnames to avoid indexing problems on some players/filesystems
119119- tmppath.gsub!(/[^A-Za-z0-9\.\/,' -]/, "_")
6969+pl.tracks.each do |t|
7070+ tmppath = td + "/" + t.safe_filename_without_gcd
1207112172 if !Dir[File.dirname(tmppath)].any?
12273 system("mkdir", "-p", File.dirname(tmppath))
12374 end
12475125125- File.symlink(t, tmppath)
7676+ File.symlink(t.location, tmppath)
12677end
1277812879puts "done."
12980130130-# times don't ever seem to match up, so only check size
8181+# file times don't ever seem to match up, so only check size
13182puts "rsyncing to #{destdir}... "
132132-system("rsync", "-Lrv", "--size-only", "--progress", "--delete",
133133- "#{td}/", destdir)
8383+system("rsync", "-Lrv", "--size-only", "--progress", "--delete", "#{td}/",
8484+ destdir)
1348513586print "cleaning up... "
13687system("rm", "-rf", td)
8888+13789puts "done."
+122
lib/itunes.rb
···11+#!/usr/bin/env ruby
22+# Copyright (c) 2009, 2012, 2013 joshua stein <jcs@jcs.org>
33+#
44+# Redistribution and use in source and binary forms, with or without
55+# modification, are permitted provided that the following conditions
66+# are met:
77+#
88+# 1. Redistributions of source code must retain the above copyright
99+# notice, this list of conditions and the following disclaimer.
1010+# 2. Redistributions in binary form must reproduce the above copyright
1111+# notice, this list of conditions and the following disclaimer in the
1212+# documentation and/or other materials provided with the distribution.
1313+# 3. The name of the author may not be used to endorse or promote products
1414+# derived from this software without specific prior written permission.
1515+#
1616+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
1717+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
1818+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
1919+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
2020+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
2121+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
2222+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
2323+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2424+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
2525+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2626+#
2727+2828+require "rubygems"
2929+require "appscript"
3030+$: << File.dirname(__FILE__)
3131+require "appscript_itunes_fix"
3232+3333+class ITunes
3434+ def initialize
3535+ @as = Appscript.app.by_name("iTunes", ITunesFix)
3636+ @_playlists = {}
3737+ end
3838+3939+ def playlist(name)
4040+ @_playlists[name] ||= Playlist.new(self, name)
4141+ end
4242+4343+ def as_playlist(name)
4444+ @as.playlists[name]
4545+ end
4646+end
4747+4848+class Playlist
4949+ def initialize(itunes, name)
5050+ @itunes = itunes
5151+ @as_playlist = @itunes.as_playlist(name)
5252+ end
5353+5454+ def tracks
5555+ @tracks ||= @as_playlist.file_tracks.get.map{|t| Track.new(self, t) }
5656+ end
5757+5858+ def total_bytes
5959+ tracks.map{|t| File.size(t.location) }.inject(:+)
6060+ end
6161+6262+ # figure out where all tracks are stored by checking for the greatest common
6363+ # directory of every track
6464+ @_gcd = nil
6565+ def gcd
6666+ return @_gcd if @_gcd
6767+6868+ @_gcd = ""
6969+7070+ locs = self.tracks.map{|t| t.location }.sort_by{|p| p.length }
7171+ (0 .. locs[0].length).each do |pos|
7272+ try = locs[0][0 ... pos]
7373+7474+ ok = true
7575+ locs.each do |loc|
7676+ if loc[0 ... try.length] != try
7777+ ok = false
7878+ break
7979+ end
8080+ end
8181+8282+ if ok
8383+ @_gcd = try
8484+ else
8585+ break
8686+ end
8787+ end
8888+8989+ @_gcd
9090+ end
9191+end
9292+9393+class Track
9494+ attr_accessor :album_artist, :artist, :album, :compilation, :disc_number,
9595+ :location, :name, :track_number
9696+9797+ def initialize(playlist, track)
9898+ @playlist = playlist
9999+100100+ @album_artist = track.album_artist.get.to_s
101101+ @artist = track.artist.get.to_s
102102+ @album = track.album.get.to_s
103103+ @compilation = !!track.compilation.get
104104+ @disc_number = track.disc_number.get.to_i
105105+ @location = track.location.get.path.to_s
106106+ @name = track.name.get.to_s
107107+ @track_number = track.track_number.get.to_i
108108+ end
109109+110110+ def compilation?
111111+ @compilation
112112+ end
113113+114114+ def album_artist_or_artist
115115+ @album_artist.strip == "" ? @artist : @album_artist
116116+ end
117117+118118+ # restrict pathname to avoid indexing problems on some players/filesystems
119119+ def safe_filename_without_gcd
120120+ @location[@playlist.gcd.length .. -1].gsub(/[^A-Za-z0-9\.\/,' -]/, "_")
121121+ end
122122+end
+61
playlist_album_shuffle.rb
···11+#!/usr/bin/ruby
22+# Copyright (c) 2013 joshua stein <jcs@jcs.org>
33+#
44+# Redistribution and use in source and binary forms, with or without
55+# modification, are permitted provided that the following conditions
66+# are met:
77+#
88+# 1. Redistributions of source code must retain the above copyright
99+# notice, this list of conditions and the following disclaimer.
1010+# 2. Redistributions in binary form must reproduce the above copyright
1111+# notice, this list of conditions and the following disclaimer in the
1212+# documentation and/or other materials provided with the distribution.
1313+# 3. The name of the author may not be used to endorse or promote products
1414+# derived from this software without specific prior written permission.
1515+#
1616+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
1717+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
1818+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
1919+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
2020+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
2121+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
2222+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
2323+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2424+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
2525+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2626+#
2727+2828+require File.dirname(__FILE__) + "/lib/itunes"
2929+3030+if !ARGV[1]
3131+ puts "usage: #{$0} <itunes playlist> <root directory of media>"
3232+ exit
3333+end
3434+3535+playlist = ARGV[0]
3636+destdir = ARGV[1]
3737+if !destdir.match(/\/$/)
3838+ destdir << "/"
3939+end
4040+4141+it = ITunes.new
4242+pl = it.playlist(playlist)
4343+4444+albums = {}
4545+pl.tracks.each do |track|
4646+ artist = (track.compilation? ? "Various" : track.album_artist_or_artist)
4747+ album = "#{artist} - #{track.album}"
4848+ albums[album] ||= []
4949+5050+ albums[album].push track
5151+end
5252+5353+# on ruby 1.9, Array#shuffle is a proper fisher-yates algorithm
5454+albums.keys.shuffle.each do |album|
5555+ # sort album tracks by disc number, then track number
5656+ albums[album].sort_by{|t| sprintf("%02d", t.disc_number) << "-" <<
5757+ sprintf("%02d", t.track_number) << "-" << t.name }.each do |t|
5858+ # show the track relative to the playlist's gcd
5959+ print destdir + t.safe_filename_without_gcd + "\r\n"
6060+ end
6161+end