itunes_store_transporter 0.0.1

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.
@@ -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
+