itunes_store_transporter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,127 @@
1
+ = iTunes::Store::Transporter
2
+
3
+ Upload and manage your assets in the iTunes Store using the iTunes Store's Transporter (+iTMSTransporter+).
4
+
5
+ === Overview
6
+
7
+ require "itunes/store/transporter"
8
+
9
+ itms = iTunes::Store::Transporter.new(:username => "CouchCaster",
10
+ :shortname => "bigtimer",
11
+ :password => "w3c@llYoU!")
12
+
13
+ itms.upload("/path/to/yourpackage.itmsp")
14
+ metadata = itms.lookup(:apple_id => "yourpackage")
15
+
16
+ begin
17
+ itms.verify("/path/to/package2.itmsp", :verify_asssets => false)
18
+ rescue iTunes::Store::Transporter::ExecutionError => e
19
+ puts "Exited with #{e.exitstatus}"
20
+
21
+ e.errors.each do |error|
22
+ puts "#{error.message} - #{error.code}"
23
+ puts "Basically, you have some faulty metadata" if error.missing_data?
24
+ end
25
+ end
26
+
27
+ === Description
28
+
29
+ iTunes::Store::Transporter is a wrapper around Apple's +iTMSTransporter+ program. It currently
30
+ supports the following operations:
31
+
32
+ * Upload packages
33
+ * Validate packages
34
+ * Retrieve status information
35
+ * Lookup package metadata
36
+ * List providers
37
+ * Retrieve iTunes metadata schemas
38
+
39
+ It also includes +itms+, an executable that's sorta like using +iTMSTransporter+ directly, except
40
+ that it can send email notifications and allows one to set global/per-command defaults via <code>$HOME/.itms</code>.
41
+
42
+ === Requirements
43
+
44
+ * Optout v0.0.2 (<code>gem install optout</code>)
45
+ * iTunes Store Transporter (http://www.apple.com/itunes/sellcontent)
46
+
47
+ === Running on Windows
48
+
49
+ On Windows +iTMSTransporter+ is called via the +iTMSTransporter.CMD+ batch file. This file does not handle the
50
+ +iTMSTransporter+'s exit status correctly, causing <code>iTunes::Store::Transporter</code> to report everything as a success.
51
+
52
+ This can be fixed by modifying +iTMSTransporter.CMD+ (note that the following does not mimic the batch file exactly):
53
+
54
+ ...
55
+
56
+ call iTMSTransporter\iTMSTransporter %*
57
+
58
+ REM Add this line:
59
+ set exited=%errorlevel%
60
+
61
+ cd %olddir%
62
+
63
+ REM Add this line too:
64
+ exit /b %exited%
65
+
66
+ === Using itms
67
+
68
+ <code>itms COMMAND [OPTIONS]</code>
69
+
70
+ * +COMMAND+ - The command (<code>iTunes::Store::Transporter</code> method) to run
71
+ * +OPTIONS+ - These are equivalent to the given +COMMAND+'s options except they must be given in long option format. For example <code>:apple_id => "X123"</code> would be <code>--apple-id=X123</code>.
72
+
73
+ ==== Config file
74
+
75
+ Default options and email notifications can be placed in a YAML file at <code>$HOME/.itms</code>.
76
+
77
+ # Global command defaults
78
+ path: /usr/bin
79
+ username: sshaw
80
+ password: Pa55W0rd!
81
+
82
+ # Global email defaults
83
+ email:
84
+ to: everyone@example.com
85
+ from: no-reply@example.com
86
+ host: smtp.example.com
87
+
88
+ # Verify command
89
+ verify:
90
+ shortname: lUzer
91
+
92
+ # Upload command
93
+ upload:
94
+ shortname: enc0d3rz
95
+ transport: Aspera
96
+ rate: 750_000
97
+ # Email notifications for the upload command
98
+ email:
99
+ success:
100
+ cc: assets@example.com
101
+ subject: iTunes Upload <%= @apple_id %>
102
+ message: |
103
+ <% @username > uploaded it using <%= @transport %>
104
+
105
+ Bye!
106
+ failure:
107
+ to: support@example.com
108
+ subject: Upload Failed!
109
+ message: |
110
+ Here's the problem:
111
+
112
+ <%= @error %>
113
+
114
+ Fix it!
115
+
116
+ === More Info
117
+
118
+ * Source Code: http://github.com/sshaw/itunes_store_transporter
119
+ * Bugs: http://github.com/sshaw/itunes_store_transporter/issues
120
+
121
+ === Author
122
+
123
+ Skye Shaw [sshaw AT lucas.cis.temple.edu]
124
+
125
+ === License
126
+
127
+ Released under the MIT License: http://www.opensource.org/licenses/MIT
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "erb"
4
+ require "yaml"
5
+ require "net/smtp"
6
+ require "itunes/store/transporter"
7
+
8
+ # Command line interface to the ITunes::Store::Transporter library.
9
+ # Using this is sorta like using iTMSTransporter except it can send email notifications and allows
10
+ # one to set global/per-command defaults via $HOME/.itms
11
+
12
+ module Command
13
+
14
+ class << self
15
+ def execute(name, options, argv)
16
+ name = name.capitalize
17
+ # Avoid Ruby 1.8/1.9 String/Symbol/const_defined? differences
18
+ unless constants.include?(name) || constants.include?(name.to_sym)
19
+ raise ArgumentError, "unknown command '#{name}'"
20
+ end
21
+
22
+ command = const_get(name).new(options)
23
+ command.execute(argv)
24
+ end
25
+ end
26
+
27
+ class Base
28
+ def initialize(options)
29
+ @itms = ITunes::Store::Transporter.new(options)
30
+ @options = options
31
+ end
32
+ end
33
+
34
+ class Providers < Base
35
+ def initialize(options)
36
+ # Let iTMSTransporter print the providers
37
+ options[:print_stdout] = true
38
+ super
39
+ end
40
+
41
+ def execute(args = [])
42
+ @itms.providers
43
+ end
44
+ end
45
+
46
+ class Lookup < Base
47
+ def execute(args = [])
48
+ filename = "#{@options[:apple_id] || @options[:vendor_id]}.xml"
49
+ metadata = @itms.lookup
50
+ File.open(filename, "w") { |f| f.write(metadata) }
51
+ puts "Metadata saved to #{filename}"
52
+ end
53
+ end
54
+
55
+ class Schema < Base
56
+ def execute(args = [])
57
+ filename = "#{@options[:version]}-#{@options[:type]}.rng"
58
+ schema = @itms.schema
59
+ File.open(filename, "w") { |f| f.write(schema) }
60
+ puts "Schema saved to #{filename}"
61
+ end
62
+ end
63
+
64
+ class Status < Base
65
+ def initialize(options)
66
+ # Let iTMSTransporter print the status
67
+ options[:print_stdout] = true
68
+ super
69
+ end
70
+
71
+ def execute(args = [])
72
+ @itms.status(args.shift)
73
+ end
74
+ end
75
+
76
+ class Upload < Base
77
+ def initialize(options)
78
+ # These can take a while so we let the user know what's going on
79
+ options[:print_stderr] = true unless options.include?(:print_stderr)
80
+ super
81
+ end
82
+
83
+ def execute(args = [])
84
+ @itms.upload(args.shift)
85
+ puts "Upload complete"
86
+ end
87
+ end
88
+
89
+ class Verify < Base
90
+ def execute(args = [])
91
+ @itms.verify(args.shift)
92
+ puts "Package is valid"
93
+ end
94
+ end
95
+
96
+ class Version < Base
97
+ def execute(args = [])
98
+ puts @itms.version
99
+ end
100
+ end
101
+ end
102
+
103
+ class Email
104
+ Binding = Class.new do
105
+ def initialize(options = {})
106
+ options.each do |k, v|
107
+ name = k.to_s.gsub(/[^\w]+/, "_")
108
+ instance_variable_set("@#{name}", v)
109
+ end
110
+ end
111
+ end
112
+
113
+ def initialize(config = {})
114
+ unless config["to"]
115
+ raise "No email recipeints provided, you must specify at least one"
116
+ end
117
+
118
+ @config = config
119
+ end
120
+
121
+ def send(params = {})
122
+ to = @config["to"].to_s.split /,/
123
+ host = @config["host"] || "localhost"
124
+ from = @config["from"] || "#{ENV["USER"]}@#{host}"
125
+ params = params.merge(@config)
126
+ message = ::ERB.new(build_template).def_class(Binding).new(params).result
127
+
128
+ # TODO: auth
129
+ smtp = ::Net::SMTP.start(host, @config["port"]) do |mail|
130
+ mail.send_message message, from, to
131
+ end
132
+ end
133
+
134
+ protected
135
+ def build_template
136
+ %w[to from subject cc bcc].inject("") do |t, key|
137
+ t << "#{key}: #{@config[key]}\n" if @config[key]
138
+ t
139
+ end + "\n#{@config["message"]}"
140
+ end
141
+ end
142
+
143
+ COMMANDS = ITunes::Store::Transporter.instance_methods(false).map(&:to_s)
144
+ RC_FILE_NAME = ".itms"
145
+
146
+ def home
147
+ ENV["HOME"] || ENV["USERPROFILE"]
148
+ end
149
+
150
+ # Should probably create a class for the options
151
+ def load_config(command)
152
+ config = {}
153
+ return config unless home
154
+
155
+ path = File.join(home, RC_FILE_NAME)
156
+ return config unless File.file?(path)
157
+
158
+ config = YAML.load_file(path)
159
+
160
+ # Get the global defaults. select() returns an aray on Ruby < 1.9
161
+ defaults = config.select { |k,v| !v.is_a?(Hash) }
162
+ defaults = Hash[defaults] unless defaults.is_a?(Hash)
163
+
164
+ config[command] = defaults.merge(config[command] || {})
165
+
166
+ # Normalize the email config
167
+ email = Hash.new { |h, k| h[k] = {} }
168
+
169
+ %w[success failure].each do |type|
170
+ email[type] = (config[command]["email"] ||= {})[type]
171
+ next unless email[type]
172
+
173
+ # Merge the global email options & the command's "global" options with the success/failure options
174
+ settings = (config["email"].to_a + config[command]["email"].to_a).reject { |k, v| k == "success" or k == "failure" }
175
+ settings.each do |k, v|
176
+ email[type][k] = email[type][k] ? "#{email[type][k]}, #{v}" : v
177
+ end
178
+ end
179
+
180
+ # ITunes::Store::Transporter uses Symbols for options
181
+ config[command] = config[command].inject({}) do |cfg, (k,v)|
182
+ cfg[k.to_sym] = v unless k.empty? # Avoid intern empty string errors in 1.8
183
+ cfg
184
+ end
185
+
186
+ config[command][:email] = email
187
+ config[command]
188
+ end
189
+
190
+ def print_errors(e)
191
+ $stderr.puts "#{e.errors.length} error(s)"
192
+ $stderr.puts "-" * 25
193
+ # TODO: group by type
194
+ e.errors.each_with_index do |msg, i|
195
+ $stderr.puts "#{i+1}. #{msg}"
196
+ end
197
+ end
198
+
199
+ def send_email(config, options)
200
+ return unless config
201
+ mail = Email.new(config)
202
+ mail.send(options)
203
+ end
204
+
205
+ command = ARGV.shift
206
+ abort("usage: itms command [options]") unless command
207
+ abort("invalid command '#{command}', valid commands are: #{COMMANDS.sort.join(', ')}") unless COMMANDS.include?(command)
208
+
209
+ options = load_config(command)
210
+
211
+ while ARGV.any?
212
+ opt = ARGV.first.dup
213
+ break unless opt.sub!(/\A--(?=\w)/, "")
214
+
215
+ key, val = opt.split(/=/, 2)
216
+ key.gsub!(/-/, "_")
217
+ # TODO: false, --no-xxxx
218
+ val = true unless val
219
+ val = val.to_i if val =~ /\A\d+\z/
220
+ options[key.to_sym] = val
221
+ ARGV.shift
222
+ end
223
+
224
+ # Keys for this are strings
225
+ email_options = options.delete(:email) || {}
226
+ command_options = options.dup
227
+ options[:argv] = ARGV.dup
228
+ options[:command] = command
229
+
230
+ begin
231
+ puts "Running command '#{command}'"
232
+ Command.execute(command, command_options, ARGV)
233
+ send_email(email_options["success"], options)
234
+ rescue ITunes::Store::Transporter::ExecutionError => e
235
+ print_errors(e)
236
+ options[:error] = e
237
+ send_email(email_options["failure"], options)
238
+ exit 1
239
+ rescue ITunes::Store::Transporter::TransporterError => e
240
+ $stderr.puts e
241
+ exit 2
242
+ end
243
+
@@ -0,0 +1,236 @@
1
+ require "itunes/store/transporter/command"
2
+ require "itunes/store/transporter/command/lookup"
3
+ require "itunes/store/transporter/command/providers"
4
+ require "itunes/store/transporter/command/schema"
5
+ require "itunes/store/transporter/command/status"
6
+ require "itunes/store/transporter/command/upload"
7
+ require "itunes/store/transporter/command/verify"
8
+ require "itunes/store/transporter/command/version"
9
+
10
+ module ITunes
11
+ module Store
12
+ ##
13
+ # Upload and manage your assets in the iTunes Store using the iTunes Store's Transporter (+iTMSTransporter+).
14
+
15
+ class Transporter
16
+
17
+ ##
18
+ # === Arguments
19
+ #
20
+ # [options (Hash)] Transporter options
21
+ #
22
+ # === Options
23
+ #
24
+ # Options given here will be used as defaults for all subsequent method calls. Thus you can set method specific options here but, if you call a method that does not accept one of these options, an OptionError will be raised.
25
+ #
26
+ # See specific methods for a list of options.
27
+ #
28
+ # [:username (String)] Your username
29
+ # [:password (String)] Your password
30
+ # [:shortname (String)] Your shortname. Optional, not every iTunes account has one
31
+ # [:path (String)] The path to the +iTMSTransporter+. Optional.
32
+ # [:print_stdout (Boolean)] Print +iTMSTransporter+'s stdout to your stdout. Defaults to +false+.
33
+ # [:print_stderr (Boolean)] Print +iTMSTransporter+'s stderr to your stderr. Defaults to +false+.
34
+ #
35
+
36
+ def initialize(options = nil)
37
+ @defaults = create_options(options)
38
+ @config = {
39
+ :path => @defaults.delete(:path),
40
+ :print_stdout => @defaults.delete(:print_stdout),
41
+ :print_stderr => @defaults.delete(:print_stderr),
42
+ }
43
+ end
44
+
45
+ ##
46
+ # :method: lookup
47
+ # :call-seq:
48
+ # lookup(options = {})
49
+ #
50
+ # Retrieve the metadata for a previously delivered package.
51
+ #
52
+ # === Arguments
53
+ #
54
+ # [options (Hash)] Transporter options
55
+ #
56
+ # ==== Options
57
+ #
58
+ # You must use either the +:apple_id+ or +:vendor_id+ option to identify the package
59
+ #
60
+ # === Errors
61
+ #
62
+ # TransporterError, OptionError, ExecutionError
63
+ #
64
+ # === Returns
65
+ #
66
+ # [String] The metadata
67
+
68
+ ##
69
+ # :method: providers
70
+ # :call-seq:
71
+ # providers(options = {})
72
+ #
73
+ # List of Providers for whom your account is authorzed to deliver for.
74
+ #
75
+ # === Arguments
76
+ #
77
+ # [options (Hash)] Transporter options
78
+ #
79
+ # === Errors
80
+ #
81
+ # TransporterError, OptionError, ExecutionError
82
+ #
83
+ # === Returns
84
+ #
85
+ # [Array] Each element is a +Hash+ with two keys: +:shortname+ and +:longname+ representing the given provider's long and short names
86
+
87
+ ##
88
+ # :method: schema
89
+ # :call-seq:
90
+ # schema(options = {})
91
+ #
92
+ # Download a RelaxNG schema file for a particular metadata specification.
93
+ #
94
+ # === Arguments
95
+ #
96
+ # [options (Hash)] Transporter options
97
+ #
98
+ # === Options
99
+ #
100
+ # [:type (String)] transitional or strict
101
+ # [:version (String)] The schema version you'd like to download. This is typically in the form of +schemaVERSION+. E.g., +film4.8+
102
+ #
103
+ # === Errors
104
+ #
105
+ # TransporterError, OptionError, ExecutionError
106
+ #
107
+ # === Returns
108
+ #
109
+ # [String] The schema
110
+
111
+ ##
112
+ # :method: status
113
+ # :call-seq:
114
+ # status(options = {})
115
+ #
116
+ # Retrieve the status of a previously uploaded package.
117
+ #
118
+ # === Arguments
119
+ #
120
+ # [options (Hash)] Transporter options
121
+ #
122
+ # === Options
123
+ #
124
+ # [:vendor_id (String)] ID of the package you want status info on
125
+ #
126
+ # === Errors
127
+ #
128
+ # TransporterError, OptionError, ExecutionError
129
+ #
130
+ # === Returns
131
+ #
132
+ # [Hash] Descibes various facets of the package's status.
133
+
134
+ ##
135
+ # :method: upload
136
+ # :call-seq:
137
+ # upload(package, options = {})
138
+ #
139
+ # Upload a package to the iTunes Store.
140
+ #
141
+ # === Arguments
142
+ #
143
+ # [package (String)] The path to the package directory to upload. Package names must end in +.itmsp+.
144
+ # [options (Hash)] Transporter options
145
+ #
146
+ # === Options
147
+ #
148
+ # [:transport (String)] The method/protocol used to upload your package. Optional. Can be one of: <code>"Aspera"</code>, <code>"Signiant"</code>, or <code>"DEV"</code>. By default +iTMSTransporter+ automatically selects the transport.
149
+ # [:rate (Integer)] Target bitrate in Kbps. Optional, only used with +Aspera+ and +Signiant+
150
+ # [:success (String)] A directory to move the package to if the upload succeeds
151
+ # [:failure (String)] A directory to move the package to if the upload fails
152
+ # [:delete (Boolean)] Delete the package if the upload succeeds. Defaults to +false+.
153
+ # [:log_history (String)] Write an +iTMSTransporter+ log to this directory. Off by default.
154
+ #
155
+ # === Errors
156
+ #
157
+ # TransporterError, OptionError, ExecutionError
158
+ #
159
+ # === Returns
160
+ #
161
+ # +true+ if the upload was successful.
162
+
163
+ ##
164
+ # :method: verify
165
+ # :call-seq:
166
+ # verify(package, options = {})
167
+ #
168
+ # Validate the contents of a package's metadata and assets.
169
+ #
170
+ # If verification fails an ExecutionError containing the errors will be raised.
171
+ # Each error message is an instance of TransporterMessage.
172
+ #
173
+ # === Arguments
174
+ #
175
+ # [package (String)] The path to the package directory to verify. Package names must end in +.itmsp+.
176
+ # [options (Hash)] Verify options
177
+ #
178
+ # === Options
179
+ #
180
+ # [:verify_assets (Boolean)] If false the assets will not be verified. Defaults to +true+.
181
+ #
182
+ # === Errors
183
+ #
184
+ # TransporterError, OptionError, ExecutionError
185
+ #
186
+ # === Returns
187
+ #
188
+ # +true+ if the package was verified.
189
+
190
+ ##
191
+ # :method: version
192
+ # :call-seq:
193
+ # version
194
+ #
195
+ # Return the underlying +iTMSTransporter+ version.
196
+ #
197
+ # === Returns
198
+ #
199
+ # [String] The version number
200
+
201
+ %w|upload verify|.each do |command|
202
+ define_method(command) do |package, *options|
203
+ cmd_options = create_options(options.first)
204
+ cmd_options[:package] = package
205
+ run_command(command, cmd_options)
206
+ end
207
+ end
208
+
209
+ %w|lookup providers schema status version|.each do |command|
210
+ define_method(command) { |*options| run_command(command, options.shift) }
211
+ end
212
+
213
+ private
214
+ def run_command(name, options)
215
+ Command.const_get(name.capitalize).new(@config, @defaults).run(create_options(options))
216
+ end
217
+
218
+ def create_options(options)
219
+ options ||= {}
220
+ raise ArgumentError, "options must be a Hash" unless Hash === options
221
+ options.dup
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ unless ENV["ITUNES_STORE_TRANSPORTER_NO_SYNTAX_SUGAR"].to_i > 0
228
+ def iTunes
229
+ ITunes
230
+ end
231
+ end
232
+
233
+
234
+
235
+
236
+