itch 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/LICENSE.txt +340 -0
  2. data/README.txt +93 -0
  3. data/bin/itch +26 -0
  4. data/lib/itch.rb +369 -0
  5. data/test/test_itch.rb +312 -0
  6. metadata +54 -0
data/LICENSE.txt ADDED
@@ -0,0 +1,340 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 2, June 1991
3
+
4
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
5
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
6
+ Everyone is permitted to copy and distribute verbatim copies
7
+ of this license document, but changing it is not allowed.
8
+
9
+ Preamble
10
+
11
+ The licenses for most software are designed to take away your
12
+ freedom to share and change it. By contrast, the GNU General Public
13
+ License is intended to guarantee your freedom to share and change free
14
+ software--to make sure the software is free for all its users. This
15
+ General Public License applies to most of the Free Software
16
+ Foundation's software and to any other program whose authors commit to
17
+ using it. (Some other Free Software Foundation software is covered by
18
+ the GNU Library General Public License instead.) You can apply it to
19
+ your programs, too.
20
+
21
+ When we speak of free software, we are referring to freedom, not
22
+ price. Our General Public Licenses are designed to make sure that you
23
+ have the freedom to distribute copies of free software (and charge for
24
+ this service if you wish), that you receive source code or can get it
25
+ if you want it, that you can change the software or use pieces of it
26
+ in new free programs; and that you know you can do these things.
27
+
28
+ To protect your rights, we need to make restrictions that forbid
29
+ anyone to deny you these rights or to ask you to surrender the rights.
30
+ These restrictions translate to certain responsibilities for you if you
31
+ distribute copies of the software, or if you modify it.
32
+
33
+ For example, if you distribute copies of such a program, whether
34
+ gratis or for a fee, you must give the recipients all the rights that
35
+ you have. You must make sure that they, too, receive or can get the
36
+ source code. And you must show them these terms so they know their
37
+ rights.
38
+
39
+ We protect your rights with two steps: (1) copyright the software, and
40
+ (2) offer you this license which gives you legal permission to copy,
41
+ distribute and/or modify the software.
42
+
43
+ Also, for each author's protection and ours, we want to make certain
44
+ that everyone understands that there is no warranty for this free
45
+ software. If the software is modified by someone else and passed on, we
46
+ want its recipients to know that what they have is not the original, so
47
+ that any problems introduced by others will not reflect on the original
48
+ authors' reputations.
49
+
50
+ Finally, any free program is threatened constantly by software
51
+ patents. We wish to avoid the danger that redistributors of a free
52
+ program will individually obtain patent licenses, in effect making the
53
+ program proprietary. To prevent this, we have made it clear that any
54
+ patent must be licensed for everyone's free use or not licensed at all.
55
+
56
+ The precise terms and conditions for copying, distribution and
57
+ modification follow.
58
+
59
+ GNU GENERAL PUBLIC LICENSE
60
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61
+
62
+ 0. This License applies to any program or other work which contains
63
+ a notice placed by the copyright holder saying it may be distributed
64
+ under the terms of this General Public License. The "Program", below,
65
+ refers to any such program or work, and a "work based on the Program"
66
+ means either the Program or any derivative work under copyright law:
67
+ that is to say, a work containing the Program or a portion of it,
68
+ either verbatim or with modifications and/or translated into another
69
+ language. (Hereinafter, translation is included without limitation in
70
+ the term "modification".) Each licensee is addressed as "you".
71
+
72
+ Activities other than copying, distribution and modification are not
73
+ covered by this License; they are outside its scope. The act of
74
+ running the Program is not restricted, and the output from the Program
75
+ is covered only if its contents constitute a work based on the
76
+ Program (independent of having been made by running the Program).
77
+ Whether that is true depends on what the Program does.
78
+
79
+ 1. You may copy and distribute verbatim copies of the Program's
80
+ source code as you receive it, in any medium, provided that you
81
+ conspicuously and appropriately publish on each copy an appropriate
82
+ copyright notice and disclaimer of warranty; keep intact all the
83
+ notices that refer to this License and to the absence of any warranty;
84
+ and give any other recipients of the Program a copy of this License
85
+ along with the Program.
86
+
87
+ You may charge a fee for the physical act of transferring a copy, and
88
+ you may at your option offer warranty protection in exchange for a fee.
89
+
90
+ 2. You may modify your copy or copies of the Program or any portion
91
+ of it, thus forming a work based on the Program, and copy and
92
+ distribute such modifications or work under the terms of Section 1
93
+ above, provided that you also meet all of these conditions:
94
+
95
+ a) You must cause the modified files to carry prominent notices
96
+ stating that you changed the files and the date of any change.
97
+
98
+ b) You must cause any work that you distribute or publish, that in
99
+ whole or in part contains or is derived from the Program or any
100
+ part thereof, to be licensed as a whole at no charge to all third
101
+ parties under the terms of this License.
102
+
103
+ c) If the modified program normally reads commands interactively
104
+ when run, you must cause it, when started running for such
105
+ interactive use in the most ordinary way, to print or display an
106
+ announcement including an appropriate copyright notice and a
107
+ notice that there is no warranty (or else, saying that you provide
108
+ a warranty) and that users may redistribute the program under
109
+ these conditions, and telling the user how to view a copy of this
110
+ License. (Exception: if the Program itself is interactive but
111
+ does not normally print such an announcement, your work based on
112
+ the Program is not required to print an announcement.)
113
+
114
+ These requirements apply to the modified work as a whole. If
115
+ identifiable sections of that work are not derived from the Program,
116
+ and can be reasonably considered independent and separate works in
117
+ themselves, then this License, and its terms, do not apply to those
118
+ sections when you distribute them as separate works. But when you
119
+ distribute the same sections as part of a whole which is a work based
120
+ on the Program, the distribution of the whole must be on the terms of
121
+ this License, whose permissions for other licensees extend to the
122
+ entire whole, and thus to each and every part regardless of who wrote it.
123
+
124
+ Thus, it is not the intent of this section to claim rights or contest
125
+ your rights to work written entirely by you; rather, the intent is to
126
+ exercise the right to control the distribution of derivative or
127
+ collective works based on the Program.
128
+
129
+ In addition, mere aggregation of another work not based on the Program
130
+ with the Program (or with a work based on the Program) on a volume of
131
+ a storage or distribution medium does not bring the other work under
132
+ the scope of this License.
133
+
134
+ 3. You may copy and distribute the Program (or a work based on it,
135
+ under Section 2) in object code or executable form under the terms of
136
+ Sections 1 and 2 above provided that you also do one of the following:
137
+
138
+ a) Accompany it with the complete corresponding machine-readable
139
+ source code, which must be distributed under the terms of Sections
140
+ 1 and 2 above on a medium customarily used for software interchange; or,
141
+
142
+ b) Accompany it with a written offer, valid for at least three
143
+ years, to give any third party, for a charge no more than your
144
+ cost of physically performing source distribution, a complete
145
+ machine-readable copy of the corresponding source code, to be
146
+ distributed under the terms of Sections 1 and 2 above on a medium
147
+ customarily used for software interchange; or,
148
+
149
+ c) Accompany it with the information you received as to the offer
150
+ to distribute corresponding source code. (This alternative is
151
+ allowed only for noncommercial distribution and only if you
152
+ received the program in object code or executable form with such
153
+ an offer, in accord with Subsection b above.)
154
+
155
+ The source code for a work means the preferred form of the work for
156
+ making modifications to it. For an executable work, complete source
157
+ code means all the source code for all modules it contains, plus any
158
+ associated interface definition files, plus the scripts used to
159
+ control compilation and installation of the executable. However, as a
160
+ special exception, the source code distributed need not include
161
+ anything that is normally distributed (in either source or binary
162
+ form) with the major components (compiler, kernel, and so on) of the
163
+ operating system on which the executable runs, unless that component
164
+ itself accompanies the executable.
165
+
166
+ If distribution of executable or object code is made by offering
167
+ access to copy from a designated place, then offering equivalent
168
+ access to copy the source code from the same place counts as
169
+ distribution of the source code, even though third parties are not
170
+ compelled to copy the source along with the object code.
171
+
172
+ 4. You may not copy, modify, sublicense, or distribute the Program
173
+ except as expressly provided under this License. Any attempt
174
+ otherwise to copy, modify, sublicense or distribute the Program is
175
+ void, and will automatically terminate your rights under this License.
176
+ However, parties who have received copies, or rights, from you under
177
+ this License will not have their licenses terminated so long as such
178
+ parties remain in full compliance.
179
+
180
+ 5. You are not required to accept this License, since you have not
181
+ signed it. However, nothing else grants you permission to modify or
182
+ distribute the Program or its derivative works. These actions are
183
+ prohibited by law if you do not accept this License. Therefore, by
184
+ modifying or distributing the Program (or any work based on the
185
+ Program), you indicate your acceptance of this License to do so, and
186
+ all its terms and conditions for copying, distributing or modifying
187
+ the Program or works based on it.
188
+
189
+ 6. Each time you redistribute the Program (or any work based on the
190
+ Program), the recipient automatically receives a license from the
191
+ original licensor to copy, distribute or modify the Program subject to
192
+ these terms and conditions. You may not impose any further
193
+ restrictions on the recipients' exercise of the rights granted herein.
194
+ You are not responsible for enforcing compliance by third parties to
195
+ this License.
196
+
197
+ 7. If, as a consequence of a court judgment or allegation of patent
198
+ infringement or for any other reason (not limited to patent issues),
199
+ conditions are imposed on you (whether by court order, agreement or
200
+ otherwise) that contradict the conditions of this License, they do not
201
+ excuse you from the conditions of this License. If you cannot
202
+ distribute so as to satisfy simultaneously your obligations under this
203
+ License and any other pertinent obligations, then as a consequence you
204
+ may not distribute the Program at all. For example, if a patent
205
+ license would not permit royalty-free redistribution of the Program by
206
+ all those who receive copies directly or indirectly through you, then
207
+ the only way you could satisfy both it and this License would be to
208
+ refrain entirely from distribution of the Program.
209
+
210
+ If any portion of this section is held invalid or unenforceable under
211
+ any particular circumstance, the balance of the section is intended to
212
+ apply and the section as a whole is intended to apply in other
213
+ circumstances.
214
+
215
+ It is not the purpose of this section to induce you to infringe any
216
+ patents or other property right claims or to contest validity of any
217
+ such claims; this section has the sole purpose of protecting the
218
+ integrity of the free software distribution system, which is
219
+ implemented by public license practices. Many people have made
220
+ generous contributions to the wide range of software distributed
221
+ through that system in reliance on consistent application of that
222
+ system; it is up to the author/donor to decide if he or she is willing
223
+ to distribute software through any other system and a licensee cannot
224
+ impose that choice.
225
+
226
+ This section is intended to make thoroughly clear what is believed to
227
+ be a consequence of the rest of this License.
228
+
229
+ 8. If the distribution and/or use of the Program is restricted in
230
+ certain countries either by patents or by copyrighted interfaces, the
231
+ original copyright holder who places the Program under this License
232
+ may add an explicit geographical distribution limitation excluding
233
+ those countries, so that distribution is permitted only in or among
234
+ countries not thus excluded. In such case, this License incorporates
235
+ the limitation as if written in the body of this License.
236
+
237
+ 9. The Free Software Foundation may publish revised and/or new versions
238
+ of the General Public License from time to time. Such new versions will
239
+ be similar in spirit to the present version, but may differ in detail to
240
+ address new problems or concerns.
241
+
242
+ Each version is given a distinguishing version number. If the Program
243
+ specifies a version number of this License which applies to it and "any
244
+ later version", you have the option of following the terms and conditions
245
+ either of that version or of any later version published by the Free
246
+ Software Foundation. If the Program does not specify a version number of
247
+ this License, you may choose any version ever published by the Free Software
248
+ Foundation.
249
+
250
+ 10. If you wish to incorporate parts of the Program into other free
251
+ programs whose distribution conditions are different, write to the author
252
+ to ask for permission. For software which is copyrighted by the Free
253
+ Software Foundation, write to the Free Software Foundation; we sometimes
254
+ make exceptions for this. Our decision will be guided by the two goals
255
+ of preserving the free status of all derivatives of our free software and
256
+ of promoting the sharing and reuse of software generally.
257
+
258
+ NO WARRANTY
259
+
260
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261
+ FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262
+ OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263
+ PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264
+ OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266
+ TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267
+ PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268
+ REPAIR OR CORRECTION.
269
+
270
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272
+ REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273
+ INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274
+ OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275
+ TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276
+ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277
+ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278
+ POSSIBILITY OF SUCH DAMAGES.
279
+
280
+ END OF TERMS AND CONDITIONS
281
+
282
+ How to Apply These Terms to Your New Programs
283
+
284
+ If you develop a new program, and you want it to be of the greatest
285
+ possible use to the public, the best way to achieve this is to make it
286
+ free software which everyone can redistribute and change under these terms.
287
+
288
+ To do so, attach the following notices to the program. It is safest
289
+ to attach them to the start of each source file to most effectively
290
+ convey the exclusion of warranty; and each file should have at least
291
+ the "copyright" line and a pointer to where the full notice is found.
292
+
293
+ <one line to give the program's name and a brief idea of what it does.>
294
+ Copyright (C) <year> <name of author>
295
+
296
+ This program is free software; you can redistribute it and/or modify
297
+ it under the terms of the GNU General Public License as published by
298
+ the Free Software Foundation; either version 2 of the License, or
299
+ (at your option) any later version.
300
+
301
+ This program is distributed in the hope that it will be useful,
302
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
303
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304
+ GNU General Public License for more details.
305
+
306
+ You should have received a copy of the GNU General Public License
307
+ along with this program; if not, write to the Free Software
308
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
309
+
310
+
311
+ Also add information on how to contact you by electronic and paper mail.
312
+
313
+ If the program is interactive, make it output a short notice like this
314
+ when it starts in an interactive mode:
315
+
316
+ Gnomovision version 69, Copyright (C) year name of author
317
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
318
+ This is free software, and you are welcome to redistribute it
319
+ under certain conditions; type `show c' for details.
320
+
321
+ The hypothetical commands `show w' and `show c' should show the appropriate
322
+ parts of the General Public License. Of course, the commands you use may
323
+ be called something other than `show w' and `show c'; they could even be
324
+ mouse-clicks or menu items--whatever suits your program.
325
+
326
+ You should also get your employer (if you work as a programmer) or your
327
+ school, if any, to sign a "copyright disclaimer" for the program, if
328
+ necessary. Here is a sample; alter the names:
329
+
330
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
331
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
332
+
333
+ <signature of Ty Coon>, 1 April 1989
334
+ Ty Coon, President of Vice
335
+
336
+ This General Public License does not permit incorporating your program into
337
+ proprietary programs. If your program is a subroutine library, you may
338
+ consider it more useful to permit linking proprietary applications with the
339
+ library. If this is what you want to do, use the GNU Library General
340
+ Public License instead of this License.
data/README.txt ADDED
@@ -0,0 +1,93 @@
1
+ == Synopsis
2
+
3
+ itch - Control Helper for ITunes - Perform operations on iTunes tracks.
4
+
5
+
6
+ == Usage
7
+
8
+ itch [--help] [--library] [--current-playlist] [--playlist name] [--all-playlists] [--create-playlist [name]] [--delete-playlist [name]] [--current-track] [--selected-tracks] [--all-tracks] [--find string] [--visible-find string] [--find-artist string] [--find-album string] [--find-composer string] [--find-track-name string] [--print-info format] [--play-found] [--set-artist string] [--set-album string] [--set-bpm number] [--set-comment string] [--set-composer string] [--set-disc-count number] [--set-disc-number number] [--set-enabled] [--set-disabled] [--set-eq string] [--set-genre string] [--set-grouping string] [--set-name string] [--set-play-count number] [--set-rating number] [--set-skip-count number] [--set-track-count number] [--set-track-number number] [--set-volume-adjustment string] [--set-year number] [--volume number] [--volume-down [number]] [--volume-up [number]] [--mute] [--unmute] [--scan-to number] [--scan-backwards [number]] [--scan-forwards [number]] [--next-track] [--pause] [--play] [--play-file name] [--play-pause] [--previous-track] [--stop] [--add-file name] [--open-url url] [--goto-store-home-page] [--update-ipod] [--quit]
9
+
10
+
11
+ == Description
12
+
13
+ Allows control of iTunes for Windows via a command line.
14
+ It provides all basic media controls such as play, pause, stop, next/previous track, and volume adjustment.
15
+ In addition, you can search for tracks in any given playlist and perform operations on them, or retrieve information regarding them.
16
+ You can also add tracks to the library, sync your iPod, and perform a variety of other operations.
17
+
18
+
19
+ == Examples
20
+
21
+ Get help:
22
+
23
+ itch --help
24
+
25
+ Start up iTunes [if necessary] and begin playing:
26
+
27
+ itch --play
28
+
29
+ Find and play a song:
30
+
31
+ itch --find "mookid" --play-found
32
+
33
+ Find all songs from an artist, show basic info on them, and play the first found:
34
+
35
+ itch --find "aphex twin" --print-info "%a - %A - %t - %n" --play-found
36
+
37
+
38
+ == Building from Source
39
+
40
+ The setup should be fairly standard for anyone familiar with Rake. There are a few external dependencies; see below for links.
41
+
42
+ Get the source from the project site.
43
+
44
+ Change into the project directory. Run "rake -T" for a list of targets. You'll need RubyScript2Exe to build a Windows executable, or NSIS to build a Windows installer. Building a gem should work out of the box. "rake" builds an installer and a gem by default.
45
+
46
+
47
+ == See Also
48
+
49
+ Project Site: http://code.google.com/p/itunes-control/
50
+
51
+ Nullsoft Scriptable Install System: http://nsis.sourceforge.net/
52
+
53
+ RubyScript2Exe: http://www.erikveen.dds.nl/rubyscript2exe/index.html
54
+
55
+ iTunes: http://www.apple.com/itunes/
56
+
57
+
58
+ == Thanks
59
+
60
+ Thanks to Eden Li and Gary Wright for various Ruby advice and assistance.
61
+
62
+
63
+ == Author
64
+
65
+ Copyright Jay McGavren, chicgeekaz@hotmail.com
66
+
67
+
68
+ == License
69
+
70
+ This program is free software; you can redistribute it and/or modify
71
+ it under the terms of the GNU General Public License as published by
72
+ the Free Software Foundation; either version 2 of the License, or
73
+ (at your option) any later version.
74
+
75
+ This program is distributed in the hope that it will be useful,
76
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
77
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
78
+ GNU General Public License for more details.
79
+
80
+ You should have received a copy of the GNU General Public License
81
+ along with this program; if not, write to the Free Software
82
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
83
+
84
+ Please see the LICENSE.txt file for the full license.
85
+
86
+
87
+ == Legal
88
+
89
+ iTunes is a trademark of Apple Inc.
90
+
91
+ Windows is a registered trademark of Microsoft Corporation in the United States and other countries.
92
+
93
+ itch is not endorsed, licensed, or sponsored by Apple Inc or Microsoft Corporation.
data/bin/itch ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/ruby
2
+
3
+ begin
4
+ require 'itch'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'itch'
8
+ end
9
+
10
+ #Create an iTunes controller.
11
+ helper = Itch.new(Itch.create_itunes_interface)
12
+
13
+ #Get program options.
14
+ config = helper.parse_options(ARGV)
15
+ #Set default options.
16
+ config = helper.set_default_options(config)
17
+ #Get playlists that tracks will be selected from.
18
+ playlists = helper.get_playlists(config)
19
+ #Delete requested playlists.
20
+ helper.delete_playlists(config)
21
+ #Get tracks for processing.
22
+ tracks = helper.get_tracks(config, playlists)
23
+ #Process general commands.
24
+ helper.perform_general_operations(config)
25
+ #Process commands for specified tracks.
26
+ helper.perform_track_operations(config, tracks)
data/lib/itch.rb ADDED
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/ruby
2
+
3
+
4
+ require 'optparse'
5
+ require 'win32ole'
6
+
7
+
8
+ #A helper that controls the iTunes application.
9
+ class Itch
10
+
11
+
12
+ #The COM/OLE interface to iTunes.
13
+ attr_reader :interface
14
+
15
+
16
+ #Create a new helper, and assign it an iTunes interface to use.
17
+ def initialize(itunes_interface)
18
+ @interface = itunes_interface
19
+ end
20
+
21
+
22
+ #Create an OLE/COM link to iTunes.
23
+ def Itch.create_itunes_interface
24
+ WIN32OLE.new('iTunes.Application') or raise "Couldn't take control of iTunes."
25
+ end
26
+
27
+
28
+ #Parse arguments and get a configuration.
29
+ def parse_options(arguments)
30
+
31
+ #Config will hold parsed option values.
32
+ config = Hash.new
33
+
34
+ #Define valid options with descriptions.
35
+ options = OptionParser.new
36
+ options.on("--help", TrueClass, "Display program help.") {
37
+ puts options.to_s
38
+ exit
39
+ }
40
+ options.on("--library", TrueClass, "Operation will include the entire iTunes library. Used by default unless other libraries are selected.") {|value| config['library'] = value}
41
+ options.on("--current-playlist", TrueClass, "Operation will include the current playlist.") {|value| config['current-playlist'] = value}
42
+ options.on("--playlist name", Object, "Operation will include the named playlist. (This option can occur more than once.)") {|value| (config['playlist'] ||= []) << value}
43
+ options.on("--all-playlists", TrueClass, "Operation will include all playlists.") {|value| config['all-playlists'] = value}
44
+ 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}
45
+ 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}
46
+ options.on("--current-track", TrueClass, "Operation will inclue the current track.") {|value| config['current-track'] = value}
47
+ options.on("--selected-tracks", TrueClass, "Operation will include the selected tracks.") {|value| config['selected-tracks'] = value}
48
+ options.on("--all-tracks", TrueClass, "Operation will include all tracks in the specified playlist(s).") {|value| config['all-tracks'] = value}
49
+ options.on("--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}
50
+ 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}
51
+ 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}
52
+ 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}
53
+ 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}
54
+ 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}
55
+ options.on("--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:",
56
+ %q#"%a": artist#,
57
+ %q#"%e": encoding#,
58
+ %q#"%A": album#,
59
+ %q#"%b": BPM (beats per minute)#,
60
+ %q#"%c": composer#,
61
+ %q#"%C": comment#,
62
+ %q#"%d": disc number#,
63
+ %q#"%D": disc count#,
64
+ %q#"%E": Enabled status ("enabled" or "disabled")#,
65
+ %q#"%l": location (file name/URL)#,
66
+ %q#"%p": play count#,
67
+ %q#"%q": equalizer#,
68
+ %q#"%g": genre#,
69
+ %q#"%G": grouping#,
70
+ %q#"%n": name (title)#,
71
+ %q#"%r": rating#,
72
+ %q#"%s": skip count#,
73
+ %q#"%t": track number#,
74
+ %q#"%T": track count#,
75
+ %q#"%v": volume adjustment#,
76
+ %q#"%y": year#,
77
+ %q#"%%": percent sign#
78
+ ) {|value| config['print-info'] = value}
79
+ options.on("--play-found", TrueClass, "Play the first of the selected tracks.") {|value| config['play-found'] = value}
80
+ options.on("--set-artist name", Object, "Set the artist for each track.") {|value| config['set-artist'] = value}
81
+ options.on("--set-album name", Object, "Set the album for each track.") {|value| config['set-album'] = value}
82
+ options.on("--set-bpm number", Integer, "Set the beats per minute for each track.") {|value| config['set-bpm'] = value}
83
+ options.on("--set-comment string", Object, "Set the comment for each track.") {|value| config['set-comment'] = value}
84
+ options.on("--set-composer string", Object, "Set the composer for each track.") {|value| config['set-composer'] = value}
85
+ 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}
86
+ 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}
87
+ options.on("--set-enabled", TrueClass, "Enable the check box for each track.") {|value| config['set-enabled'] = value}
88
+ options.on("--set-disabled", TrueClass, "Disable the check box for each track.") {|value| config['set-disabled'] = value}
89
+ options.on("--set-eq name", Object, "Set the equalizer to the named preset. Use 'None' to disable.") {|value| config['set-eq'] = value}
90
+ options.on("--set-genre name", Object, "Set the genre for each track.") {|value| config['set-genre'] = value}
91
+ options.on("--set-grouping string", Object, "Set the grouping for each track.") {|value| config['set-grouping'] = value}
92
+ options.on("--set-name name", Object, "Set the name (title) for each track.") {|value| config['set-name'] = value}
93
+ options.on("--set-play-count number", Integer, "Set the play count for each track.") {|value| config['set-play-count'] = value}
94
+ options.on("--set-rating number", Integer, "Set the rating for each track. Valid values are 0 through 5.") {|value| config['set-rating'] = value}
95
+ options.on("--set-skip-count number", Integer, "Set the skip count for each track.") {|value| config['set-skip-count'] = value}
96
+ options.on("--set-track-number number", Integer, "For each track, set its album track number.") {|value| config['set-track-number'] = value}
97
+ options.on("--set-track-count number", Integer, "For each track, set the number of tracks on its album.") {|value| config['set-track-count'] = value}
98
+ options.on("--set-volume-adjustment 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-volume-adjustment'] = value}
99
+ options.on("--set-year number", Integer, "Set the year of publication for each track.") {|value| config['set-year'] = value}
100
+ options.on("--volume number", Integer, "Set the volume to X percentage points.") {|value| config['volume'] = value}
101
+ options.on("--volume-down [number]", Integer, "Decrease the volume by X percentage points (default 10).") {|value| config['volume-down'] = value || 10}
102
+ options.on("--volume-up [number]", Integer, "Increase the volume by X percentage points (default 10).") {|value| config['volume-up'] = value || 10}
103
+ options.on("--mute", TrueClass, "Mute the audio.") {|value| config['mute'] = value}
104
+ options.on("--unmute", TrueClass, "Unmute the audio.") {|value| config['unmute'] = value}
105
+ options.on("--scan-to seconds", Integer, "Scan to an offset X seconds within the current track.") {|value| config['scan-to'] = value}
106
+ options.on("--scan-backwards [seconds]", Integer, "Scan backwards X seconds within the current track (default 10).") {|value| config['scan-backwards'] = value || 10}
107
+ options.on("--scan-forwards [seconds]", Integer, "Scan forwards X seconds within the current track (default 10).") {|value| config['scan-forwards'] = value || 10}
108
+ options.on("--next-track", TrueClass, "Go to the next track.") {|value| config['next-track'] = value}
109
+ options.on("--pause", TrueClass, "Pause playback.") {|value| config['pause'] = value}
110
+ options.on("--play", TrueClass, "Play the current track.") {|value| config['play'] = value}
111
+ options.on("--play-file name", Object, "Play the specified file or folder.") {|value| config['play-file'] = value}
112
+ options.on("--play-pause", TrueClass, "If currently paused, begin playing. If currently playing, pause playback.") {|value| config['play-pause'] = value}
113
+ options.on("--previous-track", TrueClass, "Go to the previous track.") {|value| config['previous-track'] = value}
114
+ options.on("--stop", TrueClass, "Stop playback.") {|value| config['stop'] = value}
115
+ options.on("--add-file name", Object, "Add the specified file or folder to the library.") {|value| (config['add-file'] ||= []) << value}
116
+ options.on("--open-url url", Object, "Open the given URL.") {|value| config['open-url'] = value}
117
+ options.on("--goto-store-home-page", TrueClass, "Go to the Store.") {|value| config['goto-store-home-page'] = value}
118
+ options.on("--update-ipod", TrueClass, "Update the iPod.") {|value| config['update-ipod'] = value}
119
+ options.on("--quit", TrueClass, "Exit iTunes.") {|value| config['quit'] = value}
120
+
121
+ #Parse the options, printing usage if parsing fails.
122
+ options.parse(arguments) rescue puts "#{$!}\nType '#{$0} --help' for valid options."
123
+
124
+ config
125
+
126
+ end
127
+
128
+
129
+ #Set defaults for an existing set of options.
130
+ def set_default_options(config=Hash.new)
131
+
132
+ config['library'] = true unless (
133
+ config.has_key?('library') or
134
+ config.has_key?('current-playlist') or
135
+ config.has_key?('playlist') or
136
+ config.has_key?('all-playlists') or
137
+ config.has_key?('create-playlist')
138
+ )
139
+
140
+ config
141
+
142
+ end
143
+
144
+ #Get playlists that will be operated on.
145
+ def get_playlists (config)
146
+
147
+ playlists = Array.new
148
+ playlists.push(@interface.LibraryPlaylist) if config['library']
149
+ if config['current-playlist']
150
+ playlists.push(@interface.CurrentPlaylist) or raise "Can't find current playlist."
151
+ end
152
+ if config['all-playlists']
153
+ if library_playlists = @interface.LibrarySource.Playlists
154
+ library_playlists.each {|playlist| playlists.push(playlist)}
155
+ else
156
+ raise "No playlists found."
157
+ end
158
+ end
159
+ with_option_values('playlist', config) do |name|
160
+ playlists.push(find_playlist(name)) or raise "Can't find playlist #{name}"
161
+ end
162
+
163
+ #If creation of playlist(s) requested, create them and add to the collection.
164
+ with_option_values('create-playlist', config) do |name|
165
+ playlists.push(@interface.CreatePlaylist(name)) or raise "Can't create playlist #{name}"
166
+ end
167
+
168
+ playlists
169
+
170
+ end
171
+
172
+
173
+ #Delete specified playlists.
174
+ def delete_playlists (config)
175
+ with_option_values('delete-playlist', config) do |name|
176
+ find_playlist(name).delete
177
+ end
178
+ end
179
+
180
+
181
+ #Find requested tracks.
182
+ def get_tracks (config, playlists)
183
+
184
+ tracks = Array.new
185
+
186
+ #Add current track to list if desired.
187
+ if config['current-track']
188
+ tracks.push(@interface.CurrentTrack) or raise "Can't find current track."
189
+ end
190
+
191
+ #Add highlighted tracks to list if desired.
192
+ if config['selected-tracks']
193
+ if selected_tracks = @interface.SelectedTracks
194
+ selected_tracks.each {|track| tracks.push(track)}
195
+ else
196
+ raise "No tracks selected."
197
+ end
198
+ end
199
+
200
+ #Find tracks in selected playlists.
201
+ for playlist in playlists
202
+
203
+ #If all tracks are to be processed, select them all.
204
+ if config['all-tracks']
205
+ playlist.Tracks.each {|track| tracks.push(track)}
206
+ #Otherwise, see if user wishes to search for tracks.
207
+ else
208
+ search_playlist = lambda {|option, field_id|
209
+ with_option_values(option, config) do |terms|
210
+ if (results = playlist.Search(terms, field_id))
211
+ results.each {|track| tracks.push(track)}
212
+ end
213
+ end
214
+ }
215
+ search_playlist.call('find', 0)
216
+ search_playlist.call('visible-find', 1)
217
+ search_playlist.call('find-artist', 2)
218
+ search_playlist.call('find-album', 3)
219
+ search_playlist.call('find-composer', 4)
220
+ search_playlist.call('find-track-name', 5)
221
+ end
222
+
223
+ #Import files to library and add resulting tracks to list.
224
+ with_option_values('add-file', config) do |path|
225
+ tracks.concat(add_file(path, playlist))
226
+ end
227
+
228
+ end
229
+
230
+ tracks
231
+
232
+ end
233
+
234
+
235
+ #Perform the operations specified in the config that affect iTunes itself.
236
+ def perform_general_operations (config)
237
+
238
+ specified?(config, 'mute') {@interface.Mute = 1}
239
+ specified?(config, 'unmute') {@interface.Mute = 0}
240
+ specified?(config, 'next-track') {@interface.NextTrack}
241
+ specified?(config, 'scan-backwards') {|v| @interface.PlayerPosition -= v}
242
+ specified?(config, 'scan-forwards') {|v| @interface.PlayerPosition += v}
243
+ specified?(config, 'scan-to') {|v| @interface.PlayerPosition = v}
244
+ specified?(config, 'volume-down') {|v| @interface.SoundVolume -= v}
245
+ specified?(config, 'volume-up') {|v| @interface.SoundVolume += v}
246
+ specified?(config, 'volume') {|v| @interface.SoundVolume = v}
247
+ specified?(config, 'play-file') {|v| @interface.PlayFile(v)}
248
+ specified?(config, 'open-url') {|v| @interface.OpenURL(v)}
249
+ specified?(config, 'goto-store-home-page') {@interface.GotoMusicStoreHomePage}
250
+ specified?(config, 'update-ipod') {@interface.UpdateIPod}
251
+ specified?(config, 'pause') {@interface.Pause}
252
+ specified?(config, 'play-pause') {@interface.PlayPause}
253
+ specified?(config, 'previous-track') {@interface.PreviousTrack}
254
+ specified?(config, 'stop') {@interface.Stop}
255
+ specified?(config, 'play') {@interface.Play}
256
+ specified?(config, 'quit') {@interface.Quit}
257
+
258
+ end
259
+
260
+
261
+ #Perform the operations specified in the config that affect the selected tracks.
262
+ def perform_track_operations (config, tracks)
263
+
264
+ #Operate on selected tracks.
265
+ for track in tracks
266
+ specified?(config, 'set-artist') {|v| track.Artist = v}
267
+ specified?(config, 'set-album') {|v| track.Album = v}
268
+ specified?(config, 'set-bpm') {|v| track.BPM = v}
269
+ specified?(config, 'set-comment') {|v| track.Comment = v}
270
+ specified?(config, 'set-composer') {|v| track.Composer = v}
271
+ specified?(config, 'set-disc-count') {|v| track.DiscCount = v}
272
+ specified?(config, 'set-disc-number') {|v| track.DiscNumber = v}
273
+ specified?(config, 'set-enabled') {|v| track.Enabled = true}
274
+ specified?(config, 'set-disabled') {|v| track.Enabled = false}
275
+ specified?(config, 'set-eq') {|v| track.EQ = v}
276
+ specified?(config, 'set-genre') {|v| track.Genre = v}
277
+ specified?(config, 'set-grouping') {|v| track.Grouping = v}
278
+ specified?(config, 'set-name') {|v| track.Name = v}
279
+ specified?(config, 'set-play-count') {|v| track.PlayedCount = v}
280
+ specified?(config, 'set-rating') {|v| track.Rating = v * 20} #Rating is stored as a number from 1-100 internally, but 1-5 in interface.
281
+ specified?(config, 'set-skip-count') {|v| track.SkippedCount = v}
282
+ specified?(config, 'set-track-count') {|v| track.TrackCount = v}
283
+ specified?(config, 'set-track-number') {|v| track.TrackNumber = v}
284
+ specified?(config, 'set-volume-adjustment') {|v| track.VolumeAdjustment = v}
285
+ specified?(config, 'set-year') {|v| track.Year = v}
286
+ specified?(config, 'print-info') {|v| puts track_info(track, v)}
287
+ specified?(config, 'set-artist') {|v| track.Artist = v}
288
+ #TODO: Re-implement add-to-playlist.
289
+ end
290
+
291
+ #Play first of selected tracks if requested.
292
+ specified?(config, 'play-found') {tracks[0].Play unless tracks.empty?}
293
+
294
+ end
295
+
296
+
297
+ #Take the specified format string with %x flags, and substitute info from the given track.
298
+ def track_info (track, format)
299
+
300
+ #Double percent signs should not be substituted, so work around them.
301
+ segments = format.split(/%%/)
302
+
303
+ #Substitute track info for markers.
304
+ output_segments = segments.map do |segment|
305
+ segment.gsub!(/%a/) {track.Artist}
306
+ segment.gsub!(/%e/) {track.KindAsString} #The encoding.
307
+ segment.gsub!(/%A/) {track.Album}
308
+ segment.gsub!(/%b/) {track.BPM.to_s}
309
+ segment.gsub!(/%c/) {track.Composer}
310
+ segment.gsub!(/%C/) {track.Comment}
311
+ segment.gsub!(/%d/) {track.DiscNumber.to_s}
312
+ segment.gsub!(/%D/) {track.DiscCount.to_s}
313
+ segment.gsub!(/%E/) {track.Enabled == 1 ? 'enabled' : 'disabled'}
314
+ segment.gsub!(/%l/) {track.Location}
315
+ segment.gsub!(/%p/) {track.PlayedCount.to_s}
316
+ segment.gsub!(/%q/) {track.EQ}
317
+ segment.gsub!(/%g/) {track.Genre}
318
+ segment.gsub!(/%G/) {track.Grouping}
319
+ segment.gsub!(/%n/) {track.Name}
320
+ segment.gsub!(/%r/) {(track.Rating.to_i / 20).to_s}
321
+ segment.gsub!(/%s/) {track.SkippedCount.to_s}
322
+ segment.gsub!(/%t/) {track.TrackNumber.to_s}
323
+ segment.gsub!(/%T/) {track.TrackCount.to_s}
324
+ segment.gsub!(/%v/) {track.VolumeAdjustment.to_s}
325
+ segment.gsub!(/%y/) {track.Year.to_s}
326
+ segment
327
+ end
328
+
329
+ #Replace double percent signs with single percent signs and return.
330
+ output = output_segments.join('%')
331
+ output += '%' if format =~ /%%$/ #split() doesn't create final empty field if string ends in a delimiter.
332
+ output
333
+
334
+ end
335
+
336
+
337
+ private
338
+
339
+ #Get selected playlist from iTunes.
340
+ def find_playlist(name)
341
+ @interface.LibrarySource.Playlists.ItemByName(name) or raise "Can't find playlist '#{name}'"
342
+ end
343
+
344
+ #Invoke a block with each of the values for the given option in the given configuration.
345
+ def with_option_values (key, config)
346
+ if config.has_key?(key)
347
+ config[key].each {|value| yield value}
348
+ end
349
+ end
350
+
351
+ #Add files to each specified playlist.
352
+ def add_file(path, playlist)
353
+ #Add the file and retrieve the status (since it's an asynchronous operation).
354
+ status = playlist.AddFile(path) or raise "Can't find '#{path}'."
355
+ #Wait for operation to complete.
356
+ sleep 1 while status.InProgress
357
+ #Return added track(s).
358
+ tracks = Array.new
359
+ status.Tracks.each {|track| tracks.push(track)}
360
+ tracks
361
+ end
362
+
363
+ #Perform an action only if the given option was specified.
364
+ def specified? (config, key)
365
+ yield config[key] if config.has_key?(key)
366
+ end
367
+
368
+
369
+ end
data/test/test_itch.rb ADDED
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'itch'
4
+ require 'test/unit'
5
+
6
+ class TestItch < Test::Unit::TestCase
7
+
8
+ #Mock object to test that proper calls are made...
9
+ class MockOLE
10
+ def initialize (name='')
11
+ @name = name
12
+ end
13
+ attr_accessor :calls, :name
14
+ #Log any called methods and return a new stub that is itself an OLE object.
15
+ def method_missing (method_id, *arguments)
16
+ (@calls ||= Array.new) << [method_id.id2name, arguments]
17
+ (@methods ||= Hash.new)[method_id.id2name] ||= MockOLE.new(method_id.id2name)
18
+ end
19
+ end
20
+
21
+ #A mock object that returns the name of the accessor as the accessor value.
22
+ class MockObject
23
+ def method_missing (method_id, *arguments)
24
+ method_id.id2name
25
+ end
26
+ end
27
+
28
+
29
+ def setup
30
+ #Create a Helper to run tests on.
31
+ @helper = Itch.new(Itch.create_itunes_interface)
32
+ end
33
+
34
+
35
+ def test_parse_options
36
+
37
+ #Test parsing.
38
+ config = @helper.parse_options [
39
+ '--all-playlists',
40
+ '--update-ipod'
41
+ ]
42
+ assert_not_nil(config['all-playlists'])
43
+ assert_not_nil(config['update-ipod'])
44
+
45
+ #Test values on options that should occur only once.
46
+ config = @helper.parse_options [
47
+ '--set-disc-count', '1',
48
+ '--set-disc-count', '2', #Override previous value.
49
+ '--set-disc-number', '1'
50
+ ]
51
+ assert_equal(2, config['set-disc-count'])
52
+ assert_equal(1, config['set-disc-number'])
53
+
54
+ #Test default values.
55
+ config = @helper.parse_options [
56
+ '--volume-up',
57
+ '--volume-down',
58
+ '--scan-forwards',
59
+ '--scan-backwards'
60
+ ]
61
+ assert_equal(10, config['volume-up'])
62
+ assert_equal(10, config['volume-down'])
63
+ assert_equal(10, config['scan-forwards'])
64
+ assert_equal(10, config['scan-backwards'])
65
+ config = @helper.parse_options [
66
+ '--volume-up', '11',
67
+ '--volume-down', '12'
68
+ ]
69
+ assert_equal(11, config['volume-up'])
70
+ assert_equal(12, config['volume-down'])
71
+
72
+ #Test gathering of multiple values for same option.
73
+ config = @helper.parse_options [
74
+ '--playlist', 'foo',
75
+ '--playlist', 'bar',
76
+ '--find', 'baz',
77
+ '--find', 'eep'
78
+ ]
79
+ assert(config['playlist'].include?('foo'))
80
+ assert(config['playlist'].include?('bar'))
81
+ assert(config['find'].include?('baz'))
82
+ assert(config['find'].include?('eep'))
83
+
84
+ end
85
+
86
+
87
+ def test_set_default_options
88
+
89
+ #Test setting defaults on an empty config.
90
+ config = @helper.set_default_options
91
+ assert(config['library'])
92
+
93
+ #Test running on an existing config.
94
+ config = @helper.set_default_options(
95
+ {'library' => false}
96
+ )
97
+ assert_equal(false, config['library'])
98
+
99
+ #Test using entire library by default.
100
+ [
101
+ {'current-playlist' => true},
102
+ {'playlist' => ['foo', 'bar']},
103
+ {'all-playlists' => true},
104
+ {'create-playlist' => ['foo', 'bar']},
105
+ ].each do |options|
106
+ config = @helper.set_default_options(options)
107
+ assert_nil(config['library'], config.to_s)
108
+ end
109
+ config = @helper.set_default_options({'library' => true}) #Obviously, we shouldn't set library to false if it was explicitly specified.
110
+ assert(config['library'])
111
+ config = @helper.set_default_options({'library' => false}) #If library is explicitly set to false, keep it false.
112
+ assert_equal(false, config['library'])
113
+
114
+ end
115
+
116
+
117
+ def test_create_itunes_interface
118
+ interface = Itch.create_itunes_interface
119
+ assert_not_nil(interface.LibraryPlaylist)
120
+ end
121
+
122
+
123
+ def test_get_playlists
124
+
125
+ #Test that the appropriate playlist set is returned for various configs.
126
+ playlists = @helper.get_playlists({'library' => true})
127
+ assert_equal('Library', playlists[0].Name)
128
+ # playlists = @helper.get_playlists({'current-playlist' => true})
129
+ # assert_equal('Party Shuffle', playlists[0].Name)
130
+ playlists = @helper.get_playlists({'all-playlists' => true})
131
+ assert_equal('Library', playlists[0].Name)
132
+ assert_equal('Party Shuffle', playlists[1].Name)
133
+ playlists = @helper.get_playlists({'create-playlist' => ['foo', 'bar']})
134
+ assert_equal('foo', playlists[0].Name)
135
+ assert_equal('bar', playlists[1].Name)
136
+ playlists = @helper.get_playlists({'playlist' => ['Recently Added', 'My Top Rated']})
137
+ assert_equal('Recently Added', playlists[0].Name)
138
+ assert_equal('My Top Rated', playlists[1].Name)
139
+
140
+ end
141
+
142
+
143
+ def test_delete_playlists
144
+
145
+ #Create some temporary playlists.
146
+ playlists = @helper.get_playlists({'create-playlist' => ['delete1', 'delete2']})
147
+ #Test that the playlist is deleted.
148
+ @helper.delete_playlists({'delete-playlist' => ['delete1', 'delete2']})
149
+ assert_raise(RuntimeError) do
150
+ @helper.get_playlists({'playlist' => ['delete1']})
151
+ end
152
+ assert_raise(RuntimeError) do
153
+ @helper.get_playlists({'playlist' => ['delete2']})
154
+ end
155
+ #Test deleting a missing playlist.
156
+ assert_raise(RuntimeError) do
157
+ @helper.delete_playlists({'delete-playlist' => ['delete1']})
158
+ end
159
+
160
+ end
161
+
162
+
163
+ def test_get_tracks
164
+ #TODO: Re-implement using mock object.
165
+ # #Create a playlist to add files to.
166
+ # playlists = @helper.get_playlists({'create-playlist' => 'add1'})
167
+ # #Test that appropriate tracks are returned for various options.
168
+ # tracks = @helper.get_tracks({'add-file' => 'test.mp3'}, playlists)
169
+ # assert_equal('J-Hop', tracks[0].Name)
170
+ # tracks = @helper.get_tracks({'all-tracks' => true}, playlists)
171
+ # assert_equal('J-Hop', tracks[0].Name)
172
+ # tracks = @helper.get_tracks({'find' => 'J-Hop'}, playlists)
173
+ # assert_equal('J-Hop', tracks[0].Name)
174
+ # tracks = @helper.get_tracks({'visible-find' => 'J-Hop'}, playlists)
175
+ # assert_equal('J-Hop', tracks[0].Name)
176
+ # tracks = @helper.get_tracks({'find-artist' => 'Jay'}, playlists)
177
+ # assert_equal('J-Hop', tracks[0].Name)
178
+ # tracks = @helper.get_tracks({'find-album' => 'HopJ'}, playlists)
179
+ # assert_equal('J-Hop', tracks[0].Name)
180
+ # tracks = @helper.get_tracks({'find-composer' => 'MrSymphony'}, playlists)
181
+ # assert_equal('J-Hop', tracks[0].Name)
182
+ # tracks = @helper.get_tracks({'find-track-name' => 'J-Hop'}, playlists)
183
+ # assert_equal('J-Hop', tracks[0].Name)
184
+ end
185
+
186
+
187
+ def test_perform_general_operations
188
+
189
+ @helper.perform_general_operations({'volume' => 50})
190
+ assert_equal(50, @helper.interface.SoundVolume)
191
+ @helper.perform_general_operations({'volume-up' => 10})
192
+ assert_equal(60, @helper.interface.SoundVolume)
193
+ @helper.perform_general_operations({'volume-down' => 9})
194
+ assert_equal(51, @helper.interface.SoundVolume)
195
+
196
+ end
197
+
198
+
199
+ def test_perform_track_operations
200
+
201
+ track = MockOLE.new
202
+
203
+ #Test generic attribute setting.
204
+ def test (attribute, track, expected_method)
205
+ @helper.perform_track_operations({attribute => attribute}, [track])
206
+ assert_equal([expected_method, [attribute]], track.calls.pop)
207
+ end
208
+ test('set-artist', track, 'Artist=')
209
+ test('set-album', track, 'Album=')
210
+ test('set-bpm', track, 'BPM=')
211
+ test('set-comment', track, 'Comment=')
212
+ test('set-composer', track, 'Composer=')
213
+ test('set-disc-count', track, 'DiscCount=')
214
+ test('set-disc-number', track, 'DiscNumber=')
215
+ test('set-eq', track, 'EQ=')
216
+ test('set-genre', track, 'Genre=')
217
+ test('set-grouping', track, 'Grouping=')
218
+ test('set-name', track, 'Name=')
219
+ test('set-play-count', track, 'PlayedCount=')
220
+ test('set-skip-count', track, 'SkippedCount=')
221
+ test('set-track-count', track, 'TrackCount=')
222
+ test('set-track-number', track, 'TrackNumber=')
223
+ test('set-volume-adjustment', track, 'VolumeAdjustment=')
224
+ test('set-year', track, 'Year=')
225
+ #Test specialized attribute setting.
226
+ @helper.perform_track_operations({'set-enabled' => true}, [track])
227
+ assert_equal(['Enabled=', [true]], track.calls.pop)
228
+ @helper.perform_track_operations({'set-disabled' => true}, [track])
229
+ assert_equal(['Enabled=', [false]], track.calls.pop)
230
+ @helper.perform_track_operations({'set-rating' => 1}, [track])
231
+ assert_equal(['Rating=', [20]], track.calls.pop) #Rating is stored as a number from 1-100 internally
232
+ @helper.perform_track_operations({'set-rating' => 5}, [track])
233
+ assert_equal(['Rating=', [100]], track.calls.pop)
234
+
235
+ end
236
+
237
+
238
+ def test_track_info
239
+
240
+ track = MockObject.new
241
+
242
+ #Test that the right attribute is returned for each tag.
243
+ assert_equal('Artist', @helper.track_info(track, '%a'))
244
+ assert_equal('KindAsString', @helper.track_info(track, '%e'))
245
+ assert_equal('Album', @helper.track_info(track, '%A'))
246
+ def track.BPM
247
+ 120
248
+ end
249
+ assert_equal('120', @helper.track_info(track, '%b'))
250
+ assert_equal('Composer', @helper.track_info(track, '%c'))
251
+ assert_equal('Comment', @helper.track_info(track, '%C'))
252
+ def track.DiscNumber
253
+ 1
254
+ end
255
+ assert_equal('1', @helper.track_info(track, '%d'))
256
+ def track.DiscCount
257
+ 2
258
+ end
259
+ assert_equal('2', @helper.track_info(track, '%D'))
260
+ def track.Enabled
261
+ 1
262
+ end
263
+ assert_equal('enabled', @helper.track_info(track, '%E'))
264
+ def track.Enabled
265
+ 0
266
+ end
267
+ assert_equal('disabled', @helper.track_info(track, '%E'))
268
+ def track.Location
269
+ 'C:\content\Orbital\The Altogether (disc 2_ the Remixes, Unr\06 Beelzebeat.mp3'
270
+ end
271
+ assert_equal(track.Location, @helper.track_info(track, '%l'))
272
+ def track.PlayedCount
273
+ 15
274
+ end
275
+ assert_equal('15', @helper.track_info(track, '%p'))
276
+ assert_equal('EQ', @helper.track_info(track, '%q'))
277
+ assert_equal('Genre', @helper.track_info(track, '%g'))
278
+ assert_equal('Grouping', @helper.track_info(track, '%G'))
279
+ assert_equal('Name', @helper.track_info(track, '%n'))
280
+ def track.Rating
281
+ 100 #Internal rating is a number from 0-100.
282
+ end
283
+ assert_equal('5', @helper.track_info(track, '%r'))
284
+ def track.SkippedCount
285
+ 3
286
+ end
287
+ assert_equal('3', @helper.track_info(track, '%s'))
288
+ def track.TrackNumber
289
+ 2
290
+ end
291
+ assert_equal('2', @helper.track_info(track, '%t'))
292
+ def track.TrackCount
293
+ 12
294
+ end
295
+ assert_equal('12', @helper.track_info(track, '%T'))
296
+ assert_equal('VolumeAdjustment', @helper.track_info(track, '%v'))
297
+ assert_equal('Year', @helper.track_info(track, '%y'))
298
+
299
+ #Test multiple tags at once.
300
+ assert_equal('Artist - Album - 2 - Name', @helper.track_info(track, '%a - %A - %t - %n'))
301
+
302
+ #Test substituting % for %%.
303
+ assert_equal('Artist%Name', @helper.track_info(track, '%a%%%n'))
304
+ assert_equal('%Name', @helper.track_info(track, '%%%n'))
305
+ assert_equal('Artist%', @helper.track_info(track, '%a%%'))
306
+ assert_equal('%Album%', @helper.track_info(track, '%%%A%%'))
307
+ assert_equal('%', @helper.track_info(track, '%%'))
308
+
309
+ end
310
+
311
+
312
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.2
3
+ specification_version: 1
4
+ name: itch
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.2
7
+ date: 2007-04-26 00:00:00 -07:00
8
+ summary: Allows control of iTunes for Windows via a command line.
9
+ require_paths:
10
+ - lib
11
+ email: chicgeekaz@hotmail.com
12
+ homepage: http://code.google.com/p/itunes-control/
13
+ rubyforge_project: itch
14
+ description:
15
+ autorequire: itch
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Jay McGavren
31
+ files:
32
+ - LICENSE.txt
33
+ - README.txt
34
+ - bin/itch
35
+ - lib/itch.rb
36
+ - test/test_itch.rb
37
+ test_files:
38
+ - test/test_itch.rb
39
+ rdoc_options:
40
+ - --title
41
+ - Itch -- Control Helper for iTunes
42
+ - --main
43
+ - README.txt
44
+ extra_rdoc_files:
45
+ - README.txt
46
+ - LICENSE.txt
47
+ executables:
48
+ - itch
49
+ extensions: []
50
+
51
+ requirements:
52
+ - iTunes for Windows
53
+ dependencies: []
54
+