Class: Itch

Inherits:
Object
  • Object
show all
Defined in:
lib/itch.rb

Overview

A helper that controls the iTunes application.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(itunes_interface) ⇒ Itch

Create a new helper, and assign it an iTunes interface to use.



17
18
19
# File 'lib/itch.rb', line 17

def initialize(itunes_interface)
	@interface = itunes_interface
end

Instance Attribute Details

#interfaceObject (readonly)

The COM/OLE interface to iTunes.



13
14
15
# File 'lib/itch.rb', line 13

def interface
  @interface
end

Class Method Details

.create_itunes_interfaceObject

Create an OLE/COM link to iTunes.



23
24
25
26
27
28
29
# File 'lib/itch.rb', line 23

def Itch.create_itunes_interface
	begin
		WIN32OLE.new('iTunes.Application') or raise "Couldn't take control of iTunes."
	rescue WIN32OLERuntimeError => exception
		raise exception.exception("Couldn't take control of iTunes.  Could a prior instance be shutting down?")
	end
end

Instance Method Details

#delete_playlists(config) ⇒ Object

Delete specified playlists.



210
211
212
213
214
# File 'lib/itch.rb', line 210

def delete_playlists (config)
	with_option_values('delete-playlist', config) do |name|
		find_playlist(name).delete
	end
end

#get_playlists(config) ⇒ Object

Get playlists that will be operated on.



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/itch.rb', line 181

def get_playlists (config)

	playlists = Array.new
	playlists << @interface.LibraryPlaylist if config['library']
	if config['current-playlist']
		playlists << @interface.CurrentPlaylist or raise "Can't find current playlist."
	end
	if config['all-playlists']
		if library_playlists = @interface.LibrarySource.Playlists
			library_playlists.each {|playlist| playlists << playlist}
		else
			raise "No playlists found."
		end
	end
	with_option_values('playlist', config) do |name|
		playlists << find_playlist(name) or raise "Can't find playlist #{name}"
	end
	
	#If creation of playlist(s) requested, create them and add to the collection.
	with_option_values('create-playlist', config) do |name|
		playlists << @interface.CreatePlaylist(name) or raise "Can't create playlist #{name}"
	end
	
	playlists
	
end

#get_tracks(config, playlists) ⇒ Object

Find requested tracks.



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/itch.rb', line 218

def get_tracks (config, playlists)

	tracks = []

	#Add current track to list if desired.
	if config['current-track']
		current_track = @interface.CurrentTrack or raise "No currently active/playing track." #"x << y.CurrentTrack or raise" won't work; nil is false but [nil] is true.
		tracks << current_track
	end
		
	#Add highlighted tracks to list if desired.
	if config['selected-tracks']
		if selected_tracks = @interface.SelectedTracks
			selected_tracks.each {|track| tracks << track}
		else
			raise "No tracks selected."
		end
	end
	
	#Find tracks in selected playlists.
	playlists.each do |playlist|
	
		#If all tracks are to be processed, select them all.
		if config['all-tracks']
			playlist.Tracks.each {|track| tracks << track}
		#Otherwise, see if user wishes to search for tracks.
		else
			search_playlist = lambda {|option, field_id|
				with_option_values(option, config) do |terms|
					if (results = playlist.Search(terms, field_id))
						results.each {|track| tracks << track}
					else
						raise "No tracks found."
					end
				end
			}
			search_playlist.call('find', 0)
			search_playlist.call('visible-find', 1)
			search_playlist.call('find-artist', 2)
			search_playlist.call('find-album', 3)
			search_playlist.call('find-composer', 4)
			search_playlist.call('find-track-name', 5)
		end
		
		#Import files to library and add resulting tracks to list.
		with_option_values('add-file', config) do |path|
			tracks.concat(add_file(File.expand_path(path), playlist))
		end
		
	end
	
	tracks
	
end

#parse_options(arguments) ⇒ Object

Parse arguments and get a configuration.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/itch.rb', line 33

def parse_options(arguments)

	#Config will hold parsed option values.
	config = Hash.new

	#Set up option parser.
	options = OptionParser.new
	#Modify option parser to wrap descriptions.
	class <<options
		alias old_on on
		def on (*opts, &block)
			opts.map! do |option|
				if option.class == String and option.length > 40
					option = option.scan(/\S.{0,40}\S(?=\s|$)|\S+/)
				end
				option
			end
			old_on(*opts.flatten, &block)
		end
	end
	
	#Set up valid options.
	
	options.separator("Program help:")
	options.on("-h", "--help", TrueClass, "Display program help.") {
		puts options.help
		exit
	}
	
	options.separator("Playback controls:")
	options.on("-p", "--play-pause", TrueClass, "If currently paused, begin playing.  If currently playing, pause playback.") {|value| config['play-pause'] = value}
	options.on("--pause", TrueClass, "Pause playback.") {|value| config['pause'] = value}
	options.on("--play", TrueClass, "Play the current track.") {|value| config['play'] = value}
	options.on("-s", "--stop", TrueClass, "Stop playback.") {|value| config['stop'] = value}
	options.on("-n", "--next-track", TrueClass, "Go to the next track.") {|value| config['next-track'] = value}
	options.on("-N", "--previous-track", TrueClass, "Go to the previous track.") {|value| config['previous-track'] = value}
	options.on("-m", "--mute", TrueClass, "Mute the audio.") {|value| config['mute'] = value}
	options.on("-M", "--unmute", TrueClass, "Unmute the audio.") {|value| config['unmute'] = value}
	options.on("-v", "--volume number", Integer, "Set the volume to X percentage points.") {|value| config['volume'] = value}
	options.on("--volume-down [number]", Integer, "Decrease the volume by X percentage points (default 10).") {|value| config['volume-down'] = value || 10}
	options.on("--volume-up [number]", Integer, "Increase the volume by X percentage points (default 10).") {|value| config['volume-up'] = value || 10}
	options.on("--scan-to seconds", Integer, "Scan to an offset X seconds within the current track.") {|value| config['scan-to'] = value}
	options.on("--scan-backwards [seconds]", Integer, "Scan backwards X seconds within the current track (default 10).") {|value| config['scan-backwards'] = value || 10}
	options.on("--scan-forwards [seconds]", Integer, "Scan forwards X seconds within the current track (default 10).") {|value| config['scan-forwards'] = value || 10}
	options.on("--play-file name", Object, "Play the specified file or folder.") {|value| config['play-file'] = value}
	
	options.separator("Info on selected tracks:")
	options.on("-i", "--print-info format", Object, "For each track, print information in the given format.  If the following strings appear in the given format, they will be replaced with the corresponding track information:", 
		%q#"%a": artist#,
		%q#"%e": encoding#,
		%q#"%A": album#,
		%q#"%b": BPM (beats per minute)#,
		%q#"%c": composer#,
		%q#"%C": comment#,
		%q#"%d": disc number#,
		%q#"%D": disc count#,
		%q#"%E": Enabled status ("enabled" or "disabled")#,
		%q#"%l": location (file name/URL)#,
		%q#"%p": play count#,
		%q#"%q": equalizer#,
		%q#"%g": genre#,
		%q#"%G": grouping#,
		%q#"%n": name (title)#,
		%q#"%r": rating#,
		%q#"%s": skip count#,
		%q#"%t": track number#,
		%q#"%T": track count#,
		%q#"%v": volume adjustment#,
		%q#"%y": year#,
		%q#"%%": percent sign#
	) {|value| config['print-info'] = value}
	
	options.separator("General iTunes controls:")
	options.on("-a", "--add-file name", Object, "Add the specified file or folder to the library.") {|value| (config['add-file'] ||= []) << value}
	options.on("-q", "--quit", TrueClass, "Exit iTunes.") {|value| config['quit'] = value}
	options.on("--open-url url", Object, "Open the given URL.") {|value| config['open-url'] = value}
	options.on("--goto-store-home-page", TrueClass, "Go to the Store.") {|value| config['goto-store-home-page'] = value}
	options.on("--update-ipod", TrueClass, "Update the iPod.") {|value| config['update-ipod'] = value}
	
	options.separator("Playlist selection:")
	options.on("--library", TrueClass, "Operation will include the entire iTunes library.  Used by default unless other libraries are selected.") {|value| config['library'] = value}
	options.on("--current-playlist", TrueClass, "Operation will include the current playlist.") {|value| config['current-playlist'] = value}
	options.on("--playlist name", Object, "Operation will include the named playlist.  (This option can occur more than once.)") {|value| (config['playlist'] ||= []) << value}
	options.on("--all-playlists", TrueClass, "Operation will include all playlists.") {|value| config['all-playlists'] = value}
	options.on("--create-playlist name", Object, "Create a playlist with the specified name and include it in the operation.  (This option can occur more than once.)") {|value| (config['create-playlist'] ||= []) << value}
	options.on("--delete-playlist name", Object, "Delete the playlist with the specified name.  (This option can occur more than once.)") {|value| (config['delete-playlist'] ||= []) << value}
	
	options.separator("Track selection:")
	options.on("-f", "--find string", Object, "Operation will include all tracks in the specified playlist(s) where any field matches the specified string.  (This option can occur more than once.)") {|value| (config['find'] ||= []) << value}
	options.on("-c", "--current-track", TrueClass, "Operation will inclue the current track.") {|value| config['current-track'] = value}
	options.on("--selected-tracks", TrueClass, "Operation will include the selected tracks.") {|value| config['selected-tracks'] = value}
	options.on("--all-tracks", TrueClass, "Operation will include all tracks in the specified playlist(s).") {|value| config['all-tracks'] = value}
	options.on("--visible-find string", Object, "Operation will include all tracks in the specified playlist(s) where any visible field matches the specified string.  (This option can occur more than once.)") {|value| (config['visible-find'] ||= []) << value}
	options.on("--find-artist string", Object, "Operation will include all tracks in the specified playlist(s) where the artist matches the specified string.  (This option can occur more than once.)") {|value| (config['find-artist'] ||= []) << value}
	options.on("--find-album string", Object, "Operation will include all tracks in the specified playlist(s) where the album matches the specified string.  (This option can occur more than once.)") {|value| (config['find-album'] ||= []) << value}
	options.on("--find-composer string", Object, "Operation will include all tracks in the specified playlist(s) where the composer matches the specified string.  (This option can occur more than once.)") {|value| (config['find-composer'] ||= []) << value}
	options.on("--find-track-name string", Object, "Operation will include all tracks in the specified playlist(s) where the track name matches the specified string.  (This option can occur more than once.)") {|value| (config['find-track-name'] ||= []) << value}
	options.on("-F", "--play-found", TrueClass, "Play the first of the selected tracks.") {|value| config['play-found'] = value}
	
	options.separator("Set track info:")
	options.on("--set-artist name", Object, "Set the artist for each track.") {|value| config['set-artist'] = value}
	options.on("--set-album name", Object, "Set the album for each track.") {|value| config['set-album'] = value}
	options.on("--set-bpm number", Integer, "Set the beats per minute for each track.") {|value| config['set-bpm'] = value}
	options.on("--set-comment string", Object, "Set the comment for each track.") {|value| config['set-comment'] = value}
	options.on("--set-composer string", Object, "Set the composer for each track.") {|value| config['set-composer'] = value}
	options.on("--set-disc-number number", Integer, "For each track, set the disc number.  (Used with multi-disc albums.)") {|value| config['set-disc-number'] = value}
	options.on("--set-disc-count number", Integer, "For each track, set the number of discs in the album.  (Used with multi-disc albums.)") {|value| config['set-disc-count'] = value}
	options.on("--set-enabled", TrueClass, "Enable the check box for each track.") {|value| config['set-enabled'] = value}
	options.on("--set-disabled", TrueClass, "Disable the check box for each track.") {|value| config['set-disabled'] = value}
	options.on("--set-eq name", Object, "Set the equalizer to the named preset.  Use 'None' to disable.") {|value| config['set-eq'] = value}
	options.on("--set-genre name", Object, "Set the genre for each track.") {|value| config['set-genre'] = value}
	options.on("--set-grouping string", Object, "Set the grouping for each track.") {|value| config['set-grouping'] = value}
	options.on("--set-name name", Object, "Set the name (title) for each track.") {|value| config['set-name'] = value}
	options.on("--set-play-count number", Integer, "Set the play count for each track.") {|value| config['set-play-count'] = value}
	options.on("--set-rating number", Integer, "Set the rating for each track.  Valid values are 0 through 5.") {|value| config['set-rating'] = value}
	options.on("--set-skip-count number", Integer, "Set the skip count for each track.") {|value| config['set-skip-count'] = value}
	options.on("--set-track-number number", Integer, "For each track, set its album track number.") {|value| config['set-track-number'] = value}
	options.on("--set-track-count number", Integer, "For each track, set the number of tracks on its album.") {|value| config['set-track-count'] = value}
	options.on("--set-track-volume percent", Integer, "Set the volume adjustment percentage for each track, from -100 to 100.  Negative numbers decrease the volume, positive numbers increase it.  0 means no adjustment.") {|value| config['set-track-volume'] = value}
	options.on("--set-year number", Integer, "Set the year of publication for each track.") {|value| config['set-year'] = value}
	
	options.separator("Troubleshooting:")
	options.on("--debug", TrueClass, "When an error occurs, show a more detailed message.") {|value| config['debug'] = value}
	
	#Parse the options, printing usage if parsing fails.
	options.parse(arguments) rescue puts "#{$!}\nType '#{$0} --help' for valid options."
	
	config
	
end

#perform_general_operations(config) ⇒ Object

Perform the operations specified in the config that affect iTunes itself.



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/itch.rb', line 275

def perform_general_operations (config)

	specified?(config, 'mute') {@interface.Mute = 1}
	specified?(config, 'unmute') {@interface.Mute = 0}
	specified?(config, 'next-track') {@interface.NextTrack}
	specified?(config, 'scan-backwards') {|v| @interface.PlayerPosition -= v}
	specified?(config, 'scan-forwards') {|v| @interface.PlayerPosition += v}
	specified?(config, 'scan-to') {|v| @interface.PlayerPosition = v}
	specified?(config, 'volume-down') {|v| @interface.SoundVolume -= v}
	specified?(config, 'volume-up') {|v| @interface.SoundVolume += v}
	specified?(config, 'volume') {|v| @interface.SoundVolume = v}
	specified?(config, 'play-file') {|v| @interface.PlayFile(v)}
	specified?(config, 'open-url') {|v| @interface.OpenURL(v)}
	specified?(config, 'goto-store-home-page') {@interface.GotoMusicStoreHomePage}
	specified?(config, 'update-ipod') {@interface.UpdateIPod}
	specified?(config, 'pause') {@interface.Pause}
	specified?(config, 'play-pause') {@interface.PlayPause}
	specified?(config, 'previous-track') {@interface.PreviousTrack}
	specified?(config, 'stop') {@interface.Stop}
	specified?(config, 'play') {@interface.Play}
	specified?(config, 'quit') {@interface.Quit}
	
end

#perform_track_operations(config, tracks) ⇒ Object

Perform the operations specified in the config that affect the selected tracks.



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/itch.rb', line 301

def perform_track_operations (config, tracks)
	
	#Operate on selected tracks.
	tracks.each do |track|
		specified?(config, 'set-artist') {|v| track.Artist = v}
		specified?(config, 'set-album') {|v| track.Album = v}
		specified?(config, 'set-bpm') {|v| track.BPM = v}
		specified?(config, 'set-comment') {|v| track.Comment = v}
		specified?(config, 'set-composer') {|v| track.Composer = v}
		specified?(config, 'set-disc-count') {|v| track.DiscCount = v}
		specified?(config, 'set-disc-number') {|v| track.DiscNumber = v}
		specified?(config, 'set-enabled') {|v| track.Enabled = true}
		specified?(config, 'set-disabled') {|v| track.Enabled = false}
		specified?(config, 'set-eq') {|v| track.EQ = v}
		specified?(config, 'set-genre') {|v| track.Genre = v}
		specified?(config, 'set-grouping') {|v| track.Grouping = v}
		specified?(config, 'set-name') {|v| track.Name = v}
		specified?(config, 'set-play-count') {|v| track.PlayedCount = v}
		specified?(config, 'set-rating') {|v| track.Rating = v * 20} #Rating is stored as a number from 1-100 internally, but 1-5 in interface.
		specified?(config, 'set-skip-count') {|v| track.SkippedCount = v}
		specified?(config, 'set-track-count') {|v| track.TrackCount = v}
		specified?(config, 'set-track-number') {|v| track.TrackNumber = v}
		specified?(config, 'set-track-volume') {|v| track.VolumeAdjustment = v}
		specified?(config, 'set-year') {|v| track.Year = v}
		specified?(config, 'print-info') {|v| puts track_info(track, v)}
		specified?(config, 'set-artist') {|v| track.Artist = v}
		#TODO: Re-implement add-to-playlist.
	end
	
	#Play first of selected tracks if requested.
	specified?(config, 'play-found') {tracks[0].Play unless tracks.empty?}
	
end

#set_default_options(config = Hash.new) ⇒ Object

Set defaults for an existing set of options.



166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/itch.rb', line 166

def set_default_options(config=Hash.new)

	config['library'] = true unless (
		config.has_key?('library') or
		config.has_key?('current-playlist') or
		config.has_key?('playlist') or
		config.has_key?('all-playlists') or
		config.has_key?('create-playlist')
	)
	
	config
	
end

#track_info(track, format) ⇒ Object

Take the specified format string with %x flags, and substitute info from the given track.



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/itch.rb', line 337

def track_info (track, format)

	#Double percent signs should not be substituted, so work around them.
	segments = format.split(/%%/)
	
	#Substitute track info for markers.
	output_segments = segments.map do |segment|
		segment.gsub!(/%a/) {track.Artist}
		segment.gsub!(/%e/) {track.KindAsString} #The encoding.
		segment.gsub!(/%A/) {track.Album}
		segment.gsub!(/%b/) {track.BPM.to_s}
		segment.gsub!(/%c/) {track.Composer}
		segment.gsub!(/%C/) {track.Comment}
		segment.gsub!(/%d/) {track.DiscNumber.to_s}
		segment.gsub!(/%D/) {track.DiscCount.to_s}
		segment.gsub!(/%E/) {track.Enabled == 1 ? 'enabled' : 'disabled'}
		segment.gsub!(/%l/) {track.Location}
		segment.gsub!(/%p/) {track.PlayedCount.to_s}
		segment.gsub!(/%q/) {track.EQ}
		segment.gsub!(/%g/) {track.Genre}
		segment.gsub!(/%G/) {track.Grouping}
		segment.gsub!(/%n/) {track.Name}
		segment.gsub!(/%r/) {(track.Rating.to_i / 20).to_s}
		segment.gsub!(/%s/) {track.SkippedCount.to_s}
		segment.gsub!(/%t/) {track.TrackNumber.to_s}
		segment.gsub!(/%T/) {track.TrackCount.to_s}
		segment.gsub!(/%v/) {volume = track.VolumeAdjustment; (1 .. 98).include?(volume) ? volume.next.to_s : volume.to_s}
		segment.gsub!(/%y/) {track.Year.to_s}
		segment
	end

	#Replace double percent signs with single percent signs and return.
	output = output_segments.join('%')
	output += '%' if format =~ /%%$/ #split() doesn't create final empty field if string ends in a delimiter.
	output
	
end