itunes-command 1.4-universal-darwin-9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/itunes-command +3 -0
- data/itunes-command.rb +416 -0
- metadata +53 -0
data/bin/itunes-command
ADDED
data/itunes-command.rb
ADDED
@@ -0,0 +1,416 @@
|
|
1
|
+
#!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby
|
2
|
+
|
3
|
+
# If the shebang line doesn't point to your OS X installation of ruby, find it
|
4
|
+
# and correct the path above.
|
5
|
+
#
|
6
|
+
# itunes-command.rb
|
7
|
+
#
|
8
|
+
# Requirements:
|
9
|
+
#
|
10
|
+
# OS X Leopard
|
11
|
+
#
|
12
|
+
# Instructions:
|
13
|
+
#
|
14
|
+
# Save this file as 'itunes-command.rb' or whatever else you wish to call it.
|
15
|
+
#
|
16
|
+
# Run it with either:
|
17
|
+
#
|
18
|
+
# ruby itunes-command.rb
|
19
|
+
#
|
20
|
+
# or
|
21
|
+
#
|
22
|
+
# ./itunes-command.rb
|
23
|
+
#
|
24
|
+
# or itunes-command.rb
|
25
|
+
#
|
26
|
+
# The 2nd and 3rd options assume that you made the file executable with
|
27
|
+
#
|
28
|
+
# chmod u+x itunes-command.rb
|
29
|
+
#
|
30
|
+
# The 3d options also assumes that itunes-command.rb is on your PATH.
|
31
|
+
#
|
32
|
+
# Author: Daniel Choi
|
33
|
+
# Location: Cambridge, MA
|
34
|
+
# Affiliation: http://betahouse.org
|
35
|
+
# Email: dhchoi@gmail.com
|
36
|
+
# Project Homepage: http://danielchoi.com/software/itunes-command.html
|
37
|
+
#
|
38
|
+
# License: MIT
|
39
|
+
#
|
40
|
+
# Copyright (c) 2008 Daniel Choi
|
41
|
+
#
|
42
|
+
# Permission is hereby granted, free of charge, to any person
|
43
|
+
# obtaining a copy of this software and associated documentation
|
44
|
+
# files (the "Software"), to deal in the Software without
|
45
|
+
# restriction, including without limitation the rights to use,
|
46
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
47
|
+
# copies of the Software, and to permit persons to whom the
|
48
|
+
# Software is furnished to do so, subject to the following
|
49
|
+
# conditions:
|
50
|
+
#
|
51
|
+
# The above copyright notice and this permission notice shall be
|
52
|
+
# included in all copies or substantial portions of the Software.
|
53
|
+
#
|
54
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
55
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
56
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
57
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
58
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
59
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
60
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
61
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
62
|
+
|
63
|
+
require 'rubygems'
|
64
|
+
$:.unshift File.dirname(__FILE__)
|
65
|
+
require 'readline'
|
66
|
+
require 'osx/cocoa'
|
67
|
+
OSX.require_framework 'ScriptingBridge'
|
68
|
+
|
69
|
+
class ITunes
|
70
|
+
QUEUE_PLAYLIST = 'itunes-command'
|
71
|
+
def initialize
|
72
|
+
@app = OSX::SBApplication.applicationWithBundleIdentifier("com.apple.iTunes")
|
73
|
+
end
|
74
|
+
|
75
|
+
# Delegate all other methods to the SBAppliation object for iTunes
|
76
|
+
def method_missing(message, *args)
|
77
|
+
@app.send(message, *args)
|
78
|
+
end
|
79
|
+
|
80
|
+
def party_shuffle
|
81
|
+
playlists.detect {|x| x.name == "Party Shuffle"}
|
82
|
+
end
|
83
|
+
|
84
|
+
def playlists
|
85
|
+
@app.sources.first.playlists
|
86
|
+
end
|
87
|
+
|
88
|
+
def playlist_by_name(name)
|
89
|
+
playlists.detect {|p| p.name == name}
|
90
|
+
end
|
91
|
+
|
92
|
+
def library
|
93
|
+
playlists.first
|
94
|
+
end
|
95
|
+
|
96
|
+
def artists(playlist=library)
|
97
|
+
return {} if playlist.tracks.empty?
|
98
|
+
artists = playlist.tracks.arrayByApplyingSelector("artist").select {|x| x.to_s =~ /\w/}
|
99
|
+
# count tracks per artist, which is represented to number of occurrences
|
100
|
+
artist = Hash.new(0)
|
101
|
+
artists.each do |name|
|
102
|
+
artist[name.to_s] += 1
|
103
|
+
end
|
104
|
+
artist
|
105
|
+
end
|
106
|
+
|
107
|
+
# Pass in a string to get matching tracks. Pass in an integer to search by
|
108
|
+
# databaseID
|
109
|
+
def find_track(query)
|
110
|
+
if query.is_a?(String)
|
111
|
+
library.searchFor_only(query, nil)
|
112
|
+
elsif query.is_a?(Integer) # lookup by databaseID
|
113
|
+
predicate = OSX::NSPredicate.predicateWithFormat("databaseID == #{query}")
|
114
|
+
# assume that only one track matches, and return it
|
115
|
+
library.tracks.filteredArrayUsingPredicate(predicate).first
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def find_tracks(track_ids)
|
120
|
+
predicate = OSX::NSPredicate.predicateWithFormat("databaseID IN {%s}" % track_ids.join(','))
|
121
|
+
library.tracks.filteredArrayUsingPredicate(predicate)
|
122
|
+
end
|
123
|
+
|
124
|
+
def add_track_to_playlist(track, playlist)
|
125
|
+
if playlist.is_a?(String)
|
126
|
+
playlist = playlist_by_name(playlist)
|
127
|
+
end
|
128
|
+
track = track.duplicateTo(playlist)
|
129
|
+
# The following does not work:
|
130
|
+
# arg1 = OSX::NSArray.arrayWithArray([track])
|
131
|
+
# puts arg1.class
|
132
|
+
# puts playlist.class
|
133
|
+
# puts @app.add_to_(arg1, playlist)
|
134
|
+
end
|
135
|
+
|
136
|
+
def add_tracks_to_playlist(tracks, playlist, credit=nil)
|
137
|
+
if playlist.is_a?(String)
|
138
|
+
playlist = playlist_by_name(playlist)
|
139
|
+
end
|
140
|
+
if tracks.is_a?(Array) # need to convert Ruby array into NSArray
|
141
|
+
tracks = OSX::NSArray.arrayWithArray(tracks)
|
142
|
+
end
|
143
|
+
tracks.makeObjectsPerformSelector_withObject("setEnabled:", 1)
|
144
|
+
if credit
|
145
|
+
tracks.makeObjectsPerformSelector_withObject("setComment:", credit)
|
146
|
+
end
|
147
|
+
# Note the colon in the selector string
|
148
|
+
tracks.makeObjectsPerformSelector_withObject("duplicateTo:", playlist)
|
149
|
+
# enable all tracks - does not work
|
150
|
+
end
|
151
|
+
|
152
|
+
def remove_track_from_playlist(track, playlist)
|
153
|
+
# looks dangerous!
|
154
|
+
return
|
155
|
+
track.delete
|
156
|
+
end
|
157
|
+
|
158
|
+
def create_playlist(name)
|
159
|
+
return if playlist_by_name(name)
|
160
|
+
props = {:name => name}
|
161
|
+
playlist = @app.classForScriptingClass("playlist").alloc.initWithProperties(props)
|
162
|
+
playlists.insertObject_atIndex(playlist, 0)
|
163
|
+
playlist
|
164
|
+
end
|
165
|
+
|
166
|
+
# makes sure the queue playlist is selected. importance for pause/play, skip,
|
167
|
+
# etc.
|
168
|
+
def select_queue_playlist
|
169
|
+
browserWindows.first.view = queue
|
170
|
+
end
|
171
|
+
|
172
|
+
# This is the playlist that itunes-rails uses
|
173
|
+
def queue
|
174
|
+
playlist_by_name(QUEUE_PLAYLIST) || create_playlist(QUEUE_PLAYLIST)
|
175
|
+
end
|
176
|
+
|
177
|
+
def queue_track(track)
|
178
|
+
add_track_to_playlist(track, queue)
|
179
|
+
end
|
180
|
+
|
181
|
+
def queue_tracks(tracks,credit=nil)
|
182
|
+
add_tracks_to_playlist(tracks, queue, credit)
|
183
|
+
end
|
184
|
+
|
185
|
+
def clear_queue
|
186
|
+
queue.tracks.removeAllObjects
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
class ItunesCommand
|
191
|
+
VERSION = '1.3'
|
192
|
+
attr_accessor :playlist_mode
|
193
|
+
def initialize
|
194
|
+
@i = ITunes.new
|
195
|
+
@playlists = []
|
196
|
+
@tracks = []
|
197
|
+
@playlist_mode = false
|
198
|
+
end
|
199
|
+
|
200
|
+
def parse(method, *args)
|
201
|
+
self.send method, args
|
202
|
+
end
|
203
|
+
|
204
|
+
def search(string=nil)
|
205
|
+
unless string
|
206
|
+
puts "Please enter a search string"
|
207
|
+
return
|
208
|
+
end
|
209
|
+
@tracks = @i.find_track(string)
|
210
|
+
print_names(@tracks)
|
211
|
+
end
|
212
|
+
|
213
|
+
def search_artist(string=nil)
|
214
|
+
unless string
|
215
|
+
puts "Please enter a search string"
|
216
|
+
return
|
217
|
+
end
|
218
|
+
@tracks = @i.find_track(string)
|
219
|
+
print_names(@tracks)
|
220
|
+
end
|
221
|
+
|
222
|
+
def print_names(items)
|
223
|
+
items = Array(items)
|
224
|
+
rows = []
|
225
|
+
items.each_with_index do |t, i|
|
226
|
+
begin
|
227
|
+
puts("%2d %s : %s" % [i, t.artist, t.name])
|
228
|
+
rescue
|
229
|
+
end
|
230
|
+
end
|
231
|
+
items
|
232
|
+
end
|
233
|
+
|
234
|
+
def play(index)
|
235
|
+
if @tracks.empty?
|
236
|
+
puts "No tracks in buffer yet. Try searching."
|
237
|
+
return
|
238
|
+
end
|
239
|
+
track = @tracks[index.to_i]
|
240
|
+
puts "Playing '#{track.name}' by #{track.artist} from #{track.album}"
|
241
|
+
track.playOnce(1)
|
242
|
+
end
|
243
|
+
|
244
|
+
def stop
|
245
|
+
@i.stop
|
246
|
+
end
|
247
|
+
|
248
|
+
def volume(level=nil)
|
249
|
+
unless level
|
250
|
+
puts "The volume is #{@i.soundVolume} out of 100"
|
251
|
+
return
|
252
|
+
end
|
253
|
+
@i.soundVolume = level
|
254
|
+
puts "The volume set to #{@i.soundVolume} out of 100"
|
255
|
+
end
|
256
|
+
|
257
|
+
def +(steps=10)
|
258
|
+
@i.soundVolume = @i.soundVolume + steps.to_i
|
259
|
+
volume
|
260
|
+
end
|
261
|
+
|
262
|
+
def -(steps=10)
|
263
|
+
@i.soundVolume = @i.soundVolume - steps.to_i
|
264
|
+
volume
|
265
|
+
end
|
266
|
+
|
267
|
+
def playlists
|
268
|
+
@playlist_mode = true
|
269
|
+
puts "Showing playlists"
|
270
|
+
playlists = @i.playlists
|
271
|
+
playlists.each_with_index do |p,i|
|
272
|
+
puts("%2d %s" % [i, p.name])
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def select_playlist(index)
|
277
|
+
@current_playlist = @i.playlists[index.to_i]
|
278
|
+
# show tracks
|
279
|
+
@tracks = @current_playlist.tracks
|
280
|
+
print_names(@tracks)
|
281
|
+
@playlist_mode = false
|
282
|
+
end
|
283
|
+
|
284
|
+
def playlist(index)
|
285
|
+
@playlists ||= playlists
|
286
|
+
puts @playlists[index].name
|
287
|
+
end
|
288
|
+
|
289
|
+
def queue(index)
|
290
|
+
@i.queue_track(track=@tracks[index.to_i])
|
291
|
+
puts "Added #{track.name} to the queue"
|
292
|
+
end
|
293
|
+
|
294
|
+
def show_queue
|
295
|
+
if @i.queue.tracks.empty?
|
296
|
+
puts "The queue is empty"
|
297
|
+
return
|
298
|
+
end
|
299
|
+
state = `osascript -e 'tell application "iTunes" to player state as string'`.strip
|
300
|
+
if state != 'stopped'
|
301
|
+
current_track_index = `osascript -e 'tell application "iTunes" to index of current track as string'`.to_i
|
302
|
+
else
|
303
|
+
current_track_index = nil
|
304
|
+
end
|
305
|
+
@i.queue.tracks.each_with_index do |t, i|
|
306
|
+
if current_track_index && current_track_index - 1 == i
|
307
|
+
puts "%s : %s <--- currently playing" % [t.artist, t.name]
|
308
|
+
else
|
309
|
+
puts "%s : %s" % [t.artist, t.name]
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def clear_queue
|
315
|
+
@i.clear_queue
|
316
|
+
puts "Cleared queue"
|
317
|
+
end
|
318
|
+
|
319
|
+
def start_queue
|
320
|
+
@i.stop
|
321
|
+
@i.queue.playOnce(1)
|
322
|
+
track = @i.currentTrack
|
323
|
+
puts "Playing '#{track.name}' by #{track.artist} from #{track.album}"
|
324
|
+
end
|
325
|
+
|
326
|
+
def skip
|
327
|
+
@i.nextTrack
|
328
|
+
track = @i.currentTrack
|
329
|
+
puts "Playing '#{track.name}' by #{track.artist} from #{track.album}"
|
330
|
+
end
|
331
|
+
|
332
|
+
def artists
|
333
|
+
rows = []
|
334
|
+
(@artists=@i.artists).keys.sort_by {|x| x.downcase}.each_with_index do |k, i|
|
335
|
+
rows << ("%s (%s tracks)" % [k, @artists[k]])
|
336
|
+
end
|
337
|
+
pager(rows)
|
338
|
+
end
|
339
|
+
|
340
|
+
def pager(rows)
|
341
|
+
out = rows.join("\n")
|
342
|
+
IO.popen("less", "w") do |less|
|
343
|
+
less.puts out
|
344
|
+
less.close_write
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
HELP = <<END
|
349
|
+
itunes-command
|
350
|
+
|
351
|
+
Commands:
|
352
|
+
|
353
|
+
q quit
|
354
|
+
h show commands
|
355
|
+
s <string> searches for tracks matching <string>
|
356
|
+
<track number> plays a track (assumes you've done a search and got results)
|
357
|
+
a lists all artists in the library
|
358
|
+
v show the current volume level
|
359
|
+
v <level> sets the volume level (1-100)
|
360
|
+
+ <increment> increases the volume by <increment>; default is 10 steps
|
361
|
+
- <increment> decreases the volume by <increment>; default is 10 steps
|
362
|
+
x stop
|
363
|
+
p shows all playlists
|
364
|
+
<playlist number> shows all the tracks in a playlist
|
365
|
+
l list all tracks in the queue (which will play tracks in succession)
|
366
|
+
n <track number> put a track in the queue
|
367
|
+
c clear the queue
|
368
|
+
g start playing tracks in the queue
|
369
|
+
k skip to next track in queue
|
370
|
+
END
|
371
|
+
|
372
|
+
COMMANDS = { 's' => :search,
|
373
|
+
'x' => :stop , 'play' => :play, 'v' => :volume, 'a' => :artists, '+' => :+, '-' => '-',
|
374
|
+
'p' => :playlists, 'select_playlist' => :select_playlist, 'l' => :show_queue, 'n' => :queue,
|
375
|
+
'c' => :clear_queue, 'g' => 'start_queue', 'k' => 'skip'}
|
376
|
+
|
377
|
+
def self.run(argv=ARGV)
|
378
|
+
i = ItunesCommand.new
|
379
|
+
puts HELP
|
380
|
+
loop do
|
381
|
+
command = Readline.readline(">> ").chomp
|
382
|
+
if command =~ /^q/
|
383
|
+
exit
|
384
|
+
elsif command =~ /^h/
|
385
|
+
puts HELP
|
386
|
+
next
|
387
|
+
end
|
388
|
+
args = command.split(' ')
|
389
|
+
if args.first =~ /\d+/
|
390
|
+
if i.playlist_mode
|
391
|
+
args.unshift 'select_playlist'
|
392
|
+
else
|
393
|
+
args.unshift 'play'
|
394
|
+
end
|
395
|
+
end
|
396
|
+
method = COMMANDS[args.shift]
|
397
|
+
if method.nil?
|
398
|
+
puts "Sorry, I don't recognize that command."
|
399
|
+
next
|
400
|
+
end
|
401
|
+
begin
|
402
|
+
args.empty? ? i.send(method) : i.send(method, args.join(' '))
|
403
|
+
rescue ArgumentError
|
404
|
+
puts "Invalid command."
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
end
|
409
|
+
|
410
|
+
end
|
411
|
+
|
412
|
+
|
413
|
+
if __FILE__ == $0
|
414
|
+
ItunesCommand.run ARGV
|
415
|
+
end
|
416
|
+
|
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: itunes-command
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.4"
|
5
|
+
platform: universal-darwin-9
|
6
|
+
authors:
|
7
|
+
- Daniel Choi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-08-17 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Search, queue, and play iTunes tracks from the command line.
|
17
|
+
email: dhchoi@gmail.com
|
18
|
+
executables:
|
19
|
+
- itunes-command
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- itunes-command.rb
|
26
|
+
has_rdoc: false
|
27
|
+
homepage: http://cesareborgia.com/software/itunes-command/
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: "0"
|
38
|
+
version:
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
requirements: []
|
46
|
+
|
47
|
+
rubyforge_project:
|
48
|
+
rubygems_version: 1.0.1
|
49
|
+
signing_key:
|
50
|
+
specification_version: 2
|
51
|
+
summary: Search and play iTunes tracks from the command line
|
52
|
+
test_files: []
|
53
|
+
|