bbc_redux 0.4.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ require 'virtus'
2
+
3
+ module BBC
4
+ module Redux
5
+
6
+ # Redux API Channel Category Object
7
+ #
8
+ # @example Properties of the channel category object
9
+ #
10
+ # category = redux_client.channel_categories.first
11
+ #
12
+ # category.description #=> String
13
+ # category.id #=> Integer
14
+ # category.priority #=> Integer
15
+ #
16
+ # @author Matt Haynes <matt.haynes@bbc.co.uk>
17
+ class ChannelCategory
18
+
19
+ include Virtus.value_object
20
+
21
+ # @!attribute [r] description
22
+ # @return [String] category's description, e.g. 'BBC TV'
23
+ attribute :description, String
24
+
25
+ # @!attribute [r] id
26
+ # @return [Integer] category's id
27
+ attribute :id, Integer
28
+
29
+ # @!attribute [r] priority
30
+ # @return [String] category's priority, a hint for display in views
31
+ attribute :priority, Integer
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,302 @@
1
+ require 'date'
2
+ require 'json'
3
+ require 'typhoeus'
4
+
5
+ module BBC
6
+ module Redux
7
+
8
+ # Redux API Client
9
+ #
10
+ # @example Initialize client with either username and password or token
11
+ #
12
+ # client = BBC::Redux::Client.new({
13
+ # :username => 'username',
14
+ # :password => 'password',
15
+ # })
16
+ #
17
+ # client = BBC::Redux::Client.new :token => 'some-token'
18
+ #
19
+ # @example Using the client to retrieve data
20
+ #
21
+ # client.asset('5966413090093319525') #=> BBC::Redux::Asset
22
+ # client.channel_categories #=> Array<BBC::Redux::ChannelCategory>
23
+ # client.channels #=> Array<BBC::Redux::Channel>
24
+ # client.schedule(Date.today) #=> Array<BBC::Redux::Asset>
25
+ # client.search(:name => 'Pingu') #=> BBC::Redux::SearchResults
26
+ # client.user #=> BBC::Redux::User
27
+ #
28
+ # @example Call logout once finished to destroy your session
29
+ #
30
+ # client.logout
31
+ #
32
+ # @author Matt Haynes <matt.haynes@bbc.co.uk>
33
+ class Client
34
+
35
+ # Raised when backend HTTP API returns a 403, indicates you are either
36
+ # trying to access some content that is unavailable to you, or your token
37
+ # and session has expired.
38
+ class ForbiddenException < Exception; end
39
+
40
+ # Raised when backend HTTP API returns a 4XX or 5XX status other than
41
+ # 403, indicates an error within the HTTP API or bug in this library
42
+ class HttpException < Exception; end
43
+
44
+ # Raised when backend HTTP API returns a body that does not parse as json
45
+ class JsonParseException < Exception; end
46
+
47
+ # @!attribute [r] http
48
+ # @return [Object] http client, by default this is Typhoeus
49
+ attr_reader :http
50
+
51
+ # @!attribute [r] token
52
+ # @return [String] token for current session
53
+ attr_reader :token
54
+
55
+ # @!attribute [r] host
56
+ # @return [String] API HTTP host
57
+ attr_reader :host
58
+
59
+ # Client must be initialized with either a username and password
60
+ # combination or a token
61
+ #
62
+ # @param [Hash] options the options to create client with
63
+ # @option options [String] :username username of a redux account
64
+ # @option options [String] :password password of a redux account
65
+ # @option options [String] :token token for an existing redux session
66
+ # @option options [String] :host (https://i.bbcredux.com) api host
67
+ # @option options [Object] :http (Typhoeus) The http client, can be
68
+ # overidden but expects method .post to return an object looking like
69
+ # Typhoeus::Response (code, headers, body, etc)
70
+ def initialize(options = {})
71
+ @host = options[:host] || 'https://i.bbcredux.com'
72
+ @http = options[:http] || Typhoeus
73
+ @token = options[:token] || begin
74
+ username = options[:username]
75
+ password = options[:password]
76
+
77
+ if username && password
78
+ data = data_for(:login, {
79
+ :username => username, :password => password
80
+ })
81
+
82
+ @token = data.fetch('token')
83
+ else
84
+ raise 'Supply either :token or :username and :password options'
85
+ end
86
+ end
87
+ end
88
+
89
+ # Fetch asset object
90
+ # @param [String] identifier either disk reference or uuid
91
+ # @see BBC::Redux::Asset
92
+ # @return [BBC::Redux::Asset, nil] the asset
93
+ def asset(identifier)
94
+ if identifier =~ /^\d{19}$/
95
+ params = { :reference => identifier }
96
+ else
97
+ params = { :uuid => identifier }
98
+ end
99
+
100
+ build :asset, :using => data_for(:asset, params)
101
+ end
102
+
103
+ # Fetch available channels for this session
104
+ # @see BBC::Redux::Channel
105
+ # @return [Array<BBC::Redux::Channel>] array of channels
106
+ def channels
107
+ build :channels, :using => data_for(:channels)
108
+ end
109
+
110
+ # Fetch available channel categories for this session
111
+ # @see BBC::Redux::ChannelCategory
112
+ # @return [Array<BBC::Redux::ChannelCategory>] array of channel categories
113
+ def channel_categories
114
+ build :channel_categories, :using => data_for(:channel_categories)
115
+ end
116
+
117
+ # Logout of redux, invalidates your token. After calling this you cannot
118
+ # make any further requests with this client
119
+ # @return [nil]
120
+ def logout
121
+ data_for(:logout)
122
+ return nil
123
+ end
124
+
125
+ # Return all programmes for the schedule date, everything from 0600 for
126
+ # 24 hours afterwards. May make multiple requests to backend to retreive
127
+ # all the data
128
+ #
129
+ # @param [Date,DateTime,Time] date query this schedule date
130
+ # @param [String,Array<String>,Channel,Array<Channel>,nil] channel
131
+ # optionally limit schedule query to one or an array of channels
132
+ # @return [Array<BBC::Redux::Asset>] the list of assets broadcast on date
133
+ def schedule(date, channels = nil)
134
+ query = {
135
+ :date => date,
136
+ :channels => channels,
137
+ :offset => 0,
138
+ :limit => 100,
139
+ }
140
+
141
+ results = search(query)
142
+
143
+ assets = [ ]
144
+
145
+ while true do
146
+
147
+ assets.concat(results.assets)
148
+
149
+ if results.has_more?
150
+ next_query = results.query.merge({
151
+ :offset => results.query[:offset] + 100
152
+ })
153
+
154
+ results = self.search(next_query)
155
+ else
156
+ break
157
+ end
158
+ end
159
+
160
+ assets.sort { |a,b| b.start - a.start }
161
+ end
162
+
163
+ # Perform a search of Redux Archive
164
+ #
165
+ # @param [Hash] params your search parameters
166
+ # @option params [String] :q free text query
167
+ # @option params [String] :name query on programme name
168
+ # @option params [String,Array<String>,Channel,Array<Channel>] :channel
169
+ # query on channel, e.g. 'bbcone'. Can provide an array to search on
170
+ # multiple channels.
171
+ # @option params [Integer] :limit number of results to return. Default 10
172
+ # @option params [Integer] :offset offset of the start of results
173
+ # @option params [Date,DateTime,Time] :before only return braodcasts
174
+ # before date
175
+ # @option params [Date,DateTime,Time] :after only return braodcasts after
176
+ # date
177
+ # @option params [Date,DateTime,Time] :date everything from 0600 on given
178
+ # date for 24hrs
179
+ # @option params [Integer] :longer constraint on the duration, in seconds
180
+ # @option params [Integer] :shorter constraint on the duration, in seconds
181
+ # @option params [String] :programme_crid TV Anytime CRID
182
+ # @option params [String] :series_crid TV Anytime CRID
183
+ # @see BBC::Redux::SearchResults
184
+ # @return [BBC::Redux::SearchResults] search results
185
+ def search(params = {})
186
+
187
+ mapper = lambda do |val|
188
+ if val.class == Date || val.class == DateTime || val.class == Time
189
+ val.strftime('%Y-%m-%dT%H:%M:%S')
190
+ elsif val.class == Channel
191
+ val.name
192
+ else
193
+ val.to_s
194
+ end
195
+ end
196
+
197
+ new_params = params.map do |key, val|
198
+ if val.class == Array
199
+ [ key, val.map(&mapper) ]
200
+ else
201
+ [ key, mapper.call(val) ]
202
+ end
203
+ end
204
+
205
+ data = data_for(:search_results, Hash[new_params]).merge({
206
+ 'query' => params
207
+ })
208
+
209
+ build :search_results, :using => data
210
+ end
211
+
212
+ # Fetch user object for current session
213
+ # @see BBC::Redux::User
214
+ # @return [BBC::Redux::User, nil] the user
215
+ def user
216
+ build :user, :using => data_for(:user)
217
+ end
218
+
219
+ private
220
+
221
+ # @private
222
+ def build(type, options)
223
+ data = options.fetch(:using)
224
+
225
+ case type
226
+ when :asset
227
+ Serializers::Asset.new(Asset.new).from_hash(data)
228
+ when :channels
229
+ Serializers::Channels.new([]).from_hash(data)
230
+ when :channel_categories
231
+ Serializers::ChannelCategories.new([]).from_hash(data)
232
+ when :search_results
233
+ Serializers::SearchResults.new(SearchResults.new).from_hash(data)
234
+ when :user
235
+ Serializers::User.new(User.new).from_hash(data)
236
+ end
237
+ end
238
+
239
+ # @private
240
+ def url_for(action)
241
+ case action
242
+ when :asset
243
+ host + '/asset/details'
244
+ when :channels
245
+ host + '/asset/channel/available'
246
+ when :channel_categories
247
+ host + '/asset/channel/categories'
248
+ when :login
249
+ host + '/user/login'
250
+ when :logout
251
+ host + '/user/logout'
252
+ when :search_results
253
+ host + '/asset/search'
254
+ when :user
255
+ host + '/user/details'
256
+ end
257
+ end
258
+
259
+ # @private
260
+ def data_for(action, params = {})
261
+ url = url_for action
262
+
263
+ # Patch typhoeus / ethon's handling of array params, essentially
264
+ # turn this /?key[0]=1&key[1]=2&key[2]=3 into this
265
+ # /?key=1&key=2&key=3
266
+
267
+ arrays = params.select { |_,v| v.class == Array }
268
+
269
+ unless arrays.empty?
270
+ url += '?' unless url =~ /\?$/
271
+
272
+ arrays.each do |key, values|
273
+ url += values.map { |v| "#{key}=#{v}" }.join('&')
274
+ end
275
+ end
276
+
277
+ params = params.select { |_,v| v.class != Array }
278
+
279
+ resp = http.post(url, {
280
+ :body => params.merge(:token => token),
281
+ :followlocation => true,
282
+ })
283
+
284
+ case resp.code
285
+ when 200
286
+ JSON.parse(resp.body)
287
+ when 403
288
+ raise ForbiddenException.new("403 response for #{url}")
289
+ when 400..599
290
+ raise HttpException.new("#{resp.code} response for #{url}")
291
+ else
292
+ raise "Umm, not sure how to handle #{resp.code} for #{url}"
293
+ end
294
+
295
+ rescue JSON::ParserError => e
296
+ raise JsonParseException.new("Error parsing #{url}, #{e.message}")
297
+ end
298
+
299
+ end
300
+
301
+ end
302
+ end
@@ -0,0 +1,78 @@
1
+ module BBC
2
+ module Redux
3
+
4
+ # @private
5
+ module EndPoints
6
+
7
+ HOST = 'https://i.bbcredux.com'
8
+
9
+ # Make dvb subs media file end point
10
+ # @param id [String] asset identifier
11
+ # @param key [String] string value of a valid access key
12
+ # @param fname [String] template for the file name
13
+ # @return [String] The end point
14
+ def self.dvbsubs(id, key, fname = nil)
15
+ HOST + "/asset/media/#{id}/#{key}/dvbsubs/#{(fname || '%s.xml') % id}"
16
+ end
17
+
18
+ # Make FLV media file end point
19
+ # @param id [String] asset identifier
20
+ # @param key [String] string value of a valid access key
21
+ # @param fname [String] template for the file name
22
+ # @return [String] The end point
23
+ def self.flv(id, key, fname = nil)
24
+ HOST + "/asset/media/#{id}/#{key}/Flash_v1.0/#{(fname || '%s.flv') % id}"
25
+ end
26
+
27
+ # Make mp3 media file end point
28
+ # @param id [String] asset identifier
29
+ # @param key [String] string value of a valid access key
30
+ # @param fname [String] template for the file name
31
+ # @return [String] The end point
32
+ def self.mp3(id, key, fname = nil)
33
+ HOST + "/asset/media/#{id}/#{key}/MP3_v1.0/#{(fname || '%s.mp3') % id}"
34
+ end
35
+
36
+ # Make h264_hi media file end point
37
+ # @param id [String] asset identifier
38
+ # @param key [String] string value of a valid access key
39
+ # @param fname [String] template for the file name
40
+ # @return [String] The end point
41
+ def self.h264_hi(id, key, fname = nil)
42
+ HOST + "/asset/media/#{id}/#{key}/h264_mp4_hi_v1.1/" \
43
+ + (fname || '%s-h264lg.mp4') % id
44
+ end
45
+
46
+ # Make h264_lo media file end point
47
+ # @param id [String] asset identifier
48
+ # @param key [String] string value of a valid access key
49
+ # @param fname [String] template for the file name
50
+ # @return [String] The end point
51
+ def self.h264_lo(id, key, fname = nil)
52
+ HOST + "/asset/media/#{id}/#{key}/h264_mp4_lo_v1.0/" \
53
+ + (fname || '%s-h264sm.mp4') % id
54
+ end
55
+
56
+ # Make ts media file end point
57
+ # @param id [String] asset identifier
58
+ # @param key [String] string value of a valid access key
59
+ # @param fname [String] template for the file name
60
+ # @return [String] The end point
61
+ def self.ts(id, key, fname = nil)
62
+ HOST + "/asset/media/#{id}/#{key}/ts/#{(fname || '%s.mpegts') % id}"
63
+ end
64
+
65
+ # Make stripped ts media file end point
66
+ # @param id [String] asset identifier
67
+ # @param key [String] string value of a valid access key
68
+ # @param fname [String] template for the file name
69
+ # @return [String] The end point
70
+ def self.ts_stripped(id, key, fname = nil)
71
+ HOST + "/asset/media/#{id}/#{key}/strip/" \
72
+ + (fname || '%s-stripped.mpegts') % id
73
+ end
74
+
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,73 @@
1
+ module BBC
2
+ module Redux
3
+
4
+ # Redux API Asset Key Object
5
+ #
6
+ # Each asset is served with an associated key that is needed to access
7
+ # it's associated media. Generally these keys have a lifetime of 24 hours
8
+ #
9
+ # @example Properties of the key object
10
+ #
11
+ # key = redux_client.asset('5966413090093319525').key
12
+ #
13
+ # key.expired? #=> Boolean
14
+ # key.expires_at #=> DateTime
15
+ # key.live? #=> Boolean
16
+ # key.ttl #=> Integer
17
+ # key.value #=> String
18
+ #
19
+ # @author Matt Haynes <matt.haynes@bbc.co.uk>
20
+ class Key
21
+
22
+ # @!attribute [r] expires_at
23
+ # @return [DateTime] the access key's expiry time
24
+ attr_reader :expires_at
25
+
26
+ # @!attribute [r] value
27
+ # @return [String] the access key's value
28
+ attr_reader :value
29
+
30
+ # @param value [String] the access keys value
31
+ def initialize(value)
32
+ @value = value
33
+ @expires_at = Time.at( value.split('-')[1].to_i ).to_datetime
34
+ end
35
+
36
+ # @see Key#expired?
37
+ # @see Key#ttl
38
+ # @return [Boolean] true if ttl <= 0, false otherwise
39
+ def expired?
40
+ ttl <= 0
41
+ end
42
+
43
+ # @see Key#expired?
44
+ # @see Key#ttl
45
+ # @return [Boolean] true if ttl > 0, false otherwise
46
+ def live?
47
+ ttl > 0
48
+ end
49
+
50
+ # @return the key's value as a string
51
+ def to_s
52
+ value
53
+ end
54
+
55
+ # @see Key#expired?
56
+ # @see Key#expires_at
57
+ # @see Key#live?
58
+ # @return [Integer] key's Time To Live in seconds
59
+ def ttl
60
+ expires_at.to_time.to_i - Time.now.to_i
61
+ end
62
+
63
+ # @return [Boolean] true if other_key is a redux key with the same value
64
+ def ==(other_key)
65
+ self.class == other_key.class && self.value == other_key.value
66
+ end
67
+
68
+ alias :eql? :==
69
+
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,123 @@
1
+ require 'forwardable'
2
+ require_relative 'end_points'
3
+ require_relative 'key'
4
+
5
+ module BBC
6
+ module Redux
7
+
8
+ # Redux API Asset URL Object
9
+ #
10
+ # Each asset is available as various transcodes using the url function.
11
+ # The urls are key based and therefore generally have a lifetime of 24 hours #
12
+ #
13
+ # @example Properties of the url object
14
+ #
15
+ # url = redux_client.asset('5966413090093319525').url(:mp3)
16
+ #
17
+ # url.expired? #=> Boolean
18
+ # url.expires_at #=> DateTime
19
+ # url.live? #=> Boolean
20
+ # url.ttl #=> Integer
21
+ # url.end_point #=> String
22
+ #
23
+ # @example generate a URL with a different filename
24
+ #
25
+ # url = redux_client.asset('5966413090093319525').mp3_url
26
+ #
27
+ # url.end_point('myfile.mp3') #=> String
28
+ #
29
+ # @author Matt Haynes <matt.haynes@bbc.co.uk>
30
+ class MediaUrl
31
+
32
+ # Error raised when trying to initiate url with an unknown template type
33
+ class UnknownTemplateType < Exception; end
34
+
35
+ # Known URL templates, these are the only valid options for the
36
+ # type attribute of MediaUrl.initialize.
37
+ # @see MediaUrl#initialize
38
+ TEMPLATES = [
39
+ :dvbsubs,
40
+ :flv,
41
+ :h264_hi,
42
+ :h264_lo,
43
+ :mp3,
44
+ :ts,
45
+ :ts_stripped
46
+ ].freeze
47
+
48
+ # @!attribute [r] expires_at
49
+ # @return [DateTime] the access url's expiry time
50
+
51
+ # @!method expired?
52
+ # @see MediaUrl#expired?
53
+ # @see MediaUrl#ttl
54
+ # @return [Boolean] true if ttl <= 0, false otherwise
55
+
56
+ # @!method live?
57
+ # @see MediaUrl#expired?
58
+ # @see MediaUrl#ttl
59
+ # @return [Boolean] true if ttl > 0, false otherwise
60
+
61
+ # @!method ttl
62
+ # @see MediaUrl#expired?
63
+ # @see MediaUrl#live?
64
+ # @return [Integer] url's time to live in seconds
65
+
66
+ # @private
67
+ extend Forwardable
68
+
69
+ def_delegators :@key, :expired?, :expires_at, :live?, :ttl
70
+
71
+ # @!attribute [r] identifier
72
+ # @return [String] the url's indentifier
73
+ attr_reader :identifier
74
+
75
+ # @!attribute [r] type
76
+ # @see MediaUrl.TYPES
77
+ # @return [Symbol] the url's type
78
+ attr_reader :type
79
+
80
+ # @!attribute [r] key
81
+ # @return [BBC::Redux::Key] the url's key
82
+ attr_reader :key
83
+
84
+ # @param identifier [String] the disk reference or UUID of the asset
85
+ # @param type [Symbol] the transcode type, must be one of MediaUrl.TYPES
86
+ # @param key [BBC::Redux:Key] the key
87
+ # @raise [UnknownTranscodeType] if type parameter is unknown
88
+ # @see MediaUrl.TYPES
89
+ def initialize( identifier, type, key )
90
+
91
+ unless TEMPLATES.include?(type)
92
+ raise UnknownTemplateType.new("Unknown template type #{type}")
93
+ end
94
+
95
+ @identifier = identifier
96
+ @type = type
97
+ @key = key
98
+ end
99
+
100
+ # Generate the end point to retreive media file
101
+ #
102
+ # @param filename [String] an optional filename to specify on the end
103
+ # point. This can also contain a "%s" template that will be populated
104
+ # with the MediaUrl#identifier
105
+ # @return [String] The URL end point
106
+ def end_point(filename = nil)
107
+ EndPoints.send(type, identifier, key.value, filename)
108
+ end
109
+
110
+ alias :to_s :end_point
111
+
112
+ # @return [Boolean] true if other_url is a redux url with the same type,
113
+ # identifier and key
114
+ def ==(other_url)
115
+ self.class == other_url.class && self.end_point == other_url.end_point
116
+ end
117
+
118
+ alias :eql? :==
119
+
120
+ end
121
+
122
+ end
123
+ end
@@ -0,0 +1,82 @@
1
+ require 'virtus'
2
+ require_relative 'asset'
3
+
4
+ module BBC
5
+ module Redux
6
+
7
+ # Search results container
8
+ #
9
+ # @example Properties of search results
10
+ #
11
+ # results = redux_client.search(:name => 'Pingu')
12
+ #
13
+ # results.created_at #=> DateTime
14
+ # results.query #=> Hash
15
+ # results.query_time #=> Float
16
+ # results.assets #=> Array<BBC::Redux::Asset>
17
+ # results.total #=> Integer
18
+ # results.total_returned #=> Integer
19
+ # results.has_more? #=> Boolean
20
+ #
21
+ # @example Iterating all search results
22
+ #
23
+ # results = redux_client.search(:name => 'Pingu', :offset => 0)
24
+ #
25
+ # while true do
26
+ # results.assets.each do |asset|
27
+ # puts asset.name
28
+ # end
29
+ #
30
+ # if results.has_more?
31
+ # next_query = results.query.merge({
32
+ # :offset => results.query[:offset] + 10
33
+ # })
34
+ #
35
+ # results = redux_client.search(next_query)
36
+ # else
37
+ # break
38
+ # end
39
+ # end
40
+ #
41
+ class SearchResults
42
+
43
+ include Virtus.value_object
44
+
45
+ # @!attribute [r] created_at
46
+ # @return [DateTime] date and time results were retrieved
47
+ attribute :created_at, DateTime
48
+
49
+ # @!attribute [r] query
50
+ # @return [Hash] original parameters used in this query
51
+ attribute :query, Hash
52
+
53
+ # @attribute [r] query_time
54
+ # @return [Float] query time in seconds
55
+ attribute :query_time, Float
56
+
57
+ # @!attribute [r] assets
58
+ # @return [Array<BBC::Redux::Asset>] assets returned in this query
59
+ attribute :assets, Array[Asset]
60
+
61
+ # @!attribute [r] offset
62
+ # @return [Integer] offset of this set of results
63
+ attribute :offset, Integer
64
+
65
+ # @!attribute [r] total
66
+ # @return [Integer] total number of results
67
+ attribute :total, Integer
68
+
69
+ # @!attribute [r] total_returned
70
+ # @return [Integer] total number of results returned in this query
71
+ attribute :total_returned, Integer
72
+
73
+ # @return [Boolean] true if there are more results available than
74
+ # returned in this query
75
+ def has_more?
76
+ (offset + total_returned) < total
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+ end