botr 0.1.0
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.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +31 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +4 -0
- data/LICENSE.txt +20 -0
- data/README.md +278 -0
- data/Rakefile +43 -0
- data/botr.gemspec +24 -0
- data/botr.watchr +3 -0
- data/certs/cacert.pem +3554 -0
- data/lib/botr.rb +34 -0
- data/lib/botr/api/api.rb +83 -0
- data/lib/botr/api/authentication.rb +24 -0
- data/lib/botr/channels/channel.rb +137 -0
- data/lib/botr/channels/channel_thumbnail.rb +88 -0
- data/lib/botr/channels/channel_video.rb +130 -0
- data/lib/botr/channels/channel_view.rb +88 -0
- data/lib/botr/common/logger.rb +31 -0
- data/lib/botr/configuration.rb +18 -0
- data/lib/botr/http/http.rb +88 -0
- data/lib/botr/http/http_backend.rb +66 -0
- data/lib/botr/http/http_response.rb +48 -0
- data/lib/botr/http/multipart.rb +84 -0
- data/lib/botr/http/uri_ext.rb +28 -0
- data/lib/botr/object.rb +20 -0
- data/lib/botr/players/player.rb +149 -0
- data/lib/botr/players/player_view.rb +88 -0
- data/lib/botr/version.rb +3 -0
- data/lib/botr/videos/video.rb +187 -0
- data/lib/botr/videos/video_caption.rb +154 -0
- data/lib/botr/videos/video_conversion.rb +114 -0
- data/lib/botr/videos/video_engagement.rb +51 -0
- data/lib/botr/videos/video_tag.rb +52 -0
- data/lib/botr/videos/video_thumbnail.rb +87 -0
- data/lib/botr/videos/video_view.rb +89 -0
- data/spec/authentication_spec.rb +31 -0
- data/spec/botr_spec.rb +8 -0
- data/spec/http_backend_spec.rb +93 -0
- data/spec/http_response_spec.rb +48 -0
- data/spec/multipart_spec.rb +35 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/test.txt +1 -0
- metadata +150 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
module BOTR
|
2
|
+
|
3
|
+
class ChannelView < BOTR::Object
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
attr_reader :last_status
|
8
|
+
|
9
|
+
def show(key, **options)
|
10
|
+
json = get_request(options.merge(:method => 'show',
|
11
|
+
:channel_key => key))
|
12
|
+
res = JSON.parse(json.body)
|
13
|
+
|
14
|
+
if json.status == 200
|
15
|
+
params = process_show_response(res)
|
16
|
+
else
|
17
|
+
raise "HTTP Error #{json.status}: #{json.body}"
|
18
|
+
end
|
19
|
+
|
20
|
+
return new(params)
|
21
|
+
end
|
22
|
+
|
23
|
+
alias :find :show
|
24
|
+
|
25
|
+
def list(**options)
|
26
|
+
json = get_request(options.merge(:method => 'list'))
|
27
|
+
res = JSON.parse(json.body)
|
28
|
+
|
29
|
+
if json.status == 200
|
30
|
+
results = process_list_response(res)
|
31
|
+
else
|
32
|
+
raise "HTTP Error #{json.status}: #{json.body}"
|
33
|
+
end
|
34
|
+
|
35
|
+
return results
|
36
|
+
end
|
37
|
+
|
38
|
+
def all
|
39
|
+
list({})
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def process_show_response(body)
|
45
|
+
@last_status = body["status"]
|
46
|
+
|
47
|
+
return body["channel"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def process_list_response(body)
|
51
|
+
@last_status = body["status"]
|
52
|
+
res = []
|
53
|
+
|
54
|
+
if body["channels"]
|
55
|
+
body["channels"].each do |channel|
|
56
|
+
res << new(channel)
|
57
|
+
end
|
58
|
+
elsif body["days"]
|
59
|
+
body["days"].each do |day|
|
60
|
+
res << new(day)
|
61
|
+
end
|
62
|
+
elsif body["years"]
|
63
|
+
body["years"].each do |year|
|
64
|
+
res << new(year)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
res << new(body)
|
68
|
+
end
|
69
|
+
|
70
|
+
return res
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_reader :last_status, :key, :days, :years, :views, :title, :date,
|
76
|
+
:timestamp, :number, :year, :number, :total
|
77
|
+
|
78
|
+
def initialize(params = {})
|
79
|
+
params.each do |key, val|
|
80
|
+
param = "@#{key.to_s}"
|
81
|
+
next unless methods.include? key.to_sym
|
82
|
+
instance_variable_set(param, val)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module BOTR
|
2
|
+
|
3
|
+
module Logger
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
def logger=(log)
|
10
|
+
@logger = log
|
11
|
+
end
|
12
|
+
|
13
|
+
def logger
|
14
|
+
@logger ||= self.class.logger
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
def logger=(log)
|
20
|
+
@@logger = log
|
21
|
+
end
|
22
|
+
|
23
|
+
def logger
|
24
|
+
@@logger ||= nil
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module BOTR
|
2
|
+
|
3
|
+
class << self
|
4
|
+
attr_accessor :configuration
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.configure
|
8
|
+
self.configuration ||= Configuration.new
|
9
|
+
yield(configuration)
|
10
|
+
end
|
11
|
+
|
12
|
+
class Configuration
|
13
|
+
|
14
|
+
attr_accessor :protocol, :server, :api_key, :secret_key
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module BOTR
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
|
5
|
+
def client
|
6
|
+
@client = BOTR::HTTPBackend.new unless @client
|
7
|
+
@client
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_request(options = {})
|
11
|
+
params = options.dup
|
12
|
+
|
13
|
+
http = params.delete(:client) || client
|
14
|
+
method = params.delete(:method)
|
15
|
+
url = params.delete(:api_url) || api_url(method)
|
16
|
+
format = params.delete(:api_format) || api_format
|
17
|
+
key = params.delete(:api_key) || api_key
|
18
|
+
timestamp = params.delete(:api_timestamp) || api_timestamp
|
19
|
+
nonce = params.delete(:api_nonce) || api_nonce
|
20
|
+
secret = params.delete(:api_secret_key)|| api_secret_key
|
21
|
+
|
22
|
+
params = params.merge(:api_format => format,
|
23
|
+
:api_key => key,
|
24
|
+
:api_timestamp => timestamp,
|
25
|
+
:api_nonce => nonce)
|
26
|
+
|
27
|
+
http.get(url, params.merge(:api_signature => self.signature(params)))
|
28
|
+
end
|
29
|
+
|
30
|
+
def post_request(options = {}, data_path = nil)
|
31
|
+
params = options.dup
|
32
|
+
|
33
|
+
http = params.delete(:client) || client
|
34
|
+
url = params.delete(:api_url) || upload_url
|
35
|
+
format = params.delete(:api_format) || api_format
|
36
|
+
key = params.delete(:upload_key) || upload_key
|
37
|
+
token = params.delete(:upload_token) || upload_token
|
38
|
+
|
39
|
+
params = params.merge(:api_format => format,
|
40
|
+
:key => key,
|
41
|
+
:token => token)
|
42
|
+
|
43
|
+
http.post(url, params, data_path)
|
44
|
+
end
|
45
|
+
|
46
|
+
def put_request(options = {})
|
47
|
+
params = options.dup
|
48
|
+
|
49
|
+
http = params.delete(:client) || client
|
50
|
+
method = 'update'
|
51
|
+
url = params.delete(:api_url) || api_url(method)
|
52
|
+
format = params.delete(:api_format) || api_format
|
53
|
+
key = params.delete(:api_key) || api_key
|
54
|
+
timestamp = params.delete(:api_timestamp) || api_timestamp
|
55
|
+
nonce = params.delete(:api_nonce) || api_nonce
|
56
|
+
secret = params.delete(:api_secret_key)|| api_secret_key
|
57
|
+
|
58
|
+
params = params.merge(:api_format => format,
|
59
|
+
:api_key => key,
|
60
|
+
:api_timestamp => timestamp,
|
61
|
+
:api_nonce => nonce)
|
62
|
+
|
63
|
+
http.post(url, params.merge(:api_signature => self.signature(params)))
|
64
|
+
end
|
65
|
+
|
66
|
+
def delete_request(options = {})
|
67
|
+
params = options.dup
|
68
|
+
|
69
|
+
http = params.delete(:client) || client
|
70
|
+
method = 'delete'
|
71
|
+
url = params.delete(:api_url) || api_url(method)
|
72
|
+
format = params.delete(:api_format) || api_format
|
73
|
+
key = params.delete(:api_key) || api_key
|
74
|
+
timestamp = params.delete(:api_timestamp) || api_timestamp
|
75
|
+
nonce = params.delete(:api_nonce) || api_nonce
|
76
|
+
secret = params.delete(:api_secret_key)|| api_secret_key
|
77
|
+
|
78
|
+
params = params.merge(:api_format => format,
|
79
|
+
:api_key => key,
|
80
|
+
:api_timestamp => timestamp,
|
81
|
+
:api_nonce => nonce)
|
82
|
+
|
83
|
+
http.post(url, params.merge(:api_signature => self.signature(params)))
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module BOTR
|
4
|
+
|
5
|
+
class HTTPBackend
|
6
|
+
|
7
|
+
# GET request.
|
8
|
+
def get(path, params = {})
|
9
|
+
uri = URI(path)
|
10
|
+
uri.query = URI.encode_www_form(params)
|
11
|
+
|
12
|
+
res = Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
|
13
|
+
http.request_get uri
|
14
|
+
end
|
15
|
+
|
16
|
+
respond(res)
|
17
|
+
end
|
18
|
+
|
19
|
+
# POST request with optional multipart/form-data.
|
20
|
+
def post(path, params = {}, data_path = "")
|
21
|
+
uri = URI(path)
|
22
|
+
uri.query = URI.encode_www_form(params)
|
23
|
+
|
24
|
+
res = Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
|
25
|
+
req = Net::HTTP::Post.new(uri.request_uri)
|
26
|
+
|
27
|
+
if data_path.empty?
|
28
|
+
req.set_form_data(params)
|
29
|
+
else
|
30
|
+
boundary = rand(1000000).to_s
|
31
|
+
|
32
|
+
req.body_stream = BOTR::Multipart.new(data_path, boundary)
|
33
|
+
req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
34
|
+
req["Content-Length"] = req.body_stream.size.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
http.request req
|
38
|
+
end
|
39
|
+
|
40
|
+
respond(res)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Map Net::HTTPResponses to internal ones.
|
46
|
+
def respond(resp)
|
47
|
+
case resp
|
48
|
+
when Net::HTTPOK
|
49
|
+
BOTR::OKResponse.new(resp.body)
|
50
|
+
when Net::HTTPBadRequest
|
51
|
+
BOTR::BadRequestResponse.new(resp.body)
|
52
|
+
when Net::HTTPUnauthorized
|
53
|
+
BOTR::UnauthorizedResponse.new(resp.body)
|
54
|
+
when Net::HTTPForbidden
|
55
|
+
BOTR::ForbiddenResponse.new(resp.body)
|
56
|
+
when Net::HTTPNotFound
|
57
|
+
BOTR::NotFoundResponse.new(resp.body)
|
58
|
+
when Net::HTTPMethodNotAllowed
|
59
|
+
BOTR::NotAllowedResponse.new(resp.body)
|
60
|
+
else
|
61
|
+
BOTR::HTTPResponse.new(resp.code, resp.body)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module BOTR
|
2
|
+
|
3
|
+
class HTTPResponse
|
4
|
+
attr_accessor :status, :body
|
5
|
+
|
6
|
+
def initialize(status, body = "")
|
7
|
+
@status = status
|
8
|
+
@body = body
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class OKResponse < HTTPResponse
|
13
|
+
def initialize(body = "")
|
14
|
+
super(200, body)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class BadRequestResponse < HTTPResponse
|
19
|
+
def initialize(body = "")
|
20
|
+
super(400, body)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class UnauthorizedResponse < HTTPResponse
|
25
|
+
def initialize(body = "")
|
26
|
+
super(401, body)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class ForbiddenResponse < HTTPResponse
|
31
|
+
def initialize(body = "")
|
32
|
+
super(403, body)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class NotFoundResponse < HTTPResponse
|
37
|
+
def initialize(body = "")
|
38
|
+
super(404, body)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class NotAllowedResponse < HTTPResponse
|
43
|
+
def initialize(body = "")
|
44
|
+
super(405, body)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'mime/types'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module BOTR
|
5
|
+
|
6
|
+
class Multipart
|
7
|
+
|
8
|
+
def initialize(data_path = "", boundary = nil)
|
9
|
+
@data_path = data_path
|
10
|
+
@boundary = boundary
|
11
|
+
|
12
|
+
build_stream
|
13
|
+
end
|
14
|
+
|
15
|
+
def close
|
16
|
+
@stream.close! unless @stream.closed?
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(*args)
|
20
|
+
@stream.send(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def respond_to?(meth)
|
24
|
+
@stream.respond_to?(meth) || super(meth)
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def boundary
|
31
|
+
@boundary ||= rand(1000000).to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def mime_type_for(path)
|
35
|
+
mime = MIME::Types.type_for(path).first
|
36
|
+
mime ? mime.content_type : 'text/plain'
|
37
|
+
end
|
38
|
+
|
39
|
+
# Build a multipart/form-data body as per RFC 2388.
|
40
|
+
def build_stream
|
41
|
+
# Form input field name for the file path must be set to file.
|
42
|
+
# Any other field name will not be picked up by the upload server.
|
43
|
+
name = 'file'
|
44
|
+
content_length = File.size(@data_path)
|
45
|
+
content_type = mime_type_for(@data_path)
|
46
|
+
|
47
|
+
@stream = Tempfile.new("BitsOnTheRun.Upload.")
|
48
|
+
@stream.binmode
|
49
|
+
|
50
|
+
write_stream_head(name, content_type, content_length)
|
51
|
+
write_stream_body
|
52
|
+
write_stream_tail
|
53
|
+
|
54
|
+
@stream.seek(0)
|
55
|
+
end
|
56
|
+
|
57
|
+
def write_stream_head(name, content_type, content_length)
|
58
|
+
transfer_encoding = "binary"
|
59
|
+
content_disposition = "form-data"
|
60
|
+
|
61
|
+
@stream.write "--#{boundary}\r\n"
|
62
|
+
@stream.write "Content-Disposition: #{content_disposition}; name=\"#{name}\"; filename=\"#{File.basename(@data_path)}\"\r\n"
|
63
|
+
@stream.write "Content-Length: #{content_length}\r\n"
|
64
|
+
@stream.write "Content-Type: #{content_type}\r\n"
|
65
|
+
@stream.write "Content-Transfer-Encoding: #{transfer_encoding}\r\n"
|
66
|
+
@stream.write "\r\n"
|
67
|
+
end
|
68
|
+
|
69
|
+
def write_stream_body
|
70
|
+
f = File.new(@data_path)
|
71
|
+
while data = f.read(8124)
|
72
|
+
@stream.write(data)
|
73
|
+
end
|
74
|
+
ensure
|
75
|
+
f.close unless f.closed?
|
76
|
+
end
|
77
|
+
|
78
|
+
def write_stream_tail
|
79
|
+
@stream.write "\r\n"
|
80
|
+
@stream.write "--#{boundary}--\r\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module URI
|
2
|
+
|
3
|
+
# FIX: For some reason, the Bits on the Run API doesn't support the encoding
|
4
|
+
# of spaces to "+". As such, (ASCII space) will encode to "%20".
|
5
|
+
TBLENCWWWCOMP__ = TBLENCWWWCOMP_.dup
|
6
|
+
TBLENCWWWCOMP__[' '] = '%20'
|
7
|
+
TBLENCWWWCOMP__.freeze
|
8
|
+
|
9
|
+
def self.encode_www_form_component(str, enc=nil)
|
10
|
+
str = str.to_s.dup
|
11
|
+
|
12
|
+
if str.encoding != Encoding::ASCII_8BIT
|
13
|
+
if enc && enc != Encoding::ASCII_8BIT
|
14
|
+
str.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
15
|
+
str.encode!(enc, fallback: ->(x){"&#{x.ord};"})
|
16
|
+
end
|
17
|
+
str.force_encoding(Encoding::ASCII_8BIT)
|
18
|
+
end
|
19
|
+
str.gsub!(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP__)
|
20
|
+
str.force_encoding(Encoding::US_ASCII)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.decode_www_form_component(str, enc=Encoding::UTF_8)
|
24
|
+
raise ArgumentError, "invalid %-encoding (#{str})" unless /\A[^%]*(?:%\h\h[^%]*)*\z/ =~ str
|
25
|
+
str.b.gsub(/\+|%\h\h/, TBLENCWWWCOMP__).force_encoding(enc)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|