utility to sync an itunes playlist to a directory (like a mounted sd card/phone)
1#!/usr/bin/env ruby
2# Copyright (c) 2009, 2012, 2013 joshua stein <jcs@jcs.org>
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions
6# are met:
7#
8# 1. Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10# 2. Redistributions in binary form must reproduce the above copyright
11# notice, this list of conditions and the following disclaimer in the
12# documentation and/or other materials provided with the distribution.
13# 3. The name of the author may not be used to endorse or promote products
14# derived from this software without specific prior written permission.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
17# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
20# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26#
27
28require "rubygems"
29require "appscript"
30$: << File.dirname(__FILE__)
31require "appscript_itunes_fix"
32
33class ITunes
34 def initialize
35 @as = Appscript.app.by_name("iTunes", ITunesFix)
36 @_playlists = {}
37 end
38
39 def playlist(name)
40 @_playlists[name] ||= Playlist.new(self, name)
41 end
42
43 def as_playlist(name)
44 @as.playlists[name]
45 end
46end
47
48class Playlist
49 def initialize(itunes, name)
50 @itunes = itunes
51 @as_playlist = @itunes.as_playlist(name)
52 end
53
54 def tracks
55 @tracks ||= @as_playlist.file_tracks.get.map{|t| Track.new(self, t) }
56 end
57
58 def total_bytes
59 tracks.map{|t| File.size(t.location) }.inject(:+)
60 end
61
62 # figure out where all tracks are stored by checking for the greatest common
63 # directory of every track
64 @_gcd = nil
65 def gcd
66 return @_gcd if @_gcd
67
68 @_gcd = ""
69
70 locs = self.tracks.map{|t| t.location }.sort_by{|p| p.length }
71 (0 .. locs[0].length).each do |pos|
72 try = locs[0][0 ... pos]
73
74 ok = true
75 locs.each do |loc|
76 if loc[0 ... try.length] != try
77 ok = false
78 break
79 end
80 end
81
82 if ok
83 @_gcd = try
84 else
85 break
86 end
87 end
88
89 @_gcd
90 end
91end
92
93class Track
94 attr_accessor :album_artist, :artist, :album, :compilation, :disc_number,
95 :location, :name, :track_number
96
97 def initialize(playlist, track)
98 @playlist = playlist
99
100 @album_artist = track.album_artist.get.to_s
101 @artist = track.artist.get.to_s
102 @album = track.album.get.to_s
103 @compilation = !!track.compilation.get
104 @disc_number = track.disc_number.get.to_i
105 @location = track.location.get.path.to_s
106 @name = track.name.get.to_s
107 @track_number = track.track_number.get.to_i
108 end
109
110 def compilation?
111 @compilation
112 end
113
114 def album_artist_or_artist
115 @album_artist.strip == "" ? @artist : @album_artist
116 end
117
118 # restrict pathname to avoid indexing problems on some players/filesystems
119 def safe_filename_without_gcd
120 @location[@playlist.gcd.length .. -1].gsub(/[^A-Za-z0-9\.\/,' -]/, "_")
121 end
122end