episodic-platform 0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +35 -0
- data/PostInstall.txt +7 -0
- data/README.rdoc +86 -0
- data/Rakefile +45 -0
- data/lib/episodic/platform.rb +82 -0
- data/lib/episodic/platform/analytics_methods.rb +190 -0
- data/lib/episodic/platform/base.rb +64 -0
- data/lib/episodic/platform/collection_response.rb +45 -0
- data/lib/episodic/platform/connection.rb +305 -0
- data/lib/episodic/platform/exceptions.rb +105 -0
- data/lib/episodic/platform/query_methods.rb +721 -0
- data/lib/episodic/platform/response.rb +94 -0
- data/lib/episodic/platform/write_methods.rb +446 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/fixtures/1-0.mp4 +0 -0
- data/test/fixtures/create-episode-response-s3.xml +15 -0
- data/test/fixtures/create-episode-response.xml +3 -0
- data/test/fixtures/episodes-response.xml +408 -0
- data/test/fixtures/episodes-summary-report-response.xml +4 -0
- data/test/fixtures/invalid-param-response-multiple.xml +8 -0
- data/test/fixtures/invalid-param-response-single.xml +9 -0
- data/test/fixtures/playlists-response.xml +611 -0
- data/test/fixtures/shows-response.xml +26 -0
- data/test/test_analytics_requests.rb +42 -0
- data/test/test_analytics_responses.rb +14 -0
- data/test/test_connection.rb +31 -0
- data/test/test_error_responses.rb +29 -0
- data/test/test_helper.rb +8 -0
- data/test/test_query_requests.rb +62 -0
- data/test/test_query_responses.rb +165 -0
- data/test/test_write_requests.rb +56 -0
- data/test/test_write_responses.rb +24 -0
- metadata +135 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
module Episodic #:nodoc:
|
2
|
+
|
3
|
+
#
|
4
|
+
# Episodic::Platform is a Ruby library for Episodic's Platform REST API (http://app.episodic.com/help/server_api)
|
5
|
+
#
|
6
|
+
# == Getting started
|
7
|
+
#
|
8
|
+
# To get started you need to require 'episodic/platform':
|
9
|
+
#
|
10
|
+
# require 'episodic/platform'
|
11
|
+
#
|
12
|
+
# Before you can use any of the object methods, you need to create a connection using <tt>Base.establish_connection!</tt>. The
|
13
|
+
# <tt>Base.establish_connection!</tt> method requires that you pass your Episodic API Key and Episodic Secret Key.
|
14
|
+
#
|
15
|
+
# Episodic::Platform::Base.establish_connection!('my_api_key', 'my_secret_key')
|
16
|
+
#
|
17
|
+
# == Handling errors
|
18
|
+
#
|
19
|
+
# Any errors returned from the Episodic Platform API are converted to exceptions and raised from the called method. For example,
|
20
|
+
# the following response would cause <tt>Episodic::Platform::InvalidAPIKey</tt> to be raised.
|
21
|
+
#
|
22
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
23
|
+
# <error>
|
24
|
+
# <code>1</code>
|
25
|
+
# <message>Invalid API Key</message>
|
26
|
+
# </error>
|
27
|
+
#
|
28
|
+
module Platform
|
29
|
+
API_HOST = 'app.episodic.com'
|
30
|
+
API_VERSION = 'v2'
|
31
|
+
|
32
|
+
#
|
33
|
+
# Episodic::Platform::Base is the abstract super class of all classes who make requests against the Episodic Platform REST API.
|
34
|
+
#
|
35
|
+
# Establishing a connection with the Base class is the entry point to using the library:
|
36
|
+
#
|
37
|
+
# Episodic::Platform::Base.establish_connection!('my_api_key', 'my_secret_key')
|
38
|
+
#
|
39
|
+
class Base
|
40
|
+
|
41
|
+
class << self
|
42
|
+
|
43
|
+
#
|
44
|
+
# Helper method to construct an Episodic Platform API request URL.
|
45
|
+
#
|
46
|
+
# ==== Parameters
|
47
|
+
#
|
48
|
+
# api_name<String>:: Specifies the API you are calling. Examples are "write", query" and "analytics"
|
49
|
+
# method_name<String>:: The method being invoked.
|
50
|
+
#
|
51
|
+
# ==== Returns
|
52
|
+
#
|
53
|
+
# URI:: The constructed URL.
|
54
|
+
#
|
55
|
+
def construct_url api_name, method_name
|
56
|
+
return URI.parse("http://#{connection.connection_options[:api_host] || API_HOST}/api/#{API_VERSION}/#{api_name}/#{method_name}")
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Episodic
|
2
|
+
|
3
|
+
module Platform
|
4
|
+
|
5
|
+
#
|
6
|
+
# Base class for responses that return an collection of objects.
|
7
|
+
#
|
8
|
+
class CollectionResponse < Response
|
9
|
+
|
10
|
+
COLLECTION_RESPONSE_ATTRIBUTES = [:page, :pages, :total, :per_page]
|
11
|
+
|
12
|
+
#
|
13
|
+
# Constructor
|
14
|
+
#
|
15
|
+
# ==== Parameters
|
16
|
+
#
|
17
|
+
# response<Episodic::Platform::HttpResponse>:: The response object returned from an Episodic Platform API request.
|
18
|
+
# xml_options<Hash>:: A set of options used by XmlSimple when parsing the response body
|
19
|
+
#
|
20
|
+
def initialize response, xml_options = {}
|
21
|
+
super(response, xml_options)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Override to look up attributes by name
|
26
|
+
#
|
27
|
+
def method_missing(method_sym, *arguments, &block)
|
28
|
+
method_name = method_sym.to_s
|
29
|
+
if (COLLECTION_RESPONSE_ATTRIBUTES.include?(method_sym))
|
30
|
+
return @parsed_body[method_name].to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
return super
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Attributes can be accessed as methods.
|
38
|
+
#
|
39
|
+
def respond_to?(symbol, include_private = false)
|
40
|
+
return COLLECTION_RESPONSE_ATTRIBUTES.include?(symbol)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,305 @@
|
|
1
|
+
module Episodic
|
2
|
+
module Platform
|
3
|
+
|
4
|
+
#
|
5
|
+
# Class used to make the actual requests to the Episodic Platform API.
|
6
|
+
#
|
7
|
+
class Connection
|
8
|
+
|
9
|
+
attr_reader :episodic_api_key, :episodic_secret_key, :connection_options
|
10
|
+
|
11
|
+
#
|
12
|
+
# Constructor
|
13
|
+
#
|
14
|
+
# episodic_api_key<String>:: The caller's Episodic API Key
|
15
|
+
# episodic_secret_key<String>:: The caller's Episodic Secret Key
|
16
|
+
# options<Hash>:: Used mostly for testing by allowing the caller to override some constants such
|
17
|
+
# the API host.
|
18
|
+
#
|
19
|
+
def initialize(episodic_api_key, episodic_secret_key, options = {})
|
20
|
+
@episodic_api_key = episodic_api_key
|
21
|
+
@episodic_secret_key = episodic_secret_key
|
22
|
+
@connection_options = options
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Perform the POST request to the specified URL. This method takes the params passed in and
|
27
|
+
# generates the signature parameter using the Episodic Secret Key for this connection. The
|
28
|
+
# signature, the Episodic API Key for this connection and the passed in params are then used to
|
29
|
+
# generate the form post.
|
30
|
+
#
|
31
|
+
# If there are any filenames passed then these are also included in the form post.
|
32
|
+
#
|
33
|
+
# ==== Parameters
|
34
|
+
#
|
35
|
+
# url<URI>:: The URL to the Episodic Platform API endpoint
|
36
|
+
# params<Hash>:: A hash of parameters to include include in the post request.
|
37
|
+
# file_params<Hash>:: A hash of file parameters. The name is the parameter name and value is the path to the file.
|
38
|
+
#
|
39
|
+
# ==== Returns
|
40
|
+
#
|
41
|
+
# Episodic::Platform::HTTPResponse:: The full response object.
|
42
|
+
#
|
43
|
+
def do_post url, params, file_params = nil
|
44
|
+
|
45
|
+
# Convert all the params to strings
|
46
|
+
request_params = convert_params_for_request(params)
|
47
|
+
|
48
|
+
# Add in the common params
|
49
|
+
append_common_params(request_params)
|
50
|
+
|
51
|
+
c = Curl::Easy.new(url.to_s)
|
52
|
+
c.multipart_form_post = true
|
53
|
+
|
54
|
+
fields = []
|
55
|
+
request_params.each_pair do |name, value|
|
56
|
+
fields << Curl::PostField.content(name, value)
|
57
|
+
end
|
58
|
+
file_params.each do |name, value|
|
59
|
+
fields << Curl::PostField.file(name, value)
|
60
|
+
end unless file_params.nil?
|
61
|
+
|
62
|
+
# Make the request
|
63
|
+
c.http_post(*fields)
|
64
|
+
|
65
|
+
return Episodic::Platform::HTTPResponse.new(c.response_code, c.body_str)
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Perform the GET request to the specified URL. This method takes the params passed in and
|
70
|
+
# generates the signature parameter using the Episodic Secret Key for this connection. The
|
71
|
+
# signature, the Episodic API Key for this connection and the passed in params are then used to
|
72
|
+
# generate the query string.
|
73
|
+
#
|
74
|
+
# ==== Parameters
|
75
|
+
#
|
76
|
+
# url<URI>:: The URL to the Episodic Platform API endpoint
|
77
|
+
# params<Hash>:: A hash of parameters to include include in the query string.
|
78
|
+
#
|
79
|
+
# ==== Returns
|
80
|
+
#
|
81
|
+
# Episodic::Platform::HTTPResponse:: The full response object.
|
82
|
+
#
|
83
|
+
def do_get url, params
|
84
|
+
|
85
|
+
# Convert all the params to strings
|
86
|
+
request_params = convert_params_for_request(params)
|
87
|
+
|
88
|
+
# Add in the common params
|
89
|
+
append_common_params(request_params)
|
90
|
+
|
91
|
+
queryString = ""
|
92
|
+
request_params.keys.each_with_index do |key, index|
|
93
|
+
queryString << "#{index == 0 ? '?' : '&'}#{key}=#{::URI.escape(request_params[key])}"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Create the request
|
97
|
+
http = Net::HTTP.new(url.host, url.port)
|
98
|
+
response = http.start() {|req| req.get(url.path + queryString)}
|
99
|
+
|
100
|
+
return Episodic::Platform::HTTPResponse.new(response.code, response.body)
|
101
|
+
end
|
102
|
+
|
103
|
+
protected
|
104
|
+
|
105
|
+
#
|
106
|
+
# Helper method to generate the request signature
|
107
|
+
#
|
108
|
+
# ==== Parameters
|
109
|
+
#
|
110
|
+
# params<Hash>:: The set of params that will be passed either in the form post or in the
|
111
|
+
# query string.
|
112
|
+
#
|
113
|
+
def generate_signature_from_params params
|
114
|
+
sorted_keys = params.keys.sort {|x,y| x.to_s <=> y.to_s }
|
115
|
+
string_to_sign = @episodic_secret_key
|
116
|
+
sorted_keys.each do |key|
|
117
|
+
string_to_sign += "#{key.to_s}=#{params[key]}"
|
118
|
+
end
|
119
|
+
|
120
|
+
return Digest::SHA256.hexdigest(string_to_sign)
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Apply the common Episodic params such as expires, signature and key
|
125
|
+
#
|
126
|
+
# ==== Parameters
|
127
|
+
#
|
128
|
+
# params<Hash>:: The params to update.
|
129
|
+
#
|
130
|
+
def append_common_params params
|
131
|
+
|
132
|
+
# Add an expires value if it has not been added already
|
133
|
+
params["expires"] ||= (Time.now.to_i + 30).to_s
|
134
|
+
|
135
|
+
# Sign the request
|
136
|
+
params["signature"] = self.generate_signature_from_params(params)
|
137
|
+
|
138
|
+
# Add our key
|
139
|
+
params["key"] = @episodic_api_key
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Converts all parameters to a form for a request. This includes converting arrays to comma delimited strings,
|
144
|
+
# Times to integers and Hashes to a form depending on its level in the passed in params.
|
145
|
+
#
|
146
|
+
# ==== Parameters
|
147
|
+
#
|
148
|
+
# params<Hash>:: The params to convert.
|
149
|
+
#
|
150
|
+
# ==== Returns
|
151
|
+
#
|
152
|
+
# Hash:: A single level hash where all keys and values are strings.
|
153
|
+
#
|
154
|
+
def convert_params_for_request params
|
155
|
+
result = {}
|
156
|
+
|
157
|
+
params.each_pair do |key, value|
|
158
|
+
|
159
|
+
# We don't want to deal with nils
|
160
|
+
value = "" if value.nil?
|
161
|
+
|
162
|
+
if value.is_a?(Array)
|
163
|
+
# Convert to a comma delimited string
|
164
|
+
result[key.to_s] = value.join(",")
|
165
|
+
elsif value.is_a?(Time)
|
166
|
+
#Convert the time to an integer (then string)
|
167
|
+
result[key.to_s] = value.to_i.to_s
|
168
|
+
elsif value.is_a?(Hash)
|
169
|
+
# Used for custom fields
|
170
|
+
value.each_pair do |sub_key, sub_value|
|
171
|
+
sub_value = "" if sub_value.nil?
|
172
|
+
if (sub_value.is_a?(Time))
|
173
|
+
result["#{key.to_s}[#{sub_key.to_s}]"] = sub_value.to_i.to_s
|
174
|
+
elsif (sub_value.is_a?(Hash))
|
175
|
+
# Put the hash in the external select field form
|
176
|
+
val = ""
|
177
|
+
sub_value.each_pair do |k, v|
|
178
|
+
val << "#{k}|#{v};"
|
179
|
+
end
|
180
|
+
result["#{key.to_s}[#{sub_key.to_s}]"] = val
|
181
|
+
else
|
182
|
+
result["#{key.to_s}[#{sub_key.to_s}]"] = sub_value.to_s
|
183
|
+
end
|
184
|
+
end
|
185
|
+
else
|
186
|
+
result[key.to_s] = value.to_s
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
return result
|
191
|
+
end
|
192
|
+
|
193
|
+
module Management #:nodoc:
|
194
|
+
def self.included(base)
|
195
|
+
base.cattr_accessor :connections
|
196
|
+
base.connections = {}
|
197
|
+
base.extend ClassMethods
|
198
|
+
end
|
199
|
+
|
200
|
+
#
|
201
|
+
# Manage the creation and destruction of connections for Episodic::Platform::Base and its subclasses. Connections are
|
202
|
+
# created with establish_connection!.
|
203
|
+
#
|
204
|
+
module ClassMethods
|
205
|
+
|
206
|
+
#
|
207
|
+
# Creates a new connection with which to make requests to the Episodic Platform for the
|
208
|
+
# calling class.
|
209
|
+
#
|
210
|
+
# Episodic::Platform::Base.establish_connection!(episodic_api_key, episodic_secret_key)
|
211
|
+
#
|
212
|
+
# You can set connections for every subclass of Episodic::Platform::Base. Once the initial
|
213
|
+
# connection is made on Base, all subsequent connections will inherit whatever values you
|
214
|
+
# don't specify explictly.
|
215
|
+
#
|
216
|
+
# ==== Parameters
|
217
|
+
#
|
218
|
+
# episodic_api_key<String>:: This is your Episodic API Key
|
219
|
+
# episodic_secret_key<String>:: This is your Episodic Secret Key
|
220
|
+
#
|
221
|
+
def establish_connection!(episodic_api_key, episodic_secret_key, options = {})
|
222
|
+
connections[connection_name] = Connection.new(episodic_api_key, episodic_secret_key, options)
|
223
|
+
end
|
224
|
+
|
225
|
+
#
|
226
|
+
# Returns the connection for the current class, or Base's default connection if the current class does not
|
227
|
+
# have its own connection.
|
228
|
+
#
|
229
|
+
# If not connection has been established yet, NoConnectionEstablished will be raised.
|
230
|
+
#
|
231
|
+
# ==== Returns
|
232
|
+
#
|
233
|
+
# Episodic::Platform::Connection:: The connection for the current class or the default connection
|
234
|
+
#
|
235
|
+
def connection
|
236
|
+
if connected?
|
237
|
+
connections[connection_name] || default_connection
|
238
|
+
else
|
239
|
+
raise NoConnectionEstablished
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
#
|
244
|
+
# Returns true if a connection has been made yet.
|
245
|
+
#
|
246
|
+
# ==== Returns
|
247
|
+
#
|
248
|
+
# Boolean:: <tt>true</tt> if there is at least one connection
|
249
|
+
#
|
250
|
+
def connected?
|
251
|
+
!connections.empty?
|
252
|
+
end
|
253
|
+
|
254
|
+
#
|
255
|
+
# Removes the connection for the current class. If there is no connection for the current class, the default
|
256
|
+
# connection will be removed.
|
257
|
+
#
|
258
|
+
# ==== Parameters
|
259
|
+
#
|
260
|
+
# name<String>:: The name of the connection. This defaults to the default connection name.
|
261
|
+
#
|
262
|
+
def disconnect(name = connection_name)
|
263
|
+
name = default_connection unless connections.has_key?(name)
|
264
|
+
connections.delete(name)
|
265
|
+
end
|
266
|
+
|
267
|
+
#
|
268
|
+
# Removes all connections
|
269
|
+
#
|
270
|
+
def disconnect!
|
271
|
+
connections.each_key {|connection| disconnect(connection)}
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
#
|
277
|
+
# Get the name of this connection
|
278
|
+
#
|
279
|
+
# ==== Returns
|
280
|
+
#
|
281
|
+
# String:: The connection name
|
282
|
+
#
|
283
|
+
def connection_name
|
284
|
+
name
|
285
|
+
end
|
286
|
+
|
287
|
+
#
|
288
|
+
# Hardcoded default connection name
|
289
|
+
#
|
290
|
+
def default_connection_name
|
291
|
+
'Episodic::Platform::Base'
|
292
|
+
end
|
293
|
+
|
294
|
+
#
|
295
|
+
# Shortcut to get the default connection
|
296
|
+
#
|
297
|
+
def default_connection
|
298
|
+
connections[default_connection_name]
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Episodic
|
2
|
+
module Platform
|
3
|
+
|
4
|
+
#
|
5
|
+
# Abstract super class of all Episodic::Platform exceptions
|
6
|
+
#
|
7
|
+
class EpisodicPlatformException < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
class FileUploadFailed < EpisodicPlatformException
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# An execption that is raised as a result of the response content.
|
15
|
+
#
|
16
|
+
class ResponseError < EpisodicPlatformException
|
17
|
+
|
18
|
+
attr_reader :response
|
19
|
+
|
20
|
+
#
|
21
|
+
# Constructor
|
22
|
+
#
|
23
|
+
# ==== Parameters
|
24
|
+
#
|
25
|
+
# message<String>:: The message to include in the exception
|
26
|
+
# response<Episodic::Platform::HTTPResponse>:: The response object.
|
27
|
+
#
|
28
|
+
def initialize(message, response)
|
29
|
+
@response = response
|
30
|
+
super(message)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# There was an unexpected error on the server. .
|
36
|
+
#
|
37
|
+
class InternalError < ResponseError
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# The API Key wasn't provided or is invalid or the signature is invalid.
|
42
|
+
#
|
43
|
+
class InvalidAPIKey < ResponseError
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# The requested report could not be found. Either the report token is invalid or the report has expired and is no longer available.
|
48
|
+
#
|
49
|
+
class ReportNotFound < ResponseError
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# The request failed to specifiy one or more of the required parameters to an API method.
|
54
|
+
#
|
55
|
+
class MissingRequiredParameter < ResponseError
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# The request is no longer valid because the expires parameter specifies a time that has passed.
|
60
|
+
#
|
61
|
+
class RequestExpired < ResponseError
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# The specified object (i.e. show, episode, etc.) could not be found.
|
66
|
+
#
|
67
|
+
class NotFound < ResponseError
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# API access for the user is disabled.
|
72
|
+
#
|
73
|
+
class APIAccessDisabled < ResponseError
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# The value specified for a parameter is not valid.
|
78
|
+
#
|
79
|
+
class InvalidParameters < ResponseError
|
80
|
+
|
81
|
+
#
|
82
|
+
# Constructor. Override to include inforation about the invalid parameter(s).
|
83
|
+
#
|
84
|
+
# ==== Parameters
|
85
|
+
#
|
86
|
+
# message<String>:: The message to include in the exception
|
87
|
+
# response<Episodic::Platform::HTTPResponse>:: The response object.
|
88
|
+
# invalid_parameters<Object>:: This is either a Hash or an Array of hashes if there is more than one invalid parameter.
|
89
|
+
#
|
90
|
+
def initialize(message, response, invalid_parameters)
|
91
|
+
|
92
|
+
if (invalid_parameters)
|
93
|
+
invalid_parameters["invalid_parameter"] = [invalid_parameters["invalid_parameter"]] if invalid_parameters["invalid_parameter"].is_a?(Hash)
|
94
|
+
|
95
|
+
# Append to the message
|
96
|
+
invalid_parameters["invalid_parameter"].each do |ip|
|
97
|
+
message << "\n#{ip['name']}: #{ip['content']}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
super(message, response)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|