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/version.rb
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'yt/collections/reports'
|
3
|
+
require 'yt/models/content_owner'
|
4
|
+
|
5
|
+
describe Yt::Collections::Reports do
|
6
|
+
subject(:reports) { Yt::Collections::Reports.new parent: content_owner }
|
7
|
+
let(:content_owner) { Yt::ContentOwner.new owner_name: 'any-name' }
|
8
|
+
let(:error){ {reason: reason, message: message} }
|
9
|
+
let(:msg) { {response_body: {error: {errors: [error]}}}.to_json }
|
10
|
+
|
11
|
+
describe '#within' do
|
12
|
+
let(:result) { reports.within Range.new(5.days.ago, 4.days.ago) }
|
13
|
+
context 'given the request raises error 400 with "Invalid Query" message' do
|
14
|
+
let(:reason) { 'badRequest' }
|
15
|
+
let(:message) { 'Invalid query. Query did not conform to the expectations' }
|
16
|
+
before { expect(reports).to receive(:list).once.and_raise Yt::Error, msg }
|
17
|
+
let(:try_again) { expect(reports).to receive(:list).at_least(:once) }
|
18
|
+
|
19
|
+
context 'every time' do
|
20
|
+
before { try_again.and_raise Yt::Error, msg }
|
21
|
+
it { expect{result}.to fail }
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'but returns a success code 2XX the second time' do
|
25
|
+
before { try_again.and_return [1,2] }
|
26
|
+
it { expect{result}.not_to fail }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spec/models/request_spec.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'yt/
|
2
|
+
require 'yt/request'
|
3
3
|
|
4
4
|
|
5
5
|
describe Yt::Request do
|
@@ -37,28 +37,6 @@ describe Yt::Request do
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
context 'an error code 400 with "Invalid Query" message' do
|
41
|
-
let(:response_class) { Net::HTTPBadRequest }
|
42
|
-
let(:response_body) { {error: {errors: [message: message]}}.to_json }
|
43
|
-
let(:message) { 'Invalid query. Query did not conform to the expectations' }
|
44
|
-
|
45
|
-
let(:retry_response) { retry_response_class.new nil, nil, nil }
|
46
|
-
before { allow(retry_response).to receive(:body) }
|
47
|
-
before { expect(Net::HTTP).to receive(:start).at_least(:once).and_return retry_response }
|
48
|
-
|
49
|
-
context 'every time' do
|
50
|
-
let(:retry_response_class) { Net::HTTPBadRequest }
|
51
|
-
|
52
|
-
it { expect{request.run}.to fail }
|
53
|
-
end
|
54
|
-
|
55
|
-
context 'but returns a success code 2XX the second time' do
|
56
|
-
let(:retry_response_class) { Net::HTTPOK }
|
57
|
-
|
58
|
-
it { expect{request.run}.not_to fail }
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
40
|
context 'an error code 401' do
|
63
41
|
let(:response_class) { Net::HTTPUnauthorized }
|
64
42
|
|
@@ -3,6 +3,13 @@ require 'spec_helper'
|
|
3
3
|
require 'yt/models/account'
|
4
4
|
|
5
5
|
describe Yt::Account, :device_app do
|
6
|
+
describe 'can create playlists' do
|
7
|
+
let(:params) { {title: 'Test Yt playlist', privacy_status: 'unlisted'} }
|
8
|
+
before { @playlist = $account.create_playlist params }
|
9
|
+
it { expect(@playlist).to be_a Yt::Playlist }
|
10
|
+
after { @playlist.delete }
|
11
|
+
end
|
12
|
+
|
6
13
|
describe '.channel' do
|
7
14
|
it { expect($account.channel).to be_a Yt::Channel }
|
8
15
|
end
|
@@ -45,7 +52,7 @@ describe Yt::Account, :device_app do
|
|
45
52
|
end
|
46
53
|
|
47
54
|
describe '.upload_video' do
|
48
|
-
let(:video_params) { {title: 'Test Yt upload', privacy_status: 'private'} }
|
55
|
+
let(:video_params) { {title: 'Test Yt upload', privacy_status: 'private', category_id: 17} }
|
49
56
|
let(:video) { $account.upload_video path_or_url, video_params }
|
50
57
|
after { video.delete }
|
51
58
|
|
@@ -13,7 +13,7 @@ describe Yt::Account, :device_app do
|
|
13
13
|
# same second, refreshing the token returns the same token. Still,
|
14
14
|
# testing that *expires_at* changes is a guarantee that we attempted
|
15
15
|
# to get a new token, which is what refresh is meant to do.
|
16
|
-
it { expect{account.
|
16
|
+
it { expect{account.refreshed_access_token?}.to change{account.expires_at} }
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
@@ -23,7 +23,6 @@ describe Yt::Channel, :device_app do
|
|
23
23
|
|
24
24
|
it { expect(channel.videos.first).to be_a Yt::Video }
|
25
25
|
it { expect(channel.playlists.first).to be_a Yt::Playlist }
|
26
|
-
it { expect{channel.create_playlist}.to raise_error Yt::Errors::RequestError }
|
27
26
|
it { expect{channel.delete_playlists}.to raise_error Yt::Errors::RequestError }
|
28
27
|
|
29
28
|
specify 'with a public list of subscriptions' do
|
@@ -105,15 +104,9 @@ describe Yt::Channel, :device_app do
|
|
105
104
|
expect(channel.subscriptions.size).to be
|
106
105
|
end
|
107
106
|
|
108
|
-
describe 'playlists can be added' do
|
109
|
-
after { channel.delete_playlists params }
|
110
|
-
it { expect(channel.create_playlist params).to be_a Yt::Playlist }
|
111
|
-
it { expect{channel.create_playlist params}.to change{channel.playlists.count}.by(1) }
|
112
|
-
end
|
113
|
-
|
114
107
|
describe 'playlists can be deleted' do
|
115
108
|
let(:title) { "Yt Test Delete All Playlists #{rand}" }
|
116
|
-
before {
|
109
|
+
before { $account.create_playlist params }
|
117
110
|
|
118
111
|
it { expect(channel.delete_playlists title: %r{#{params[:title]}}).to eq [true] }
|
119
112
|
it { expect(channel.delete_playlists params).to eq [true] }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: yt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Claudio Baccigalupo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-09-
|
11
|
+
date: 2014-09-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -111,7 +111,7 @@ files:
|
|
111
111
|
- MIT-LICENSE
|
112
112
|
- README.md
|
113
113
|
- Rakefile
|
114
|
-
-
|
114
|
+
- YOUTUBE_IT.md
|
115
115
|
- bin/yt
|
116
116
|
- gemfiles/Gemfile.activesupport-3.x
|
117
117
|
- gemfiles/Gemfile.activesupport-4.x
|
@@ -193,7 +193,6 @@ files:
|
|
193
193
|
- lib/yt/models/policy_rule.rb
|
194
194
|
- lib/yt/models/rating.rb
|
195
195
|
- lib/yt/models/reference.rb
|
196
|
-
- lib/yt/models/request.rb
|
197
196
|
- lib/yt/models/resource.rb
|
198
197
|
- lib/yt/models/resumable_session.rb
|
199
198
|
- lib/yt/models/right_owner.rb
|
@@ -205,12 +204,14 @@ files:
|
|
205
204
|
- lib/yt/models/url.rb
|
206
205
|
- lib/yt/models/user_info.rb
|
207
206
|
- lib/yt/models/video.rb
|
207
|
+
- lib/yt/request.rb
|
208
208
|
- lib/yt/version.rb
|
209
209
|
- spec/collections/claims_spec.rb
|
210
210
|
- spec/collections/playlist_items_spec.rb
|
211
211
|
- spec/collections/playlists_spec.rb
|
212
212
|
- spec/collections/policies_spec.rb
|
213
213
|
- spec/collections/references_spec.rb
|
214
|
+
- spec/collections/reports_spec.rb
|
214
215
|
- spec/collections/subscriptions_spec.rb
|
215
216
|
- spec/collections/videos_spec.rb
|
216
217
|
- spec/errors/forbidden_spec.rb
|
@@ -304,6 +305,7 @@ test_files:
|
|
304
305
|
- spec/collections/playlists_spec.rb
|
305
306
|
- spec/collections/policies_spec.rb
|
306
307
|
- spec/collections/references_spec.rb
|
308
|
+
- spec/collections/reports_spec.rb
|
307
309
|
- spec/collections/subscriptions_spec.rb
|
308
310
|
- spec/collections/videos_spec.rb
|
309
311
|
- spec/errors/forbidden_spec.rb
|
data/TODO.md
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
* methods like Yt::Account.new(params = {}) should use HashWithIndifferentAccess
|
2
|
-
* add canonical_url to Resource, then use it in promo
|
3
|
-
|
4
|
-
List of supported methods
|
5
|
-
=========================
|
6
|
-
|
7
|
-
YouTube Data API V3 (https://developers.google.com/youtube/v3/docs)
|
8
|
-
-------------------------------------------------------------------
|
9
|
-
|
10
|
-
- [ ] Activities
|
11
|
-
- [ ] list
|
12
|
-
- [ ] insert
|
13
|
-
- [ ] ChannelBanners
|
14
|
-
- [ ] insert
|
15
|
-
- [ ] Channels
|
16
|
-
- [ ] list
|
17
|
-
- [ ] update
|
18
|
-
- [ ] ChannelSections
|
19
|
-
- [ ] list
|
20
|
-
- [ ] insert
|
21
|
-
- [ ] update
|
22
|
-
- [ ] delete
|
23
|
-
- [ ] GuideCategories
|
24
|
-
- [ ] list
|
25
|
-
- [ ] I18nLanguages
|
26
|
-
- [ ] list
|
27
|
-
- [ ] I18nRegions
|
28
|
-
- [ ] list
|
29
|
-
- [ ] PlaylistItems
|
30
|
-
- [ ] list
|
31
|
-
- [ ] insert
|
32
|
-
- [ ] update
|
33
|
-
- [ ] delete
|
34
|
-
- [ ] Playlists
|
35
|
-
- [ ] list
|
36
|
-
- [ ] insert
|
37
|
-
- [ ] update
|
38
|
-
- [ ] delete
|
39
|
-
- [ ] Search
|
40
|
-
- [ ] list
|
41
|
-
- [ ] Subscriptions
|
42
|
-
- [ ] list
|
43
|
-
- [ ] insert
|
44
|
-
- [ ] delete
|
45
|
-
- [ ] Thumbnails
|
46
|
-
- [ ] set
|
47
|
-
- [ ] VideoCategories
|
48
|
-
- [ ] list
|
49
|
-
- [ ] Videos
|
50
|
-
- [ ] list
|
51
|
-
- [ ] insert
|
52
|
-
- [ ] update
|
53
|
-
- [ ] rate
|
54
|
-
- [ ] getRating
|
55
|
-
- [ ] delete
|
56
|
-
- [ ] Watermarks
|
57
|
-
- [ ] set
|
58
|
-
- [ ] unset
|
data/lib/yt/models/request.rb
DELETED
@@ -1,223 +0,0 @@
|
|
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/config'
|
8
|
-
require 'yt/errors/unauthorized'
|
9
|
-
require 'yt/errors/request_error'
|
10
|
-
require 'yt/errors/server_error'
|
11
|
-
require 'yt/errors/forbidden'
|
12
|
-
|
13
|
-
module Yt
|
14
|
-
module Models
|
15
|
-
class Request
|
16
|
-
def initialize(options = {})
|
17
|
-
@auth = options[:auth]
|
18
|
-
@file = options[:file]
|
19
|
-
@body_type = options.fetch :body_type, :json
|
20
|
-
@expected_response = options.fetch :expected_response, Net::HTTPSuccess
|
21
|
-
@format = options.fetch :format, :json
|
22
|
-
@headers = options.fetch :headers, gzip_headers
|
23
|
-
@host = options.fetch :host, google_api_host
|
24
|
-
@method = options.fetch :method, :get
|
25
|
-
@path = options[:path]
|
26
|
-
@body = options[:body]
|
27
|
-
camelize_keys! @body if options.fetch(:camelize_body, true)
|
28
|
-
params = options.fetch :params, {}
|
29
|
-
camelize_keys! params if options.fetch(:camelize_params, true)
|
30
|
-
@query = params.to_param
|
31
|
-
end
|
32
|
-
|
33
|
-
|
34
|
-
def run
|
35
|
-
p as_curl if Yt.configuration.developing?
|
36
|
-
|
37
|
-
if response.is_a? @expected_response
|
38
|
-
response.tap{|response| response.body = parse_format response.body}
|
39
|
-
else
|
40
|
-
run_again? ? run : raise(response_error, request_error_message)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def request_error_message
|
45
|
-
{}.tap do |message|
|
46
|
-
message[:request_curl] = as_curl
|
47
|
-
message[:response_body] = JSON(response.body) rescue response.inspect
|
48
|
-
end.to_json
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def response
|
54
|
-
@response ||= send_http_request
|
55
|
-
rescue OpenSSL::SSL::SSLError, Errno::ETIMEDOUT, Errno::ENETUNREACH, Errno::ECONNRESET => e
|
56
|
-
@response ||= e
|
57
|
-
end
|
58
|
-
|
59
|
-
def send_http_request
|
60
|
-
ActiveSupport::Notifications.instrument 'request.yt' do |payload|
|
61
|
-
payload[:method] = @method
|
62
|
-
payload[:request_uri] = uri
|
63
|
-
payload[:response] = Net::HTTP.start(*net_http_options) do |http|
|
64
|
-
http.request http_request
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def http_request
|
70
|
-
@http_request ||= net_http_class.new(uri.request_uri).tap do |request|
|
71
|
-
set_headers! request
|
72
|
-
set_body! request
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def set_headers!(request)
|
77
|
-
case @body_type
|
78
|
-
when :json
|
79
|
-
request.initialize_http_header 'Content-Type' => 'application/json'
|
80
|
-
request.initialize_http_header 'Content-length' => '0' unless @body
|
81
|
-
when :file
|
82
|
-
request.initialize_http_header 'Content-Length' => @body.size.to_s
|
83
|
-
request.initialize_http_header 'Transfer-Encoding' => 'chunked'
|
84
|
-
end
|
85
|
-
@headers.each{|name, value| request.add_field name, value}
|
86
|
-
end
|
87
|
-
|
88
|
-
# To receive a gzip-encoded response you must do two things:
|
89
|
-
# - Set the Accept-Encoding HTTP request header to gzip.
|
90
|
-
# - Modify your user agent to contain the string gzip.
|
91
|
-
# Net::HTTP already sets the Accept-Encoding header, so all it’s left
|
92
|
-
# to do is to specify an appropriate User Agent.
|
93
|
-
# @see https://developers.google.com/youtube/v3/getting-started#gzip
|
94
|
-
# @see http://www.ietf.org/rfc/rfc2616.txt
|
95
|
-
def gzip_headers
|
96
|
-
@gzip_headers ||= {}.tap do |headers|
|
97
|
-
headers['user-agent'] = 'Yt (gzip)'
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def set_body!(request)
|
102
|
-
case @body_type
|
103
|
-
when :json then request.body = @body.to_json
|
104
|
-
when :form then request.set_form_data @body
|
105
|
-
when :file then request.body_stream = @body
|
106
|
-
end if @body
|
107
|
-
end
|
108
|
-
|
109
|
-
def net_http_options
|
110
|
-
[uri.host, uri.port, use_ssl: true]
|
111
|
-
end
|
112
|
-
|
113
|
-
def net_http_class
|
114
|
-
"Net::HTTP::#{@method.capitalize}".constantize
|
115
|
-
end
|
116
|
-
|
117
|
-
def uri
|
118
|
-
@uri ||= build_uri
|
119
|
-
end
|
120
|
-
|
121
|
-
def build_uri
|
122
|
-
add_authorization! if @host == google_api_host
|
123
|
-
URI::HTTPS.build host: @host, path: @path, query: @query
|
124
|
-
end
|
125
|
-
|
126
|
-
def add_authorization!
|
127
|
-
if @auth.respond_to? :access_token
|
128
|
-
@headers['Authorization'] = "Bearer #{@auth.access_token}"
|
129
|
-
elsif Yt.configuration.api_key
|
130
|
-
params = URI.decode_www_form @query || ''
|
131
|
-
params << [:key, Yt.configuration.api_key]
|
132
|
-
@query = URI.encode_www_form params
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def google_api_host
|
137
|
-
'www.googleapis.com'
|
138
|
-
end
|
139
|
-
|
140
|
-
def parse_format(body)
|
141
|
-
case @format
|
142
|
-
when :xml then Hash.from_xml body
|
143
|
-
when :json then JSON body
|
144
|
-
end if body
|
145
|
-
end
|
146
|
-
|
147
|
-
def camelize_keys!(object)
|
148
|
-
return object unless object.is_a?(Hash)
|
149
|
-
object.dup.each_key do |key|
|
150
|
-
object[key.to_s.camelize(:lower).to_sym] = object.delete key
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
# There are two cases to run a request again: YouTube responds with a
|
155
|
-
# random error that can be fixed by waiting for some seconds and running
|
156
|
-
# the exact same query, or the access token needs to be refreshed.
|
157
|
-
def run_again?
|
158
|
-
refresh_token_and_retry? || server_error? && sleep_and_retry?
|
159
|
-
end
|
160
|
-
|
161
|
-
# Once in a while, YouTube responds with 500, or 503, or 400 Error and
|
162
|
-
# the text "Invalid query. Query did not conform to the expectations.".
|
163
|
-
# In all these cases, running the same query after some seconds fixes
|
164
|
-
# the issue. This it not documented by YouTube and hardly testable, but
|
165
|
-
# trying again is a workaround that works and hardly causes any damage.
|
166
|
-
def sleep_and_retry?(max_retries = 1)
|
167
|
-
@retries_so_far ||= -1
|
168
|
-
@retries_so_far += 1
|
169
|
-
if (@retries_so_far < max_retries)
|
170
|
-
@response = @http_request = @uri = nil
|
171
|
-
sleep 3
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
def server_error?
|
176
|
-
case response
|
177
|
-
when OpenSSL::SSL::SSLError then true
|
178
|
-
when Errno::ETIMEDOUT then true
|
179
|
-
when Errno::ENETUNREACH then true
|
180
|
-
when Net::HTTPServerError then true
|
181
|
-
when Net::HTTPBadRequest then response.body =~ /did not conform/
|
182
|
-
else false
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# If a request authorized with an access token returns 401, then the
|
187
|
-
# access token might have expired. If a refresh token is also present,
|
188
|
-
# try to run the request one more time with a refreshed access token.
|
189
|
-
# If it's not present, then don't raise the returned MissingAuth, just
|
190
|
-
# let the original error bubble up.
|
191
|
-
def refresh_token_and_retry?
|
192
|
-
if response.is_a? Net::HTTPUnauthorized
|
193
|
-
@auth.refresh.tap { @response = @http_request = @uri = nil }
|
194
|
-
end if @auth.respond_to? :refresh
|
195
|
-
rescue Errors::MissingAuth
|
196
|
-
false
|
197
|
-
end
|
198
|
-
|
199
|
-
def response_error
|
200
|
-
if server_error?
|
201
|
-
Errors::ServerError
|
202
|
-
else case response
|
203
|
-
when Net::HTTPUnauthorized then Errors::Unauthorized
|
204
|
-
when Net::HTTPForbidden then Errors::Forbidden
|
205
|
-
else Errors::RequestError
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
def as_curl
|
211
|
-
'curl'.tap do |curl|
|
212
|
-
curl << " -X #{http_request.method}"
|
213
|
-
http_request.each_header do |name, value|
|
214
|
-
next if gzip_headers.has_key? name
|
215
|
-
curl << %Q{ -H "#{name}: #{value}"}
|
216
|
-
end
|
217
|
-
curl << %Q{ -d '#{http_request.body}'} if http_request.body
|
218
|
-
curl << %Q{ "#{@uri.to_s}"}
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|