bbc_redux 0.4.0.pre

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,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