episodic-platform 0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|