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.
@@ -1,3 +1,3 @@
1
1
  module Yt
2
- VERSION = '0.12.2'
2
+ VERSION = '0.13.0'
3
3
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  require 'spec_helper'
2
- require 'yt/models/request'
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.refresh}.to change{account.expires_at} }
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 { channel.create_playlist params }
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.12.2
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-10 00:00:00.000000000 Z
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
- - TODO.md
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
@@ -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