yt 0.12.2 → 0.13.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +10 -6
- data/YOUTUBE_IT.md +867 -0
- data/lib/yt/actions/delete_all.rb +1 -1
- data/lib/yt/actions/insert.rb +11 -3
- data/lib/yt/actions/list.rb +18 -10
- data/lib/yt/actions/modify.rb +11 -3
- data/lib/yt/associations/has_authentication.rb +1 -1
- data/lib/yt/collections/annotations.rb +4 -2
- data/lib/yt/collections/authentications.rb +4 -2
- data/lib/yt/collections/ownerships.rb +1 -1
- data/lib/yt/collections/reports.rb +13 -1
- data/lib/yt/collections/resumable_sessions.rb +4 -2
- data/lib/yt/collections/user_infos.rb +3 -1
- data/lib/yt/models/account.rb +9 -1
- data/lib/yt/models/channel.rb +0 -4
- data/lib/yt/models/resource.rb +1 -1
- data/lib/yt/models/resumable_session.rb +1 -1
- data/lib/yt/request.rb +267 -0
- data/lib/yt/version.rb +1 -1
- data/spec/collections/reports_spec.rb +30 -0
- data/spec/models/request_spec.rb +1 -23
- data/spec/requests/as_account/account_spec.rb +8 -1
- data/spec/requests/as_account/authentications_spec.rb +1 -1
- data/spec/requests/as_account/channel_spec.rb +1 -8
- metadata +6 -4
- data/TODO.md +0 -58
- data/lib/yt/models/request.rb +0 -223
data/lib/yt/actions/insert.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
require 'yt/
|
1
|
+
require 'yt/request'
|
2
2
|
require 'yt/actions/base'
|
3
|
+
require 'yt/config'
|
3
4
|
|
4
5
|
module Yt
|
5
6
|
module Actions
|
@@ -9,20 +10,27 @@ module Yt
|
|
9
10
|
private
|
10
11
|
|
11
12
|
def do_insert(extra_insert_params = {})
|
12
|
-
|
13
|
-
response = request.run
|
13
|
+
response = insert_request(extra_insert_params).run
|
14
14
|
@items = []
|
15
15
|
new_item extract_data_from(response)
|
16
16
|
end
|
17
17
|
|
18
|
+
def insert_request(params = {})
|
19
|
+
Yt::Request.new(insert_params.deep_merge params).tap do |request|
|
20
|
+
print "#{request.as_curl}\n" if Yt.configuration.developing?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
18
24
|
def insert_params
|
19
25
|
path = "/youtube/v3/#{self.class.to_s.demodulize.camelize :lower}"
|
20
26
|
|
21
27
|
{}.tap do |params|
|
22
28
|
params[:path] = path
|
29
|
+
params[:host] = 'www.googleapis.com'
|
23
30
|
params[:method] = :post
|
24
31
|
params[:auth] = @auth
|
25
32
|
params[:expected_response] = Net::HTTPOK
|
33
|
+
params[:api_key] = Yt.configuration.api_key if Yt.configuration.api_key
|
26
34
|
end
|
27
35
|
end
|
28
36
|
|
data/lib/yt/actions/list.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
require 'yt/
|
1
|
+
require 'yt/request'
|
2
2
|
require 'yt/models/iterator'
|
3
3
|
require 'yt/errors/no_items'
|
4
|
+
require 'yt/config'
|
4
5
|
|
5
6
|
module Yt
|
6
7
|
module Actions
|
@@ -9,7 +10,7 @@ module Yt
|
|
9
10
|
:size, to: :list
|
10
11
|
|
11
12
|
def first!
|
12
|
-
first.tap{|item| raise Errors::NoItems,
|
13
|
+
first.tap{|item| raise Errors::NoItems, error_message unless item}
|
13
14
|
end
|
14
15
|
|
15
16
|
private
|
@@ -38,7 +39,7 @@ module Yt
|
|
38
39
|
# iterate through all the pages, which can results in many requests.
|
39
40
|
# To avoid this, +total_results+ is provided as a good size estimation.
|
40
41
|
def total_results
|
41
|
-
response =
|
42
|
+
response = list_request(list_params).run
|
42
43
|
total_results = response.body.fetch('pageInfo', {})['totalResults']
|
43
44
|
total_results ||= response.body.fetch(items_key, []).size
|
44
45
|
end
|
@@ -87,18 +88,23 @@ module Yt
|
|
87
88
|
end
|
88
89
|
|
89
90
|
def fetch_page(params = {})
|
90
|
-
|
91
|
-
token =
|
92
|
-
items =
|
91
|
+
@last_response = list_request(params).run
|
92
|
+
token = @last_response.body['nextPageToken']
|
93
|
+
items = @last_response.body.fetch items_key, []
|
93
94
|
{items: items, token: token}
|
94
95
|
end
|
95
96
|
|
96
|
-
def
|
97
|
-
@
|
97
|
+
def list_request(params = {})
|
98
|
+
@list_request = Yt::Request.new(params).tap do |request|
|
99
|
+
print "#{request.as_curl}\n" if Yt.configuration.developing?
|
100
|
+
end
|
98
101
|
end
|
99
102
|
|
100
|
-
def
|
101
|
-
|
103
|
+
def error_message
|
104
|
+
{}.tap do |message|
|
105
|
+
message[:request_curl] = @list_request.as_curl
|
106
|
+
message[:response_body] = JSON(@last_response.body)
|
107
|
+
end.to_json if @list_request && @last_response
|
102
108
|
end
|
103
109
|
|
104
110
|
def list_params
|
@@ -106,9 +112,11 @@ module Yt
|
|
106
112
|
|
107
113
|
{}.tap do |params|
|
108
114
|
params[:method] = :get
|
115
|
+
params[:host] = 'www.googleapis.com'
|
109
116
|
params[:auth] = @auth
|
110
117
|
params[:path] = path
|
111
118
|
params[:exptected_response] = Net::HTTPOK
|
119
|
+
params[:api_key] = Yt.configuration.api_key if Yt.configuration.api_key
|
112
120
|
end
|
113
121
|
end
|
114
122
|
|
data/lib/yt/actions/modify.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
require 'yt/
|
1
|
+
require 'yt/request'
|
2
2
|
require 'yt/actions/base'
|
3
|
+
require 'yt/config'
|
3
4
|
|
4
5
|
module Yt
|
5
6
|
module Actions
|
@@ -10,18 +11,25 @@ module Yt
|
|
10
11
|
private
|
11
12
|
|
12
13
|
def do_modify(params = {})
|
13
|
-
|
14
|
-
response = request.run
|
14
|
+
response = modify_request(params).run
|
15
15
|
yield response.body if block_given?
|
16
16
|
end
|
17
17
|
|
18
|
+
def modify_request(params = {})
|
19
|
+
Yt::Request.new(params).tap do |request|
|
20
|
+
print "#{request.as_curl}\n" if Yt.configuration.developing?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
18
24
|
def modify_params
|
19
25
|
path = "/youtube/v3/#{self.class.to_s.demodulize.pluralize.camelize :lower}"
|
20
26
|
|
21
27
|
{}.tap do |params|
|
22
28
|
params[:path] = path
|
23
29
|
params[:auth] = @auth
|
30
|
+
params[:host] = 'www.googleapis.com'
|
24
31
|
params[:expected_response] = Net::HTTPNoContent
|
32
|
+
params[:api_key] = Yt.configuration.api_key if Yt.configuration.api_key
|
25
33
|
end
|
26
34
|
end
|
27
35
|
end
|
@@ -51,7 +51,7 @@ module Yt
|
|
51
51
|
|
52
52
|
# Obtains a new access token.
|
53
53
|
# Returns true if the new access token is different from the previous one
|
54
|
-
def
|
54
|
+
def refreshed_access_token?
|
55
55
|
old_access_token = authentication.access_token
|
56
56
|
@authentication = @access_token = @refreshed_authentications = nil
|
57
57
|
old_access_token != authentication.access_token
|
@@ -17,7 +17,7 @@ module Yt
|
|
17
17
|
# a video, so we use an "old-style" URL that YouTube still maintains.
|
18
18
|
def list_params
|
19
19
|
super.tap do |params|
|
20
|
-
params[:
|
20
|
+
params[:response_format] = :xml
|
21
21
|
params[:host] = 'www.youtube.com'
|
22
22
|
params[:path] = '/annotations_invideo'
|
23
23
|
params[:params] = {video_id: @parent.id}
|
@@ -30,7 +30,9 @@ module Yt
|
|
30
30
|
# @note Annotations overwrites +next_page+ since the list of annotations
|
31
31
|
# is not paginated API-style, but in its own custom way.
|
32
32
|
def next_page
|
33
|
-
request = Yt::Request.new
|
33
|
+
request = Yt::Request.new(list_params).tap do |request|
|
34
|
+
print "#{request.as_curl}\n" if Yt.configuration.developing?
|
35
|
+
end
|
34
36
|
response = request.run
|
35
37
|
@page_token = nil
|
36
38
|
|
@@ -17,7 +17,7 @@ module Yt
|
|
17
17
|
super.tap do |params|
|
18
18
|
params[:host] = 'accounts.google.com'
|
19
19
|
params[:path] = '/o/oauth2/token'
|
20
|
-
params[:
|
20
|
+
params[:request_format] = :form
|
21
21
|
params[:method] = :post
|
22
22
|
params[:auth] = nil
|
23
23
|
params[:body] = auth_params
|
@@ -30,7 +30,9 @@ module Yt
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def next_page
|
33
|
-
request = Yt::Request.new
|
33
|
+
request = Yt::Request.new(list_params).tap do |request|
|
34
|
+
print "#{request.as_curl}\n" if Yt.configuration.developing?
|
35
|
+
end
|
34
36
|
Array.wrap request.run.body
|
35
37
|
rescue Yt::Error => error
|
36
38
|
expected?(error) ? [] : raise(error)
|
@@ -5,9 +5,17 @@ module Yt
|
|
5
5
|
class Reports < Base
|
6
6
|
attr_writer :metric
|
7
7
|
|
8
|
-
def within(days_range)
|
8
|
+
def within(days_range, try_again = true)
|
9
9
|
@days_range = days_range
|
10
10
|
Hash[*flat_map{|daily_value| daily_value}]
|
11
|
+
# NOTE: Once in a while, YouTube responds with 400 Error and the message
|
12
|
+
# "Invalid query. Query did not conform to the expectations."; in this
|
13
|
+
# case running the same query after one second fixes the issue. This is
|
14
|
+
# not documented by YouTube and hardly testable, but trying again the
|
15
|
+
# same query is a workaround that works and can hardly cause any damage.
|
16
|
+
# Similarly, once in while YouTube responds with a random 503 error.
|
17
|
+
rescue Yt::Error => e
|
18
|
+
try_again && rescue?(e) ? sleep(3) && within(days_range, false) : raise
|
11
19
|
end
|
12
20
|
|
13
21
|
private
|
@@ -47,6 +55,10 @@ module Yt
|
|
47
55
|
def items_key
|
48
56
|
'rows'
|
49
57
|
end
|
58
|
+
|
59
|
+
def rescue?(error)
|
60
|
+
'badRequest'.in?(error.reasons) && error.to_s =~ /did not conform/
|
61
|
+
end
|
50
62
|
end
|
51
63
|
end
|
52
64
|
end
|
@@ -18,12 +18,14 @@ module Yt
|
|
18
18
|
# @option options [String] :title The video’s title.
|
19
19
|
# @option options [String] :description The video’s description.
|
20
20
|
# @option options [Array<String>] :title The video’s tags.
|
21
|
+
# @option options [Integer] :category_id The video’s category ID.
|
21
22
|
# @option options [String] :privacy_status The video’s privacy status.
|
22
23
|
def insert(content_length, options = {})
|
23
24
|
@headers = headers_for content_length
|
24
25
|
body = {}
|
25
26
|
|
26
|
-
snippet = options.slice :title, :description, :tags
|
27
|
+
snippet = options.slice :title, :description, :tags, :category_id
|
28
|
+
snippet[:categoryId] = snippet.delete(:category_id) if snippet[:category_id]
|
27
29
|
body[:snippet] = snippet if snippet.any?
|
28
30
|
|
29
31
|
status = options[:privacy_status]
|
@@ -40,7 +42,7 @@ module Yt
|
|
40
42
|
|
41
43
|
def insert_params
|
42
44
|
super.tap do |params|
|
43
|
-
params[:
|
45
|
+
params[:response_format] = nil
|
44
46
|
params[:path] = '/upload/youtube/v3/videos'
|
45
47
|
params[:params] = {part: 'snippet,status', uploadType: 'resumable'}
|
46
48
|
end
|
@@ -22,7 +22,9 @@ module Yt
|
|
22
22
|
# so @page_token has to be explcitly set to nil, and the result wrapped
|
23
23
|
# in an Array.
|
24
24
|
def next_page
|
25
|
-
request = Yt::Request.new
|
25
|
+
request = Yt::Request.new(list_params).tap do |request|
|
26
|
+
print "#{request.as_curl}\n" if Yt.configuration.developing?
|
27
|
+
end
|
26
28
|
response = request.run
|
27
29
|
@page_token = nil
|
28
30
|
|
data/lib/yt/models/account.rb
CHANGED
@@ -9,7 +9,7 @@ module Yt
|
|
9
9
|
# @!attribute [r] channel
|
10
10
|
# @return [Yt::Models::Channel] the account’s channel.
|
11
11
|
has_one :channel
|
12
|
-
delegate :playlists, :
|
12
|
+
delegate :playlists, :delete_playlists,
|
13
13
|
:subscribed_channels, to: :channel
|
14
14
|
|
15
15
|
# @!attribute [r] user_info
|
@@ -42,6 +42,10 @@ module Yt
|
|
42
42
|
# by the account.
|
43
43
|
has_many :content_owners
|
44
44
|
|
45
|
+
# @!attribute [r] playlists
|
46
|
+
# @return [Yt::Collections::Playlists] the account’s playlists.
|
47
|
+
has_many :playlists
|
48
|
+
|
45
49
|
# Uploads a video
|
46
50
|
# @param [String] path_or_url the video to upload. Can either be the
|
47
51
|
# path of a local file or the URL of a remote file.
|
@@ -57,6 +61,10 @@ module Yt
|
|
57
61
|
session.upload_video file
|
58
62
|
end
|
59
63
|
|
64
|
+
def create_playlist(params = {})
|
65
|
+
playlists.insert params
|
66
|
+
end
|
67
|
+
|
60
68
|
# @private
|
61
69
|
# Tells `has_many :videos` that account.videos should return all the
|
62
70
|
# videos *owned by* the account (public, private, unlisted).
|
data/lib/yt/models/channel.rb
CHANGED
data/lib/yt/models/resource.rb
CHANGED
@@ -34,7 +34,7 @@ module Yt
|
|
34
34
|
# To be sure to include both cases, HTTPSuccess is used
|
35
35
|
def update_params
|
36
36
|
super.tap do |params|
|
37
|
-
params[:
|
37
|
+
params[:request_format] = :file
|
38
38
|
params[:host] = @uri.host
|
39
39
|
params[:path] = @uri.path
|
40
40
|
params[:expected_response] = Net::HTTPSuccess
|
data/lib/yt/request.rb
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'net/http' # for Net::HTTP.start
|
2
|
+
require 'uri' # for URI.json
|
3
|
+
require 'json' # for JSON.parse
|
4
|
+
require 'active_support' # does not load anything by default but is required
|
5
|
+
require 'active_support/core_ext' # for Hash.from_xml, Hash.to_param
|
6
|
+
|
7
|
+
require 'yt/errors/forbidden'
|
8
|
+
require 'yt/errors/missing_auth'
|
9
|
+
require 'yt/errors/request_error'
|
10
|
+
require 'yt/errors/server_error'
|
11
|
+
require 'yt/errors/unauthorized'
|
12
|
+
|
13
|
+
module Yt
|
14
|
+
# A wrapper around Net::HTTP to send HTTP requests to any web API and
|
15
|
+
# return their result or raise an error if the result is unexpected.
|
16
|
+
# The basic way to use Request is by calling +run+ on an instance.
|
17
|
+
# @example List the most popular videos on YouTube.
|
18
|
+
# host = ''www.googleapis.com'
|
19
|
+
# path = '/youtube/v3/videos'
|
20
|
+
# params = {chart: 'mostPopular', key: ENV['API_KEY'], part: 'snippet'}
|
21
|
+
# response = Yt::Request.new(path: path, params: params).run
|
22
|
+
# response.body['items'].map{|video| video['snippet']['title']}
|
23
|
+
#
|
24
|
+
class Request
|
25
|
+
# Initializes a Request object.
|
26
|
+
# @param [Hash] options the options for the request.
|
27
|
+
# @option options [String, Symbol] :method (:get) The HTTP method to use.
|
28
|
+
# @option options [Class] :expected_response (Net::HTTPSuccess) The class
|
29
|
+
# of response that the request should obtain when run.
|
30
|
+
# @option options [String, Symbol] :response_format (:json) The expected
|
31
|
+
# format of the response body. If passed, the response body will be
|
32
|
+
# parsed according to the format before being returned.
|
33
|
+
# @option options [String] :host The host component of the request URI.
|
34
|
+
# @option options [String] :path The path component of the request URI.
|
35
|
+
# @option options [Hash] :params ({}) The params to use as the query
|
36
|
+
# component of the request URI, for instance the Hash {a: 1, b: 2}
|
37
|
+
# corresponds to the query parameters "a=1&b=2".
|
38
|
+
# @option options [Hash] :camelize_params (true) whether to transform
|
39
|
+
# each key of params into a camel-case symbol before sending the
|
40
|
+
# request. For instance, if set to true, the params {aBc: 1, d_e: 2,
|
41
|
+
# 'f' => 3} would be sent as {aBc: 1, dE: 2, f: 3}.
|
42
|
+
# @option options [Hash] :request_format (:json) The format of the
|
43
|
+
# requesty body. If a request body is passed, it will be parsed
|
44
|
+
# according to this format before sending it in the request.
|
45
|
+
# @option options [#size] :body The body component of the request.
|
46
|
+
# @option options [Hash] :headers ({}) The headers component of the
|
47
|
+
# request.
|
48
|
+
# @option options [#access_token, #refreshed_access_token?] :auth The
|
49
|
+
# authentication object. If set, must respond to +access_token+ and
|
50
|
+
# return the OAuth token to make an authenticated request, and must
|
51
|
+
# respond to +refreshed_access_token?+ and return whether the access
|
52
|
+
# token can be refreshed if expired.
|
53
|
+
def initialize(options = {})
|
54
|
+
@method = options.fetch :method, :get
|
55
|
+
@expected_response = options.fetch :expected_response, Net::HTTPSuccess
|
56
|
+
@response_format = options.fetch :response_format, :json
|
57
|
+
@host = options[:host]
|
58
|
+
@path = options[:path]
|
59
|
+
@params = options.fetch :params, {}
|
60
|
+
# Note: This is to be invoked by auth-only YouTube APIs.
|
61
|
+
@params[:key] = options[:api_key] if options[:api_key]
|
62
|
+
# Note: This is to be invoked by all YouTube API except Annotations,
|
63
|
+
# Analyitics and Uploads
|
64
|
+
camelize_keys! @params if options.fetch(:camelize_params, true)
|
65
|
+
@request_format = options.fetch :request_format, :json
|
66
|
+
@body = options[:body]
|
67
|
+
@headers = options.fetch :headers, {}
|
68
|
+
@auth = options[:auth]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Sends the request and returns the response.
|
72
|
+
# If the request fails once for a temporary server error or an expired
|
73
|
+
# token, tries the request again before eventually raising an error.
|
74
|
+
# @return [Net::HTTPResponse] if the request succeeds and matches the
|
75
|
+
# expectations, the response with the body appropriately parsed.
|
76
|
+
# @raise [Yt::RequestError] if the request fails or the response does
|
77
|
+
# not match the expectations.
|
78
|
+
def run
|
79
|
+
if matches_expectations?
|
80
|
+
response.tap{parse_response!}
|
81
|
+
elsif run_again?
|
82
|
+
run
|
83
|
+
else
|
84
|
+
raise response_error, error_message.to_json
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the +cURL+ version of the request, useful to re-run the request
|
89
|
+
# in a shell terminal.
|
90
|
+
# @return [String] the +cURL+ version of the request.
|
91
|
+
def as_curl
|
92
|
+
'curl'.tap do |curl|
|
93
|
+
curl << " -X #{http_request.method}"
|
94
|
+
http_request.each_header{|k, v| curl << %Q{ -H "#{k}: #{v}"}}
|
95
|
+
curl << %Q{ -d '#{http_request.body}'} if http_request.body
|
96
|
+
curl << %Q{ "#{uri.to_s}"}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# @return [URI::HTTPS] the (memoized) URI of the request.
|
103
|
+
def uri
|
104
|
+
attributes = {host: @host, path: @path, query: @params.to_param}
|
105
|
+
@uri ||= URI::HTTPS.build attributes
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Net::HTTPRequest] the full HTTP request object,
|
109
|
+
# inclusive of headers of request body.
|
110
|
+
def http_request
|
111
|
+
net_http_class = "Net::HTTP::#{@method.capitalize}".constantize
|
112
|
+
@http_request ||= net_http_class.new(uri.request_uri).tap do |request|
|
113
|
+
set_request_body! request
|
114
|
+
set_request_headers! request
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Adds the request body to the request in the appropriate format.
|
119
|
+
# if the request body is a JSON Object, transform its keys into camel-case,
|
120
|
+
# since this is the common format for JSON APIs.
|
121
|
+
def set_request_body!(request)
|
122
|
+
case @request_format
|
123
|
+
when :json then request.body = (camelize_keys! @body).to_json
|
124
|
+
when :form then request.set_form_data @body
|
125
|
+
when :file then request.body_stream = @body
|
126
|
+
end if @body
|
127
|
+
end
|
128
|
+
|
129
|
+
# Destructively converts all the keys of hash to camel-case symbols.
|
130
|
+
# Note: This is to be invoked by all YouTube API except Accounts
|
131
|
+
def camelize_keys!(hash)
|
132
|
+
hash.keys.each do |key|
|
133
|
+
hash[key.to_s.camelize(:lower).to_sym] = hash.delete key
|
134
|
+
end if hash.is_a? Hash
|
135
|
+
hash
|
136
|
+
end
|
137
|
+
|
138
|
+
# Adds the request headers to the request in the appropriate format.
|
139
|
+
# The User-Agent header is also set to recognize the request, and to
|
140
|
+
# tell the server that gzip compression can be used, since Net::HTTP
|
141
|
+
# supports it and automatically sets the Accept-Encoding header.
|
142
|
+
def set_request_headers!(request)
|
143
|
+
case @request_format
|
144
|
+
when :json
|
145
|
+
request.initialize_http_header 'Content-Type' => 'application/json'
|
146
|
+
request.initialize_http_header 'Content-length' => '0' unless @body
|
147
|
+
when :file
|
148
|
+
request.initialize_http_header 'Content-Length' => @body.size.to_s
|
149
|
+
request.initialize_http_header 'Transfer-Encoding' => 'chunked'
|
150
|
+
end
|
151
|
+
@headers['User-Agent'] = 'Yt::Request (gzip)'
|
152
|
+
@headers['Authorization'] = "Bearer #{@auth.access_token}" if @auth
|
153
|
+
@headers.each{|name, value| request.add_field name, value}
|
154
|
+
end
|
155
|
+
|
156
|
+
# @return [Boolean] whether the class of response returned by running
|
157
|
+
# the request matches the expected class of response.
|
158
|
+
def matches_expectations?
|
159
|
+
response.is_a? @expected_response
|
160
|
+
end
|
161
|
+
|
162
|
+
# Run the request and memoize the response or the server error received.
|
163
|
+
def response
|
164
|
+
@response ||= send_http_request
|
165
|
+
rescue *server_errors => e
|
166
|
+
@response ||= e
|
167
|
+
end
|
168
|
+
|
169
|
+
# Send the request to the server, allowing ActiveSupport::Notifications
|
170
|
+
# client to subscribe to the request.
|
171
|
+
def send_http_request
|
172
|
+
net_http_options = [uri.host, uri.port, use_ssl: true]
|
173
|
+
ActiveSupport::Notifications.instrument 'request.yt' do |payload|
|
174
|
+
payload[:method] = @method
|
175
|
+
payload[:request_uri] = uri
|
176
|
+
payload[:response] = Net::HTTP.start(*net_http_options) do |http|
|
177
|
+
http.request http_request
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Replaces the body of the response with the parsed version of the body,
|
183
|
+
# according to the format specified in the Request.
|
184
|
+
def parse_response!
|
185
|
+
response.body = case @response_format
|
186
|
+
when :xml then Hash.from_xml response.body
|
187
|
+
when :json then JSON response.body
|
188
|
+
end if response.body
|
189
|
+
end
|
190
|
+
|
191
|
+
# Returns whether it is worth to run a failed request again.
|
192
|
+
# There are two cases in which retrying a request might be worth:
|
193
|
+
# - when the server specifies that the request token has expired and
|
194
|
+
# the user has to refresh the token in order to tryi again
|
195
|
+
# - when the server is unreachable, and waiting for a couple of seconds
|
196
|
+
# might solve the connection issues.
|
197
|
+
def run_again?
|
198
|
+
refresh_token_and_retry? || server_error? && sleep_and_retry?
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns the list of server errors worth retrying the request once.
|
202
|
+
def server_errors
|
203
|
+
[
|
204
|
+
OpenSSL::SSL::SSLError,
|
205
|
+
Errno::ETIMEDOUT,
|
206
|
+
Errno::ENETUNREACH,
|
207
|
+
Errno::ECONNRESET,
|
208
|
+
Net::HTTPServerError
|
209
|
+
]
|
210
|
+
end
|
211
|
+
|
212
|
+
# Sleeps for a while and returns true for the first +max_retries+ times,
|
213
|
+
# then returns false. Useful to try the same request again multiple
|
214
|
+
# times with a delay if a connection error occurs.
|
215
|
+
def sleep_and_retry?(max_retries = 1)
|
216
|
+
@retries_so_far ||= -1
|
217
|
+
@retries_so_far += 1
|
218
|
+
if (@retries_so_far < max_retries)
|
219
|
+
@response = @http_request = @uri = nil
|
220
|
+
sleep 3
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# In case an authorized request responds with "Unauthorized", checks
|
225
|
+
# if the original access token can be refreshed. If that's the case,
|
226
|
+
# clears the memoized variables and returns true, so the request can
|
227
|
+
# be run again, otherwise raises an error.
|
228
|
+
def refresh_token_and_retry?
|
229
|
+
if unauthorized? && @auth && @auth.refreshed_access_token?
|
230
|
+
@response = @http_request = @uri = nil
|
231
|
+
true
|
232
|
+
end
|
233
|
+
rescue Errors::MissingAuth
|
234
|
+
false
|
235
|
+
end
|
236
|
+
|
237
|
+
# @return [Yt::RequestError] the error associated to the class of the
|
238
|
+
# response.
|
239
|
+
def response_error
|
240
|
+
case response
|
241
|
+
when *server_errors then Errors::ServerError
|
242
|
+
when Net::HTTPUnauthorized then Errors::Unauthorized
|
243
|
+
when Net::HTTPForbidden then Errors::Forbidden
|
244
|
+
else Errors::RequestError
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# @return [Boolean] whether the response matches any server error.
|
249
|
+
def server_error?
|
250
|
+
response_error == Errors::ServerError
|
251
|
+
end
|
252
|
+
|
253
|
+
# @return [Boolean] whether the request lacks proper authorization.
|
254
|
+
def unauthorized?
|
255
|
+
response_error == Errors::Unauthorized
|
256
|
+
end
|
257
|
+
|
258
|
+
# Return the elements of the request/response that are worth displaying
|
259
|
+
# as an error message if the request fails.
|
260
|
+
# If the response format is JSON, showing the parsed body is sufficient,
|
261
|
+
# otherwise the whole (inspected) response is worth looking at.
|
262
|
+
def error_message
|
263
|
+
response_body = JSON(response.body) rescue response.inspect
|
264
|
+
{request_curl: as_curl, response_body: response_body}
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|