yt 0.25.13 → 0.32.2
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 +5 -5
- data/CHANGELOG.md +305 -1
- data/MIT-LICENSE +1 -1
- data/README.md +86 -5
- data/YOUTUBE_IT.md +3 -3
- data/lib/yt.rb +5 -2
- data/lib/yt/actions/list.rb +3 -3
- data/lib/yt/associations/has_authentication.rb +33 -1
- data/lib/yt/associations/has_reports.rb +13 -18
- data/lib/yt/collections/assets.rb +2 -2
- data/lib/yt/collections/authentications.rb +9 -2
- data/lib/yt/collections/base.rb +3 -3
- data/lib/yt/collections/bulk_report_jobs.rb +28 -0
- data/lib/yt/collections/bulk_reports.rb +24 -0
- data/lib/yt/collections/claims.rb +22 -1
- data/lib/yt/collections/comment_threads.rb +41 -0
- data/lib/yt/collections/content_owners.rb +1 -1
- data/lib/yt/collections/group_infos.rb +27 -0
- data/lib/yt/collections/group_items.rb +45 -0
- data/lib/yt/collections/reports.rb +75 -13
- data/lib/yt/collections/revocations.rb +30 -0
- data/lib/yt/collections/video_groups.rb +29 -0
- data/lib/yt/collections/videos.rb +34 -9
- data/lib/yt/constants/geography.rb +326 -0
- data/lib/yt/errors/forbidden.rb +1 -3
- data/lib/yt/errors/no_items.rb +1 -3
- data/lib/yt/errors/request_error.rb +10 -7
- data/lib/yt/errors/server_error.rb +1 -3
- data/lib/yt/errors/unauthorized.rb +3 -3
- data/lib/yt/models/account.rb +12 -0
- data/lib/yt/models/advertising_options_set.rb +4 -4
- data/lib/yt/models/bulk_report.rb +23 -0
- data/lib/yt/models/bulk_report_job.rb +23 -0
- data/lib/yt/models/channel.rb +21 -12
- data/lib/yt/models/claim.rb +13 -2
- data/lib/yt/models/comment.rb +37 -0
- data/lib/yt/models/comment_thread.rb +50 -0
- data/lib/yt/models/content_detail.rb +6 -0
- data/lib/yt/models/content_owner.rb +31 -1
- data/lib/yt/models/group_info.rb +16 -0
- data/lib/yt/models/group_item.rb +15 -0
- data/lib/yt/models/resource.rb +3 -10
- data/lib/yt/models/revocation.rb +12 -0
- data/lib/yt/models/right_owner.rb +0 -2
- data/lib/yt/models/snippet.rb +24 -3
- data/lib/yt/models/video.rb +42 -11
- data/lib/yt/models/video_group.rb +186 -0
- data/lib/yt/request.rb +5 -3
- data/lib/yt/version.rb +2 -2
- data/spec/collections/comment_threads_spec.rb +46 -0
- data/spec/collections/playlist_items_spec.rb +1 -1
- data/spec/collections/reports_spec.rb +2 -2
- data/spec/constants/geography_spec.rb +16 -0
- data/spec/models/annotation_spec.rb +1 -1
- data/spec/models/claim_spec.rb +15 -3
- data/spec/models/comment_spec.rb +40 -0
- data/spec/models/comment_thread_spec.rb +93 -0
- data/spec/models/content_detail_spec.rb +7 -0
- data/spec/models/reference_spec.rb +2 -2
- data/spec/models/request_spec.rb +21 -0
- data/spec/models/resource_spec.rb +0 -15
- data/spec/models/video_spec.rb +1 -1
- data/spec/requests/as_account/account_spec.rb +16 -4
- data/spec/requests/as_account/authentications_spec.rb +1 -13
- data/spec/requests/as_account/channel_spec.rb +15 -45
- data/spec/requests/as_account/playlist_item_spec.rb +3 -3
- data/spec/requests/as_account/playlist_spec.rb +5 -32
- data/spec/requests/as_account/video_spec.rb +2022 -21
- data/spec/requests/as_content_owner/account_spec.rb +4 -0
- data/spec/requests/as_content_owner/bulk_report_job_spec.rb +19 -0
- data/spec/requests/as_content_owner/channel_spec.rb +59 -270
- data/spec/requests/as_content_owner/content_owner_spec.rb +89 -1
- data/spec/requests/as_content_owner/playlist_spec.rb +0 -15
- data/spec/requests/as_content_owner/video_group_spec.rb +112 -0
- data/spec/requests/as_content_owner/video_spec.rb +72 -146
- data/spec/requests/as_server_app/channel_spec.rb +1 -21
- data/spec/requests/as_server_app/comment_spec.rb +22 -0
- data/spec/requests/as_server_app/comment_thread_spec.rb +27 -0
- data/spec/requests/as_server_app/comment_threads_spec.rb +41 -0
- data/spec/requests/as_server_app/playlist_item_spec.rb +2 -2
- data/spec/requests/as_server_app/playlist_spec.rb +1 -22
- data/spec/requests/as_server_app/video_spec.rb +21 -19
- data/spec/requests/as_server_app/videos_spec.rb +5 -5
- data/spec/requests/unauthenticated/video_spec.rb +1 -9
- data/spec/spec_helper.rb +1 -1
- data/yt.gemspec +2 -1
- metadata +51 -17
- data/lib/yt/collections/ids.rb +0 -27
- data/lib/yt/config.rb +0 -54
- data/lib/yt/models/configuration.rb +0 -70
- data/lib/yt/models/description.rb +0 -58
- data/lib/yt/models/url.rb +0 -91
- data/spec/models/configuration_spec.rb +0 -44
- data/spec/models/description_spec.rb +0 -94
- data/spec/models/url_spec.rb +0 -84
- data/spec/requests/as_account/resource_spec.rb +0 -18
|
@@ -80,22 +80,6 @@ describe Yt::Channel, :device_app do
|
|
|
80
80
|
expect(channel.video_count).to be > 500
|
|
81
81
|
expect(channel.videos.size).to be > 500
|
|
82
82
|
end
|
|
83
|
-
|
|
84
|
-
specify 'over 500 videos can only be retrieved when sorting by date' do
|
|
85
|
-
# @note: these tests are slow because they go through multiple pages
|
|
86
|
-
# of results to test that we can overcome YouTube’s limitation of only
|
|
87
|
-
# returning the first 500 results when ordered by date.
|
|
88
|
-
expect(channel.videos.count).to be > 500
|
|
89
|
-
expect(channel.videos.where(order: 'viewCount').count).to be 500
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
specify 'over 500 videos can be retrieved even with a publishedBefore condition' do
|
|
93
|
-
# @note: these tests are slow because they go through multiple pages
|
|
94
|
-
# of results to test that we can overcome YouTube’s limitation of only
|
|
95
|
-
# returning the first 500 results when ordered by date.
|
|
96
|
-
today = Date.today.beginning_of_day.iso8601(0)
|
|
97
|
-
expect(channel.videos.where(published_before: today).count).to be > 500
|
|
98
|
-
end
|
|
99
83
|
end
|
|
100
84
|
end
|
|
101
85
|
|
|
@@ -164,8 +148,12 @@ describe Yt::Channel, :device_app do
|
|
|
164
148
|
before { channel.throttle_subscriptions }
|
|
165
149
|
|
|
166
150
|
it { expect(channel.subscribed?).to be true }
|
|
167
|
-
|
|
168
|
-
|
|
151
|
+
# NOTE: These tests are commented out because YouTube randomly changed the
|
|
152
|
+
# behavior of the API without changing the documentation, so subscribing
|
|
153
|
+
# to a channel you are already subscribed to does not raise an error
|
|
154
|
+
# anymore.
|
|
155
|
+
# it { expect(channel.subscribe).to be_falsey }
|
|
156
|
+
# it { expect{channel.subscribe!}.to raise_error Yt::Errors::RequestError }
|
|
169
157
|
|
|
170
158
|
context 'when I unsubscribe' do
|
|
171
159
|
before { channel.unsubscribe }
|
|
@@ -195,7 +183,7 @@ describe Yt::Channel, :device_app do
|
|
|
195
183
|
|
|
196
184
|
it { expect(channel.delete_playlists title: %r{#{params[:title]}}).to eq [true] }
|
|
197
185
|
it { expect(channel.delete_playlists params).to eq [true] }
|
|
198
|
-
it { expect{channel.delete_playlists params}.to change{channel.playlists.count}.by(-1) }
|
|
186
|
+
it { expect{channel.delete_playlists params}.to change{sleep 1; channel.playlists.count}.by(-1) }
|
|
199
187
|
end
|
|
200
188
|
|
|
201
189
|
# Can't subscribe to your own channel.
|
|
@@ -212,8 +200,6 @@ describe Yt::Channel, :device_app do
|
|
|
212
200
|
expect{channel.shares}.not_to raise_error
|
|
213
201
|
expect{channel.subscribers_gained}.not_to raise_error
|
|
214
202
|
expect{channel.subscribers_lost}.not_to raise_error
|
|
215
|
-
expect{channel.favorites_added}.not_to raise_error
|
|
216
|
-
expect{channel.favorites_removed}.not_to raise_error
|
|
217
203
|
expect{channel.videos_added_to_playlists}.not_to raise_error
|
|
218
204
|
expect{channel.videos_removed_from_playlists}.not_to raise_error
|
|
219
205
|
expect{channel.estimated_minutes_watched}.not_to raise_error
|
|
@@ -222,33 +208,17 @@ describe Yt::Channel, :device_app do
|
|
|
222
208
|
expect{channel.annotation_clicks}.not_to raise_error
|
|
223
209
|
expect{channel.annotation_click_through_rate}.not_to raise_error
|
|
224
210
|
expect{channel.annotation_close_rate}.not_to raise_error
|
|
211
|
+
expect{channel.card_impressions}.not_to raise_error
|
|
212
|
+
expect{channel.card_clicks}.not_to raise_error
|
|
213
|
+
expect{channel.card_click_rate}.not_to raise_error
|
|
214
|
+
expect{channel.card_teaser_impressions}.not_to raise_error
|
|
215
|
+
expect{channel.card_teaser_clicks}.not_to raise_error
|
|
216
|
+
expect{channel.card_teaser_click_rate}.not_to raise_error
|
|
225
217
|
expect{channel.viewer_percentage}.not_to raise_error
|
|
226
|
-
expect{channel.
|
|
227
|
-
expect{channel.
|
|
218
|
+
expect{channel.estimated_revenue}.to raise_error Yt::Errors::Unauthorized
|
|
219
|
+
expect{channel.ad_impressions}.to raise_error Yt::Errors::Unauthorized
|
|
228
220
|
expect{channel.monetized_playbacks}.to raise_error Yt::Errors::Unauthorized
|
|
229
221
|
expect{channel.playback_based_cpm}.to raise_error Yt::Errors::Unauthorized
|
|
230
|
-
|
|
231
|
-
expect{channel.views_on 3.days.ago}.not_to raise_error
|
|
232
|
-
expect{channel.comments_on 3.days.ago}.not_to raise_error
|
|
233
|
-
expect{channel.likes_on 3.days.ago}.not_to raise_error
|
|
234
|
-
expect{channel.dislikes_on 3.days.ago}.not_to raise_error
|
|
235
|
-
expect{channel.shares_on 3.days.ago}.not_to raise_error
|
|
236
|
-
expect{channel.subscribers_gained_on 3.days.ago}.not_to raise_error
|
|
237
|
-
expect{channel.subscribers_lost_on 3.days.ago}.not_to raise_error
|
|
238
|
-
expect{channel.favorites_added_on 3.days.ago}.not_to raise_error
|
|
239
|
-
expect{channel.favorites_removed_on 3.days.ago}.not_to raise_error
|
|
240
|
-
expect{channel.videos_added_to_playlists_on 3.days.ago}.not_to raise_error
|
|
241
|
-
expect{channel.videos_removed_from_playlists_on 3.days.ago}.not_to raise_error
|
|
242
|
-
expect{channel.estimated_minutes_watched_on 3.days.ago}.not_to raise_error
|
|
243
|
-
expect{channel.average_view_duration_on 3.days.ago}.not_to raise_error
|
|
244
|
-
expect{channel.average_view_percentage_on 3.days.ago}.not_to raise_error
|
|
245
|
-
expect{channel.earnings_on 3.days.ago}.to raise_error Yt::Errors::Unauthorized
|
|
246
|
-
expect{channel.impressions_on 3.days.ago}.to raise_error Yt::Errors::Unauthorized
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
it 'cannot give information about its content owner' do
|
|
250
|
-
expect{channel.content_owner}.to raise_error Yt::Errors::Forbidden
|
|
251
|
-
expect{channel.linked_at}.to raise_error Yt::Errors::Forbidden
|
|
252
222
|
end
|
|
253
223
|
end
|
|
254
224
|
|
|
@@ -5,7 +5,7 @@ describe Yt::PlaylistItem, :device_app do
|
|
|
5
5
|
subject(:item) { Yt::PlaylistItem.new id: id, auth: $account }
|
|
6
6
|
|
|
7
7
|
context 'given an existing playlist item' do
|
|
8
|
-
let(:id) { '
|
|
8
|
+
let(:id) { 'UExTV1lrWXpPclBNVDlwSkc1U3Q1RzBXRGFsaFJ6R2tVNC4yQUE2Q0JEMTk4NTM3RTZC' }
|
|
9
9
|
|
|
10
10
|
it 'returns valid metadata' do
|
|
11
11
|
expect(item.title).to be_a String
|
|
@@ -32,8 +32,8 @@ describe Yt::PlaylistItem, :device_app do
|
|
|
32
32
|
context 'given one of my own playlist items that I want to update' do
|
|
33
33
|
before(:all) do
|
|
34
34
|
@my_playlist = $account.create_playlist title: "Yt Test Update Playlist Item #{rand}"
|
|
35
|
-
@my_playlist.add_video '
|
|
36
|
-
@my_playlist_item = @my_playlist.add_video '
|
|
35
|
+
@my_playlist.add_video '9bZkp7q19f0'
|
|
36
|
+
@my_playlist_item = @my_playlist.add_video '9bZkp7q19f0'
|
|
37
37
|
end
|
|
38
38
|
after(:all) { @my_playlist.delete }
|
|
39
39
|
|
|
@@ -48,18 +48,6 @@ describe Yt::Playlist, :device_app do
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
context 'given a playlist that only includes other people’s private or deleted videos' do
|
|
52
|
-
let(:id) { 'PLsnYEvcCzABOsJdehqkIDhwz8CPGWzX59' }
|
|
53
|
-
|
|
54
|
-
describe '.playlist_items.includes(:video)' do
|
|
55
|
-
let(:items) { playlist.playlist_items.includes(:video).map{|i| i} }
|
|
56
|
-
|
|
57
|
-
specify 'returns nil (without running an infinite loop)' do
|
|
58
|
-
expect(items.size).to be 2
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
51
|
context 'given an unknown playlist' do
|
|
64
52
|
let(:id) { 'not-a-playlist-id' }
|
|
65
53
|
|
|
@@ -69,10 +57,10 @@ describe Yt::Playlist, :device_app do
|
|
|
69
57
|
|
|
70
58
|
context 'given someone else’s playlist' do
|
|
71
59
|
let(:id) { 'PLSWYkYzOrPMT9pJG5St5G0WDalhRzGkU4' }
|
|
72
|
-
let(:video_id) { '
|
|
60
|
+
let(:video_id) { '9bZkp7q19f0' }
|
|
73
61
|
|
|
74
|
-
it { expect{playlist.delete}.to fail.with '
|
|
75
|
-
it { expect{playlist.update}.to fail.with '
|
|
62
|
+
it { expect{playlist.delete}.to fail.with 'playlistForbidden' }
|
|
63
|
+
it { expect{playlist.update}.to fail.with 'playlistForbidden' }
|
|
76
64
|
it { expect{playlist.add_video! video_id}.to raise_error Yt::Errors::RequestError }
|
|
77
65
|
it { expect{playlist.delete_playlist_items}.to raise_error Yt::Errors::RequestError }
|
|
78
66
|
end
|
|
@@ -164,7 +152,7 @@ describe Yt::Playlist, :device_app do
|
|
|
164
152
|
end
|
|
165
153
|
|
|
166
154
|
context 'given an existing video' do
|
|
167
|
-
let(:video_id) { '
|
|
155
|
+
let(:video_id) { '9bZkp7q19f0' }
|
|
168
156
|
|
|
169
157
|
describe 'can be added' do
|
|
170
158
|
it { expect(playlist.add_video video_id).to be_a Yt::PlaylistItem }
|
|
@@ -206,18 +194,8 @@ describe Yt::Playlist, :device_app do
|
|
|
206
194
|
end
|
|
207
195
|
end
|
|
208
196
|
|
|
209
|
-
context 'given a video of a terminated account' do
|
|
210
|
-
let(:video_id) { 'kDCpdKeTe5g' }
|
|
211
|
-
|
|
212
|
-
describe 'cannot be added' do
|
|
213
|
-
it { expect(playlist.add_video video_id).to be_nil }
|
|
214
|
-
it { expect{playlist.add_video video_id}.not_to change{playlist.playlist_items.count} }
|
|
215
|
-
it { expect{playlist.add_video! video_id}.to fail.with 'forbidden' }
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
197
|
context 'given one existing and one unknown video' do
|
|
220
|
-
let(:video_ids) { ['
|
|
198
|
+
let(:video_ids) { ['9bZkp7q19f0', 'not-a-video'] }
|
|
221
199
|
|
|
222
200
|
describe 'only one can be added' do
|
|
223
201
|
it { expect(playlist.add_videos(video_ids).length).to eq 2 }
|
|
@@ -235,11 +213,6 @@ describe Yt::Playlist, :device_app do
|
|
|
235
213
|
expect{playlist.playlist_starts}.not_to raise_error
|
|
236
214
|
expect{playlist.average_time_in_playlist}.not_to raise_error
|
|
237
215
|
expect{playlist.views_per_playlist_start}.not_to raise_error
|
|
238
|
-
|
|
239
|
-
expect{playlist.views_on 3.days.ago}.not_to raise_error
|
|
240
|
-
expect{playlist.playlist_starts_on 3.days.ago}.not_to raise_error
|
|
241
|
-
expect{playlist.average_time_in_playlist_on 3.days.ago}.not_to raise_error
|
|
242
|
-
expect{playlist.views_per_playlist_start_on 3.days.ago}.not_to raise_error
|
|
243
216
|
end
|
|
244
217
|
end
|
|
245
218
|
end
|
|
@@ -7,7 +7,7 @@ describe Yt::Video, :device_app do
|
|
|
7
7
|
subject(:video) { Yt::Video.new id: id, auth: $account }
|
|
8
8
|
|
|
9
9
|
context 'given someone else’s video' do
|
|
10
|
-
let(:id) { '
|
|
10
|
+
let(:id) { '9bZkp7q19f0' }
|
|
11
11
|
|
|
12
12
|
it { expect(video.content_detail).to be_a Yt::ContentDetail }
|
|
13
13
|
|
|
@@ -20,6 +20,7 @@ describe Yt::Video, :device_app do
|
|
|
20
20
|
expect(video.tags).to be_an Array
|
|
21
21
|
expect(video.channel_id).to be_a String
|
|
22
22
|
expect(video.channel_title).to be_a String
|
|
23
|
+
expect(video.channel_url).to be_a String
|
|
23
24
|
expect(video.category_id).to be_a String
|
|
24
25
|
expect(video.live_broadcast_content).to be_a String
|
|
25
26
|
expect(video.view_count).to be_an Integer
|
|
@@ -28,6 +29,7 @@ describe Yt::Video, :device_app do
|
|
|
28
29
|
expect(video.favorite_count).to be_an Integer
|
|
29
30
|
expect(video.comment_count).to be_an Integer
|
|
30
31
|
expect(video.duration).to be_an Integer
|
|
32
|
+
expect(video.length).to be_a String
|
|
31
33
|
expect(video.hd?).to be_in [true, false]
|
|
32
34
|
expect(video.stereoscopic?).to be_in [true, false]
|
|
33
35
|
expect(video.captioned?).to be_in [true, false]
|
|
@@ -290,8 +292,6 @@ describe Yt::Video, :device_app do
|
|
|
290
292
|
expect{video.shares}.not_to raise_error
|
|
291
293
|
expect{video.subscribers_gained}.not_to raise_error
|
|
292
294
|
expect{video.subscribers_lost}.not_to raise_error
|
|
293
|
-
expect{video.favorites_added}.not_to raise_error
|
|
294
|
-
expect{video.favorites_removed}.not_to raise_error
|
|
295
295
|
expect{video.videos_added_to_playlists}.not_to raise_error
|
|
296
296
|
expect{video.videos_removed_from_playlists}.not_to raise_error
|
|
297
297
|
expect{video.estimated_minutes_watched}.not_to raise_error
|
|
@@ -300,29 +300,2030 @@ describe Yt::Video, :device_app do
|
|
|
300
300
|
expect{video.annotation_clicks}.not_to raise_error
|
|
301
301
|
expect{video.annotation_click_through_rate}.not_to raise_error
|
|
302
302
|
expect{video.annotation_close_rate}.not_to raise_error
|
|
303
|
+
expect{video.card_impressions}.not_to raise_error
|
|
304
|
+
expect{video.card_clicks}.not_to raise_error
|
|
305
|
+
expect{video.card_click_rate}.not_to raise_error
|
|
306
|
+
expect{video.card_teaser_impressions}.not_to raise_error
|
|
307
|
+
expect{video.card_teaser_clicks}.not_to raise_error
|
|
308
|
+
expect{video.card_teaser_click_rate}.not_to raise_error
|
|
303
309
|
expect{video.viewer_percentage}.not_to raise_error
|
|
304
|
-
expect{video.
|
|
305
|
-
expect{video.
|
|
310
|
+
expect{video.estimated_revenue}.to raise_error Yt::Errors::Unauthorized
|
|
311
|
+
expect{video.ad_impressions}.to raise_error Yt::Errors::Unauthorized
|
|
306
312
|
expect{video.monetized_playbacks}.to raise_error Yt::Errors::Unauthorized
|
|
307
313
|
expect{video.playback_based_cpm}.to raise_error Yt::Errors::Unauthorized
|
|
308
314
|
expect{video.advertising_options_set}.to raise_error Yt::Errors::Forbidden
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# @note: This test is separated from the block above because, for some
|
|
319
|
+
# undocumented reasons, if an existing video was private, then set to
|
|
320
|
+
# unlisted, then set to private again, YouTube _sometimes_ raises a
|
|
321
|
+
# 400 Error when trying to set the publishAt timestamp.
|
|
322
|
+
# Therefore, just to test the updating of publishAt, we use a brand new
|
|
323
|
+
# video (set to private), rather than reusing an existing one as above.
|
|
324
|
+
context 'given one of my own *private* videos that I want to update' do
|
|
325
|
+
before { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: old_title, privacy_status: old_privacy_status }
|
|
326
|
+
let(:id) { @tmp_video.id }
|
|
327
|
+
let!(:old_title) { "Yt Test Update publishAt Video #{rand}" }
|
|
328
|
+
let!(:old_privacy_status) { 'private' }
|
|
329
|
+
after { video.delete }
|
|
330
|
+
|
|
331
|
+
let!(:new_scheduled_at) { Yt::Timestamp.parse("#{rand(30) + 1} Jan 2020", Time.now) }
|
|
332
|
+
|
|
333
|
+
context 'passing the parameter in underscore syntax' do
|
|
334
|
+
let(:attrs) { {publish_at: new_scheduled_at} }
|
|
335
|
+
|
|
336
|
+
specify 'only updates the timestamp to publish the video' do
|
|
337
|
+
expect(video.update attrs).to be true
|
|
338
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
339
|
+
expect(video.title).to eq old_title
|
|
340
|
+
# NOTE: This is another irrational behavior of YouTube API. In short,
|
|
341
|
+
# the response of Video#update *does not* include the publishAt value
|
|
342
|
+
# even if it exists. You need to call Video#list again to get it.
|
|
343
|
+
video = Yt::Video.new id: id, auth: $account
|
|
344
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
345
|
+
# Setting a private (scheduled) video to private has no effect:
|
|
346
|
+
expect(video.update privacy_status: 'private').to be true
|
|
347
|
+
video = Yt::Video.new id: id, auth: $account
|
|
348
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
349
|
+
# Setting a private (scheduled) video to unlisted/public removes publishAt:
|
|
350
|
+
expect(video.update privacy_status: 'unlisted').to be true
|
|
351
|
+
video = Yt::Video.new id: id, auth: $account
|
|
352
|
+
expect(video.scheduled_at).to be_nil
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
context 'passing the parameter in camel-case syntax' do
|
|
357
|
+
let(:attrs) { {publishAt: new_scheduled_at} }
|
|
358
|
+
|
|
359
|
+
specify 'only updates the timestamp to publish the video' do
|
|
360
|
+
expect(video.update attrs).to be true
|
|
361
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
362
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
363
|
+
expect(video.title).to eq old_title
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# @note: This should somehow test that the thumbnail *changes*. However,
|
|
369
|
+
# YouTube does not change the URL of the thumbnail even though the content
|
|
370
|
+
# changes. A full test would have to *download* the thumbnails before and
|
|
371
|
+
# after, and compare the files. For now, not raising error is enough.
|
|
372
|
+
# Eventually, change to `expect{update}.to change{video.thumbnail_url}`
|
|
373
|
+
context 'given one of my own videos for which I want to upload a thumbnail' do
|
|
374
|
+
let(:id) { $account.videos.where(order: 'viewCount').first.id }
|
|
375
|
+
let(:update) { video.upload_thumbnail path_or_url }
|
|
376
|
+
|
|
377
|
+
context 'given the path to a local JPG image file' do
|
|
378
|
+
let(:path_or_url) { File.expand_path '../thumbnail.jpg', __FILE__ }
|
|
379
|
+
|
|
380
|
+
it { expect{update}.not_to raise_error }
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
context 'given the path to a remote PNG image file' do
|
|
384
|
+
let(:path_or_url) { 'https://bit.ly/yt_thumbnail' }
|
|
385
|
+
|
|
386
|
+
it { expect{update}.not_to raise_error }
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
context 'given an invalid URL' do
|
|
390
|
+
let(:path_or_url) { 'this-is-not-a-url' }
|
|
391
|
+
|
|
392
|
+
it { expect{update}.to raise_error Yt::Errors::RequestError }
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# @note: This test is separated from the block above because YouTube only
|
|
397
|
+
# returns file details for *some videos*: "The fileDetails object will
|
|
398
|
+
# only be returned if the processingDetails.fileAvailability property
|
|
399
|
+
# has a value of available.". Therefore, just to test fileDetails, we use a
|
|
400
|
+
# different video that (for some unknown reason) is marked as 'available'.
|
|
401
|
+
# Also note that I was not able to find a single video returning fileName,
|
|
402
|
+
# therefore video.file_name is not returned by Yt, until it can be tested.
|
|
403
|
+
# @see https://developers.google.com/youtube/v3/docs/videos#processingDetails.fileDetailsAvailability
|
|
404
|
+
context 'given one of my own *available* videos' do
|
|
405
|
+
let(:id) { 'yCmaOvUFhlI' }
|
|
406
|
+
|
|
407
|
+
it 'returns valid file details' do
|
|
408
|
+
expect(video.file_size).to be_an Integer
|
|
409
|
+
expect(video.file_type).to be_a String
|
|
410
|
+
expect(video.container).to be_a String
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
# encoding: UTF-8
|
|
415
|
+
|
|
416
|
+
require 'spec_helper'
|
|
417
|
+
require 'yt/models/video'
|
|
418
|
+
|
|
419
|
+
describe Yt::Video, :device_app do
|
|
420
|
+
subject(:video) { Yt::Video.new id: id, auth: $account }
|
|
421
|
+
|
|
422
|
+
context 'given someone else’s video' do
|
|
423
|
+
let(:id) { '9bZkp7q19f0' }
|
|
424
|
+
|
|
425
|
+
it { expect(video.content_detail).to be_a Yt::ContentDetail }
|
|
426
|
+
|
|
427
|
+
it 'returns valid metadata' do
|
|
428
|
+
expect(video.title).to be_a String
|
|
429
|
+
expect(video.description).to be_a String
|
|
430
|
+
expect(video.thumbnail_url).to be_a String
|
|
431
|
+
expect(video.published_at).to be_a Time
|
|
432
|
+
expect(video.privacy_status).to be_a String
|
|
433
|
+
expect(video.tags).to be_an Array
|
|
434
|
+
expect(video.channel_id).to be_a String
|
|
435
|
+
expect(video.channel_title).to be_a String
|
|
436
|
+
expect(video.channel_url).to be_a String
|
|
437
|
+
expect(video.category_id).to be_a String
|
|
438
|
+
expect(video.live_broadcast_content).to be_a String
|
|
439
|
+
expect(video.view_count).to be_an Integer
|
|
440
|
+
expect(video.like_count).to be_an Integer
|
|
441
|
+
expect(video.dislike_count).to be_an Integer
|
|
442
|
+
expect(video.favorite_count).to be_an Integer
|
|
443
|
+
expect(video.comment_count).to be_an Integer
|
|
444
|
+
expect(video.duration).to be_an Integer
|
|
445
|
+
expect(video.hd?).to be_in [true, false]
|
|
446
|
+
expect(video.stereoscopic?).to be_in [true, false]
|
|
447
|
+
expect(video.captioned?).to be_in [true, false]
|
|
448
|
+
expect(video.licensed?).to be_in [true, false]
|
|
449
|
+
expect(video.deleted?).to be_in [true, false]
|
|
450
|
+
expect(video.failed?).to be_in [true, false]
|
|
451
|
+
expect(video.processed?).to be_in [true, false]
|
|
452
|
+
expect(video.rejected?).to be_in [true, false]
|
|
453
|
+
expect(video.uploading?).to be_in [true, false]
|
|
454
|
+
expect(video.uses_unsupported_codec?).to be_in [true, false]
|
|
455
|
+
expect(video.has_failed_conversion?).to be_in [true, false]
|
|
456
|
+
expect(video.empty?).to be_in [true, false]
|
|
457
|
+
expect(video.invalid?).to be_in [true, false]
|
|
458
|
+
expect(video.too_small?).to be_in [true, false]
|
|
459
|
+
expect(video.aborted?).to be_in [true, false]
|
|
460
|
+
expect(video.claimed?).to be_in [true, false]
|
|
461
|
+
expect(video.infringes_copyright?).to be_in [true, false]
|
|
462
|
+
expect(video.duplicate?).to be_in [true, false]
|
|
463
|
+
expect(video.scheduled_at.class).to be_in [NilClass, Time]
|
|
464
|
+
expect(video.scheduled?).to be_in [true, false]
|
|
465
|
+
expect(video.too_long?).to be_in [true, false]
|
|
466
|
+
expect(video.violates_terms_of_use?).to be_in [true, false]
|
|
467
|
+
expect(video.inappropriate?).to be_in [true, false]
|
|
468
|
+
expect(video.infringes_trademark?).to be_in [true, false]
|
|
469
|
+
expect(video.belongs_to_closed_account?).to be_in [true, false]
|
|
470
|
+
expect(video.belongs_to_suspended_account?).to be_in [true, false]
|
|
471
|
+
expect(video.licensed_as_creative_commons?).to be_in [true, false]
|
|
472
|
+
expect(video.licensed_as_standard_youtube?).to be_in [true, false]
|
|
473
|
+
expect(video.has_public_stats_viewable?).to be_in [true, false]
|
|
474
|
+
expect(video.embeddable?).to be_in [true, false]
|
|
475
|
+
expect(video.actual_start_time).to be_nil
|
|
476
|
+
expect(video.actual_end_time).to be_nil
|
|
477
|
+
expect(video.scheduled_start_time).to be_nil
|
|
478
|
+
expect(video.scheduled_end_time).to be_nil
|
|
479
|
+
expect(video.concurrent_viewers).to be_nil
|
|
480
|
+
expect(video.embed_html).to be_a String
|
|
481
|
+
expect(video.category_title).to be_a String
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
it { expect{video.update}.to fail }
|
|
485
|
+
it { expect{video.delete}.to fail.with 'forbidden' }
|
|
486
|
+
|
|
487
|
+
context 'that I like' do
|
|
488
|
+
before { video.like }
|
|
489
|
+
it { expect(video).to be_liked }
|
|
490
|
+
it { expect(video.dislike).to be true }
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
context 'that I dislike' do
|
|
494
|
+
before { video.dislike }
|
|
495
|
+
it { expect(video).not_to be_liked }
|
|
496
|
+
it { expect(video.like).to be true }
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
context 'that I am indifferent to' do
|
|
500
|
+
before { video.unlike }
|
|
501
|
+
it { expect(video).not_to be_liked }
|
|
502
|
+
it { expect(video.like).to be true }
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
context 'given someone else’s live video broadcast scheduled in the future' do
|
|
507
|
+
let(:id) { 'PqzGI8gO_gk' }
|
|
508
|
+
|
|
509
|
+
it 'returns valid live streaming details' do
|
|
510
|
+
expect(video.actual_start_time).to be_nil
|
|
511
|
+
expect(video.actual_end_time).to be_nil
|
|
512
|
+
expect(video.scheduled_start_time).to be_a Time
|
|
513
|
+
expect(video.scheduled_end_time).to be_nil
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
context 'given someone else’s past live video broadcast' do
|
|
518
|
+
let(:id) { 'COOM8_tOy6U' }
|
|
519
|
+
|
|
520
|
+
it 'returns valid live streaming details' do
|
|
521
|
+
expect(video.actual_start_time).to be_a Time
|
|
522
|
+
expect(video.actual_end_time).to be_a Time
|
|
523
|
+
expect(video.scheduled_start_time).to be_a Time
|
|
524
|
+
expect(video.scheduled_end_time).to be_a Time
|
|
525
|
+
expect(video.concurrent_viewers).to be_nil
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
context 'given an unknown video' do
|
|
530
|
+
let(:id) { 'not-a-video-id' }
|
|
531
|
+
|
|
532
|
+
it { expect{video.content_detail}.to raise_error Yt::Errors::NoItems }
|
|
533
|
+
it { expect{video.snippet}.to raise_error Yt::Errors::NoItems }
|
|
534
|
+
it { expect{video.rating}.to raise_error Yt::Errors::NoItems }
|
|
535
|
+
it { expect{video.status}.to raise_error Yt::Errors::NoItems }
|
|
536
|
+
it { expect{video.statistics_set}.to raise_error Yt::Errors::NoItems }
|
|
537
|
+
it { expect{video.file_detail}.to raise_error Yt::Errors::NoItems }
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
context 'given one of my own videos that I want to delete' do
|
|
541
|
+
before(:all) { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: "Yt Test Delete Video #{rand}" }
|
|
542
|
+
let(:id) { @tmp_video.id }
|
|
543
|
+
|
|
544
|
+
it { expect(video.delete).to be true }
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
context 'given one of my own videos that I want to update' do
|
|
548
|
+
let(:id) { $account.videos.where(order: 'viewCount').first.id }
|
|
549
|
+
let!(:old_title) { video.title }
|
|
550
|
+
let!(:old_privacy_status) { video.privacy_status }
|
|
551
|
+
let(:update) { video.update attrs }
|
|
552
|
+
|
|
553
|
+
context 'given I update the title' do
|
|
554
|
+
# NOTE: The use of UTF-8 characters is to test that we can pass up to
|
|
555
|
+
# 50 characters, independently of their representation
|
|
556
|
+
let(:attrs) { {title: "Yt Example Update Video #{rand} - ®•♡❥❦❧☙"} }
|
|
557
|
+
|
|
558
|
+
specify 'only updates the title' do
|
|
559
|
+
expect(update).to be true
|
|
560
|
+
expect(video.title).not_to eq old_title
|
|
561
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
context 'given I update the description' do
|
|
566
|
+
let!(:old_description) { video.description }
|
|
567
|
+
let(:attrs) { {description: "Yt Example Description #{rand} - ®•♡❥❦❧☙"} }
|
|
568
|
+
|
|
569
|
+
specify 'only updates the description' do
|
|
570
|
+
expect(update).to be true
|
|
571
|
+
expect(video.description).not_to eq old_description
|
|
572
|
+
expect(video.title).to eq old_title
|
|
573
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
context 'given I update the tags' do
|
|
578
|
+
let!(:old_tags) { video.tags }
|
|
579
|
+
let(:attrs) { {tags: ["Yt Test Tag #{rand}"]} }
|
|
580
|
+
|
|
581
|
+
specify 'only updates the tag' do
|
|
582
|
+
expect(update).to be true
|
|
583
|
+
expect(video.tags).not_to eq old_tags
|
|
584
|
+
expect(video.title).to eq old_title
|
|
585
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
context 'given I update the category ID' do
|
|
590
|
+
let!(:old_category_id) { video.category_id }
|
|
591
|
+
let!(:new_category_id) { old_category_id == '22' ? '21' : '22' }
|
|
592
|
+
|
|
593
|
+
context 'passing the parameter in underscore syntax' do
|
|
594
|
+
let(:attrs) { {category_id: new_category_id} }
|
|
595
|
+
|
|
596
|
+
specify 'only updates the category ID' do
|
|
597
|
+
expect(update).to be true
|
|
598
|
+
expect(video.category_id).not_to eq old_category_id
|
|
599
|
+
expect(video.title).to eq old_title
|
|
600
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
context 'passing the parameter in camel-case syntax' do
|
|
605
|
+
let(:attrs) { {categoryId: new_category_id} }
|
|
606
|
+
|
|
607
|
+
specify 'only updates the category ID' do
|
|
608
|
+
expect(update).to be true
|
|
609
|
+
expect(video.category_id).not_to eq old_category_id
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
context 'given I update title, description and/or tags using angle brackets' do
|
|
615
|
+
let(:attrs) { {title: "Example Yt Test < >", description: '< >', tags: ['<tag>']} }
|
|
616
|
+
|
|
617
|
+
specify 'updates them replacing angle brackets with similar unicode characters accepted by YouTube' do
|
|
618
|
+
expect(update).to be true
|
|
619
|
+
expect(video.title).to eq 'Example Yt Test ‹ ›'
|
|
620
|
+
expect(video.description).to eq '‹ ›'
|
|
621
|
+
expect(video.tags).to eq ['‹tag›']
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# note: 'scheduled' videos cannot be set to 'unlisted'
|
|
626
|
+
context 'given I update the privacy status' do
|
|
627
|
+
before { video.update publish_at: nil if video.scheduled? }
|
|
628
|
+
let!(:new_privacy_status) { old_privacy_status == 'private' ? 'unlisted' : 'private' }
|
|
629
|
+
|
|
630
|
+
context 'passing the parameter in underscore syntax' do
|
|
631
|
+
let(:attrs) { {privacy_status: new_privacy_status} }
|
|
632
|
+
|
|
633
|
+
specify 'only updates the privacy status' do
|
|
634
|
+
expect(update).to be true
|
|
635
|
+
expect(video.privacy_status).not_to eq old_privacy_status
|
|
636
|
+
expect(video.title).to eq old_title
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
context 'passing the parameter in camel-case syntax' do
|
|
641
|
+
let(:attrs) { {privacyStatus: new_privacy_status} }
|
|
642
|
+
|
|
643
|
+
specify 'only updates the privacy status' do
|
|
644
|
+
expect(update).to be true
|
|
645
|
+
expect(video.privacy_status).not_to eq old_privacy_status
|
|
646
|
+
expect(video.title).to eq old_title
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
context 'given I update the embeddable status' do
|
|
652
|
+
let!(:old_embeddable) { video.embeddable? }
|
|
653
|
+
let!(:new_embeddable) { !old_embeddable }
|
|
654
|
+
|
|
655
|
+
let(:attrs) { {embeddable: new_embeddable} }
|
|
656
|
+
|
|
657
|
+
# @note: This test is a reflection of another irrational behavior of
|
|
658
|
+
# YouTube API. Although 'embeddable' can be passed as an 'update'
|
|
659
|
+
# attribute according to the documentation, it simply does not work.
|
|
660
|
+
# The day YouTube fixes it, then this test will finally fail and will
|
|
661
|
+
# be removed, documenting how to update 'embeddable' too.
|
|
662
|
+
# @see https://developers.google.com/youtube/v3/docs/videos/update
|
|
663
|
+
# @see https://code.google.com/p/gdata-issues/issues/detail?id=4861
|
|
664
|
+
specify 'does not update the embeddable status' do
|
|
665
|
+
expect(update).to be true
|
|
666
|
+
expect(video.embeddable?).to eq old_embeddable
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
context 'given I update the public stats viewable setting' do
|
|
671
|
+
let!(:old_public_stats_viewable) { video.has_public_stats_viewable? }
|
|
672
|
+
let!(:new_public_stats_viewable) { !old_public_stats_viewable }
|
|
673
|
+
|
|
674
|
+
context 'passing the parameter in underscore syntax' do
|
|
675
|
+
let(:attrs) { {public_stats_viewable: new_public_stats_viewable} }
|
|
676
|
+
|
|
677
|
+
specify 'only updates the public stats viewable setting' do
|
|
678
|
+
expect(update).to be true
|
|
679
|
+
expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
|
|
680
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
681
|
+
expect(video.title).to eq old_title
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
context 'passing the parameter in camel-case syntax' do
|
|
686
|
+
let(:attrs) { {publicStatsViewable: new_public_stats_viewable} }
|
|
687
|
+
|
|
688
|
+
specify 'only updates the public stats viewable setting' do
|
|
689
|
+
expect(update).to be true
|
|
690
|
+
expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
|
|
691
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
692
|
+
expect(video.title).to eq old_title
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
it 'returns valid reports for video-related metrics' do
|
|
698
|
+
# Some reports are only available to Content Owners.
|
|
699
|
+
# See content owner test for more details about what the methods return.
|
|
700
|
+
expect{video.views}.not_to raise_error
|
|
701
|
+
expect{video.comments}.not_to raise_error
|
|
702
|
+
expect{video.likes}.not_to raise_error
|
|
703
|
+
expect{video.dislikes}.not_to raise_error
|
|
704
|
+
expect{video.shares}.not_to raise_error
|
|
705
|
+
expect{video.subscribers_gained}.not_to raise_error
|
|
706
|
+
expect{video.subscribers_lost}.not_to raise_error
|
|
707
|
+
expect{video.videos_added_to_playlists}.not_to raise_error
|
|
708
|
+
expect{video.videos_removed_from_playlists}.not_to raise_error
|
|
709
|
+
expect{video.estimated_minutes_watched}.not_to raise_error
|
|
710
|
+
expect{video.average_view_duration}.not_to raise_error
|
|
711
|
+
expect{video.average_view_percentage}.not_to raise_error
|
|
712
|
+
expect{video.annotation_clicks}.not_to raise_error
|
|
713
|
+
expect{video.annotation_click_through_rate}.not_to raise_error
|
|
714
|
+
expect{video.annotation_close_rate}.not_to raise_error
|
|
715
|
+
expect{video.viewer_percentage}.not_to raise_error
|
|
716
|
+
expect{video.estimated_revenue}.to raise_error Yt::Errors::Unauthorized
|
|
717
|
+
expect{video.ad_impressions}.to raise_error Yt::Errors::Unauthorized
|
|
718
|
+
expect{video.monetized_playbacks}.to raise_error Yt::Errors::Unauthorized
|
|
719
|
+
expect{video.playback_based_cpm}.to raise_error Yt::Errors::Unauthorized
|
|
720
|
+
expect{video.advertising_options_set}.to raise_error Yt::Errors::Forbidden
|
|
721
|
+
end
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# @note: This test is separated from the block above because, for some
|
|
725
|
+
# undocumented reasons, if an existing video was private, then set to
|
|
726
|
+
# unlisted, then set to private again, YouTube _sometimes_ raises a
|
|
727
|
+
# 400 Error when trying to set the publishAt timestamp.
|
|
728
|
+
# Therefore, just to test the updating of publishAt, we use a brand new
|
|
729
|
+
# video (set to private), rather than reusing an existing one as above.
|
|
730
|
+
context 'given one of my own *private* videos that I want to update' do
|
|
731
|
+
before { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: old_title, privacy_status: old_privacy_status }
|
|
732
|
+
let(:id) { @tmp_video.id }
|
|
733
|
+
let!(:old_title) { "Yt Test Update publishAt Video #{rand}" }
|
|
734
|
+
let!(:old_privacy_status) { 'private' }
|
|
735
|
+
after { video.delete }
|
|
736
|
+
|
|
737
|
+
let!(:new_scheduled_at) { Yt::Timestamp.parse("#{rand(30) + 1} Jan 2020", Time.now) }
|
|
738
|
+
|
|
739
|
+
context 'passing the parameter in underscore syntax' do
|
|
740
|
+
let(:attrs) { {publish_at: new_scheduled_at} }
|
|
741
|
+
|
|
742
|
+
specify 'only updates the timestamp to publish the video' do
|
|
743
|
+
expect(video.update attrs).to be true
|
|
744
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
745
|
+
expect(video.title).to eq old_title
|
|
746
|
+
# NOTE: This is another irrational behavior of YouTube API. In short,
|
|
747
|
+
# the response of Video#update *does not* include the publishAt value
|
|
748
|
+
# even if it exists. You need to call Video#list again to get it.
|
|
749
|
+
video = Yt::Video.new id: id, auth: $account
|
|
750
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
751
|
+
# Setting a private (scheduled) video to private has no effect:
|
|
752
|
+
expect(video.update privacy_status: 'private').to be true
|
|
753
|
+
video = Yt::Video.new id: id, auth: $account
|
|
754
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
755
|
+
# Setting a private (scheduled) video to unlisted/public removes publishAt:
|
|
756
|
+
expect(video.update privacy_status: 'unlisted').to be true
|
|
757
|
+
video = Yt::Video.new id: id, auth: $account
|
|
758
|
+
expect(video.scheduled_at).to be_nil
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
context 'passing the parameter in camel-case syntax' do
|
|
763
|
+
let(:attrs) { {publishAt: new_scheduled_at} }
|
|
764
|
+
|
|
765
|
+
specify 'only updates the timestamp to publish the video' do
|
|
766
|
+
expect(video.update attrs).to be true
|
|
767
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
768
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
769
|
+
expect(video.title).to eq old_title
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# @note: This should somehow test that the thumbnail *changes*. However,
|
|
775
|
+
# YouTube does not change the URL of the thumbnail even though the content
|
|
776
|
+
# changes. A full test would have to *download* the thumbnails before and
|
|
777
|
+
# after, and compare the files. For now, not raising error is enough.
|
|
778
|
+
# Eventually, change to `expect{update}.to change{video.thumbnail_url}`
|
|
779
|
+
context 'given one of my own videos for which I want to upload a thumbnail' do
|
|
780
|
+
let(:id) { $account.videos.where(order: 'viewCount').first.id }
|
|
781
|
+
let(:update) { video.upload_thumbnail path_or_url }
|
|
782
|
+
|
|
783
|
+
context 'given the path to a local JPG image file' do
|
|
784
|
+
let(:path_or_url) { File.expand_path '../thumbnail.jpg', __FILE__ }
|
|
785
|
+
|
|
786
|
+
it { expect{update}.not_to raise_error }
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
context 'given the path to a remote PNG image file' do
|
|
790
|
+
let(:path_or_url) { 'https://bit.ly/yt_thumbnail' }
|
|
791
|
+
|
|
792
|
+
it { expect{update}.not_to raise_error }
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
context 'given an invalid URL' do
|
|
796
|
+
let(:path_or_url) { 'this-is-not-a-url' }
|
|
797
|
+
|
|
798
|
+
it { expect{update}.to raise_error Yt::Errors::RequestError }
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# @note: This test is separated from the block above because YouTube only
|
|
803
|
+
# returns file details for *some videos*: "The fileDetails object will
|
|
804
|
+
# only be returned if the processingDetails.fileAvailability property
|
|
805
|
+
# has a value of available.". Therefore, just to test fileDetails, we use a
|
|
806
|
+
# different video that (for some unknown reason) is marked as 'available'.
|
|
807
|
+
# Also note that I was not able to find a single video returning fileName,
|
|
808
|
+
# therefore video.file_name is not returned by Yt, until it can be tested.
|
|
809
|
+
# @see https://developers.google.com/youtube/v3/docs/videos#processingDetails.fileDetailsAvailability
|
|
810
|
+
context 'given one of my own *available* videos' do
|
|
811
|
+
let(:id) { 'yCmaOvUFhlI' }
|
|
812
|
+
|
|
813
|
+
it 'returns valid file details' do
|
|
814
|
+
expect(video.file_size).to be_an Integer
|
|
815
|
+
expect(video.file_type).to be_a String
|
|
816
|
+
expect(video.container).to be_a String
|
|
817
|
+
end
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
# encoding: UTF-8
|
|
821
|
+
|
|
822
|
+
require 'spec_helper'
|
|
823
|
+
require 'yt/models/video'
|
|
824
|
+
|
|
825
|
+
describe Yt::Video, :device_app do
|
|
826
|
+
subject(:video) { Yt::Video.new id: id, auth: $account }
|
|
827
|
+
|
|
828
|
+
context 'given someone else’s video' do
|
|
829
|
+
let(:id) { '9bZkp7q19f0' }
|
|
830
|
+
|
|
831
|
+
it { expect(video.content_detail).to be_a Yt::ContentDetail }
|
|
832
|
+
|
|
833
|
+
it 'returns valid metadata' do
|
|
834
|
+
expect(video.title).to be_a String
|
|
835
|
+
expect(video.description).to be_a String
|
|
836
|
+
expect(video.thumbnail_url).to be_a String
|
|
837
|
+
expect(video.published_at).to be_a Time
|
|
838
|
+
expect(video.privacy_status).to be_a String
|
|
839
|
+
expect(video.tags).to be_an Array
|
|
840
|
+
expect(video.channel_id).to be_a String
|
|
841
|
+
expect(video.channel_title).to be_a String
|
|
842
|
+
expect(video.channel_url).to be_a String
|
|
843
|
+
expect(video.category_id).to be_a String
|
|
844
|
+
expect(video.live_broadcast_content).to be_a String
|
|
845
|
+
expect(video.view_count).to be_an Integer
|
|
846
|
+
expect(video.like_count).to be_an Integer
|
|
847
|
+
expect(video.dislike_count).to be_an Integer
|
|
848
|
+
expect(video.favorite_count).to be_an Integer
|
|
849
|
+
expect(video.comment_count).to be_an Integer
|
|
850
|
+
expect(video.duration).to be_an Integer
|
|
851
|
+
expect(video.hd?).to be_in [true, false]
|
|
852
|
+
expect(video.stereoscopic?).to be_in [true, false]
|
|
853
|
+
expect(video.captioned?).to be_in [true, false]
|
|
854
|
+
expect(video.licensed?).to be_in [true, false]
|
|
855
|
+
expect(video.deleted?).to be_in [true, false]
|
|
856
|
+
expect(video.failed?).to be_in [true, false]
|
|
857
|
+
expect(video.processed?).to be_in [true, false]
|
|
858
|
+
expect(video.rejected?).to be_in [true, false]
|
|
859
|
+
expect(video.uploading?).to be_in [true, false]
|
|
860
|
+
expect(video.uses_unsupported_codec?).to be_in [true, false]
|
|
861
|
+
expect(video.has_failed_conversion?).to be_in [true, false]
|
|
862
|
+
expect(video.empty?).to be_in [true, false]
|
|
863
|
+
expect(video.invalid?).to be_in [true, false]
|
|
864
|
+
expect(video.too_small?).to be_in [true, false]
|
|
865
|
+
expect(video.aborted?).to be_in [true, false]
|
|
866
|
+
expect(video.claimed?).to be_in [true, false]
|
|
867
|
+
expect(video.infringes_copyright?).to be_in [true, false]
|
|
868
|
+
expect(video.duplicate?).to be_in [true, false]
|
|
869
|
+
expect(video.scheduled_at.class).to be_in [NilClass, Time]
|
|
870
|
+
expect(video.scheduled?).to be_in [true, false]
|
|
871
|
+
expect(video.too_long?).to be_in [true, false]
|
|
872
|
+
expect(video.violates_terms_of_use?).to be_in [true, false]
|
|
873
|
+
expect(video.inappropriate?).to be_in [true, false]
|
|
874
|
+
expect(video.infringes_trademark?).to be_in [true, false]
|
|
875
|
+
expect(video.belongs_to_closed_account?).to be_in [true, false]
|
|
876
|
+
expect(video.belongs_to_suspended_account?).to be_in [true, false]
|
|
877
|
+
expect(video.licensed_as_creative_commons?).to be_in [true, false]
|
|
878
|
+
expect(video.licensed_as_standard_youtube?).to be_in [true, false]
|
|
879
|
+
expect(video.has_public_stats_viewable?).to be_in [true, false]
|
|
880
|
+
expect(video.embeddable?).to be_in [true, false]
|
|
881
|
+
expect(video.actual_start_time).to be_nil
|
|
882
|
+
expect(video.actual_end_time).to be_nil
|
|
883
|
+
expect(video.scheduled_start_time).to be_nil
|
|
884
|
+
expect(video.scheduled_end_time).to be_nil
|
|
885
|
+
expect(video.concurrent_viewers).to be_nil
|
|
886
|
+
expect(video.embed_html).to be_a String
|
|
887
|
+
expect(video.category_title).to be_a String
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
it { expect{video.update}.to fail }
|
|
891
|
+
it { expect{video.delete}.to fail.with 'forbidden' }
|
|
892
|
+
|
|
893
|
+
context 'that I like' do
|
|
894
|
+
before { video.like }
|
|
895
|
+
it { expect(video).to be_liked }
|
|
896
|
+
it { expect(video.dislike).to be true }
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
context 'that I dislike' do
|
|
900
|
+
before { video.dislike }
|
|
901
|
+
it { expect(video).not_to be_liked }
|
|
902
|
+
it { expect(video.like).to be true }
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
context 'that I am indifferent to' do
|
|
906
|
+
before { video.unlike }
|
|
907
|
+
it { expect(video).not_to be_liked }
|
|
908
|
+
it { expect(video.like).to be true }
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
context 'given someone else’s live video broadcast scheduled in the future' do
|
|
913
|
+
let(:id) { 'PqzGI8gO_gk' }
|
|
914
|
+
|
|
915
|
+
it 'returns valid live streaming details' do
|
|
916
|
+
expect(video.actual_start_time).to be_nil
|
|
917
|
+
expect(video.actual_end_time).to be_nil
|
|
918
|
+
expect(video.scheduled_start_time).to be_a Time
|
|
919
|
+
expect(video.scheduled_end_time).to be_nil
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
context 'given someone else’s past live video broadcast' do
|
|
924
|
+
let(:id) { 'COOM8_tOy6U' }
|
|
925
|
+
|
|
926
|
+
it 'returns valid live streaming details' do
|
|
927
|
+
expect(video.actual_start_time).to be_a Time
|
|
928
|
+
expect(video.actual_end_time).to be_a Time
|
|
929
|
+
expect(video.scheduled_start_time).to be_a Time
|
|
930
|
+
expect(video.scheduled_end_time).to be_a Time
|
|
931
|
+
expect(video.concurrent_viewers).to be_nil
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
context 'given an unknown video' do
|
|
936
|
+
let(:id) { 'not-a-video-id' }
|
|
937
|
+
|
|
938
|
+
it { expect{video.content_detail}.to raise_error Yt::Errors::NoItems }
|
|
939
|
+
it { expect{video.snippet}.to raise_error Yt::Errors::NoItems }
|
|
940
|
+
it { expect{video.rating}.to raise_error Yt::Errors::NoItems }
|
|
941
|
+
it { expect{video.status}.to raise_error Yt::Errors::NoItems }
|
|
942
|
+
it { expect{video.statistics_set}.to raise_error Yt::Errors::NoItems }
|
|
943
|
+
it { expect{video.file_detail}.to raise_error Yt::Errors::NoItems }
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
context 'given one of my own videos that I want to delete' do
|
|
947
|
+
before(:all) { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: "Yt Test Delete Video #{rand}" }
|
|
948
|
+
let(:id) { @tmp_video.id }
|
|
949
|
+
|
|
950
|
+
it { expect(video.delete).to be true }
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
context 'given one of my own videos that I want to update' do
|
|
954
|
+
let(:id) { $account.videos.where(order: 'viewCount').first.id }
|
|
955
|
+
let!(:old_title) { video.title }
|
|
956
|
+
let!(:old_privacy_status) { video.privacy_status }
|
|
957
|
+
let(:update) { video.update attrs }
|
|
958
|
+
|
|
959
|
+
context 'given I update the title' do
|
|
960
|
+
# NOTE: The use of UTF-8 characters is to test that we can pass up to
|
|
961
|
+
# 50 characters, independently of their representation
|
|
962
|
+
let(:attrs) { {title: "Yt Example Update Video #{rand} - ®•♡❥❦❧☙"} }
|
|
963
|
+
|
|
964
|
+
specify 'only updates the title' do
|
|
965
|
+
expect(update).to be true
|
|
966
|
+
expect(video.title).not_to eq old_title
|
|
967
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
context 'given I update the description' do
|
|
972
|
+
let!(:old_description) { video.description }
|
|
973
|
+
let(:attrs) { {description: "Yt Example Description #{rand} - ®•♡❥❦❧☙"} }
|
|
974
|
+
|
|
975
|
+
specify 'only updates the description' do
|
|
976
|
+
expect(update).to be true
|
|
977
|
+
expect(video.description).not_to eq old_description
|
|
978
|
+
expect(video.title).to eq old_title
|
|
979
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
980
|
+
end
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
context 'given I update the tags' do
|
|
984
|
+
let!(:old_tags) { video.tags }
|
|
985
|
+
let(:attrs) { {tags: ["Yt Test Tag #{rand}"]} }
|
|
986
|
+
|
|
987
|
+
specify 'only updates the tag' do
|
|
988
|
+
expect(update).to be true
|
|
989
|
+
expect(video.tags).not_to eq old_tags
|
|
990
|
+
expect(video.title).to eq old_title
|
|
991
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
992
|
+
end
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
context 'given I update the category ID' do
|
|
996
|
+
let!(:old_category_id) { video.category_id }
|
|
997
|
+
let!(:new_category_id) { old_category_id == '22' ? '21' : '22' }
|
|
998
|
+
|
|
999
|
+
context 'passing the parameter in underscore syntax' do
|
|
1000
|
+
let(:attrs) { {category_id: new_category_id} }
|
|
1001
|
+
|
|
1002
|
+
specify 'only updates the category ID' do
|
|
1003
|
+
expect(update).to be true
|
|
1004
|
+
expect(video.category_id).not_to eq old_category_id
|
|
1005
|
+
expect(video.title).to eq old_title
|
|
1006
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1007
|
+
end
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1011
|
+
let(:attrs) { {categoryId: new_category_id} }
|
|
1012
|
+
|
|
1013
|
+
specify 'only updates the category ID' do
|
|
1014
|
+
expect(update).to be true
|
|
1015
|
+
expect(video.category_id).not_to eq old_category_id
|
|
1016
|
+
end
|
|
1017
|
+
end
|
|
1018
|
+
end
|
|
1019
|
+
|
|
1020
|
+
context 'given I update title, description and/or tags using angle brackets' do
|
|
1021
|
+
let(:attrs) { {title: "Example Yt Test < >", description: '< >', tags: ['<tag>']} }
|
|
1022
|
+
|
|
1023
|
+
specify 'updates them replacing angle brackets with similar unicode characters accepted by YouTube' do
|
|
1024
|
+
expect(update).to be true
|
|
1025
|
+
expect(video.title).to eq 'Example Yt Test ‹ ›'
|
|
1026
|
+
expect(video.description).to eq '‹ ›'
|
|
1027
|
+
expect(video.tags).to eq ['‹tag›']
|
|
1028
|
+
end
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
# note: 'scheduled' videos cannot be set to 'unlisted'
|
|
1032
|
+
context 'given I update the privacy status' do
|
|
1033
|
+
before { video.update publish_at: nil if video.scheduled? }
|
|
1034
|
+
let!(:new_privacy_status) { old_privacy_status == 'private' ? 'unlisted' : 'private' }
|
|
1035
|
+
|
|
1036
|
+
context 'passing the parameter in underscore syntax' do
|
|
1037
|
+
let(:attrs) { {privacy_status: new_privacy_status} }
|
|
1038
|
+
|
|
1039
|
+
specify 'only updates the privacy status' do
|
|
1040
|
+
expect(update).to be true
|
|
1041
|
+
expect(video.privacy_status).not_to eq old_privacy_status
|
|
1042
|
+
expect(video.title).to eq old_title
|
|
1043
|
+
end
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1047
|
+
let(:attrs) { {privacyStatus: new_privacy_status} }
|
|
1048
|
+
|
|
1049
|
+
specify 'only updates the privacy status' do
|
|
1050
|
+
expect(update).to be true
|
|
1051
|
+
expect(video.privacy_status).not_to eq old_privacy_status
|
|
1052
|
+
expect(video.title).to eq old_title
|
|
1053
|
+
end
|
|
1054
|
+
end
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
context 'given I update the public stats viewable setting' do
|
|
1058
|
+
let!(:old_public_stats_viewable) { video.has_public_stats_viewable? }
|
|
1059
|
+
let!(:new_public_stats_viewable) { !old_public_stats_viewable }
|
|
1060
|
+
|
|
1061
|
+
context 'passing the parameter in underscore syntax' do
|
|
1062
|
+
let(:attrs) { {public_stats_viewable: new_public_stats_viewable} }
|
|
1063
|
+
|
|
1064
|
+
specify 'only updates the public stats viewable setting' do
|
|
1065
|
+
expect(update).to be true
|
|
1066
|
+
expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
|
|
1067
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1068
|
+
expect(video.title).to eq old_title
|
|
1069
|
+
end
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1073
|
+
let(:attrs) { {publicStatsViewable: new_public_stats_viewable} }
|
|
1074
|
+
|
|
1075
|
+
specify 'only updates the public stats viewable setting' do
|
|
1076
|
+
expect(update).to be true
|
|
1077
|
+
expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
|
|
1078
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1079
|
+
expect(video.title).to eq old_title
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
it 'returns valid reports for video-related metrics' do
|
|
1085
|
+
# Some reports are only available to Content Owners.
|
|
1086
|
+
# See content owner test for more details about what the methods return.
|
|
1087
|
+
expect{video.views}.not_to raise_error
|
|
1088
|
+
expect{video.comments}.not_to raise_error
|
|
1089
|
+
expect{video.likes}.not_to raise_error
|
|
1090
|
+
expect{video.dislikes}.not_to raise_error
|
|
1091
|
+
expect{video.shares}.not_to raise_error
|
|
1092
|
+
expect{video.subscribers_gained}.not_to raise_error
|
|
1093
|
+
expect{video.subscribers_lost}.not_to raise_error
|
|
1094
|
+
expect{video.videos_added_to_playlists}.not_to raise_error
|
|
1095
|
+
expect{video.videos_removed_from_playlists}.not_to raise_error
|
|
1096
|
+
expect{video.estimated_minutes_watched}.not_to raise_error
|
|
1097
|
+
expect{video.average_view_duration}.not_to raise_error
|
|
1098
|
+
expect{video.average_view_percentage}.not_to raise_error
|
|
1099
|
+
expect{video.annotation_clicks}.not_to raise_error
|
|
1100
|
+
expect{video.annotation_click_through_rate}.not_to raise_error
|
|
1101
|
+
expect{video.annotation_close_rate}.not_to raise_error
|
|
1102
|
+
expect{video.viewer_percentage}.not_to raise_error
|
|
1103
|
+
expect{video.estimated_revenue}.to raise_error Yt::Errors::Unauthorized
|
|
1104
|
+
expect{video.ad_impressions}.to raise_error Yt::Errors::Unauthorized
|
|
1105
|
+
expect{video.monetized_playbacks}.to raise_error Yt::Errors::Unauthorized
|
|
1106
|
+
expect{video.playback_based_cpm}.to raise_error Yt::Errors::Unauthorized
|
|
1107
|
+
expect{video.advertising_options_set}.to raise_error Yt::Errors::Forbidden
|
|
1108
|
+
end
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
# @note: This test is separated from the block above because, for some
|
|
1112
|
+
# undocumented reasons, if an existing video was private, then set to
|
|
1113
|
+
# unlisted, then set to private again, YouTube _sometimes_ raises a
|
|
1114
|
+
# 400 Error when trying to set the publishAt timestamp.
|
|
1115
|
+
# Therefore, just to test the updating of publishAt, we use a brand new
|
|
1116
|
+
# video (set to private), rather than reusing an existing one as above.
|
|
1117
|
+
context 'given one of my own *private* videos that I want to update' do
|
|
1118
|
+
before { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: old_title, privacy_status: old_privacy_status }
|
|
1119
|
+
let(:id) { @tmp_video.id }
|
|
1120
|
+
let!(:old_title) { "Yt Test Update publishAt Video #{rand}" }
|
|
1121
|
+
let!(:old_privacy_status) { 'private' }
|
|
1122
|
+
after { video.delete }
|
|
1123
|
+
|
|
1124
|
+
let!(:new_scheduled_at) { Yt::Timestamp.parse("#{rand(30) + 1} Jan 2020", Time.now) }
|
|
1125
|
+
|
|
1126
|
+
context 'passing the parameter in underscore syntax' do
|
|
1127
|
+
let(:attrs) { {publish_at: new_scheduled_at} }
|
|
1128
|
+
|
|
1129
|
+
specify 'only updates the timestamp to publish the video' do
|
|
1130
|
+
expect(video.update attrs).to be true
|
|
1131
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1132
|
+
expect(video.title).to eq old_title
|
|
1133
|
+
# NOTE: This is another irrational behavior of YouTube API. In short,
|
|
1134
|
+
# the response of Video#update *does not* include the publishAt value
|
|
1135
|
+
# even if it exists. You need to call Video#list again to get it.
|
|
1136
|
+
video = Yt::Video.new id: id, auth: $account
|
|
1137
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
1138
|
+
# Setting a private (scheduled) video to private has no effect:
|
|
1139
|
+
expect(video.update privacy_status: 'private').to be true
|
|
1140
|
+
video = Yt::Video.new id: id, auth: $account
|
|
1141
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
1142
|
+
# Setting a private (scheduled) video to unlisted/public removes publishAt:
|
|
1143
|
+
expect(video.update privacy_status: 'unlisted').to be true
|
|
1144
|
+
video = Yt::Video.new id: id, auth: $account
|
|
1145
|
+
expect(video.scheduled_at).to be_nil
|
|
1146
|
+
end
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1150
|
+
let(:attrs) { {publishAt: new_scheduled_at} }
|
|
1151
|
+
|
|
1152
|
+
specify 'only updates the timestamp to publish the video' do
|
|
1153
|
+
expect(video.update attrs).to be true
|
|
1154
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
1155
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1156
|
+
expect(video.title).to eq old_title
|
|
1157
|
+
end
|
|
1158
|
+
end
|
|
1159
|
+
end
|
|
1160
|
+
|
|
1161
|
+
# @note: This should somehow test that the thumbnail *changes*. However,
|
|
1162
|
+
# YouTube does not change the URL of the thumbnail even though the content
|
|
1163
|
+
# changes. A full test would have to *download* the thumbnails before and
|
|
1164
|
+
# after, and compare the files. For now, not raising error is enough.
|
|
1165
|
+
# Eventually, change to `expect{update}.to change{video.thumbnail_url}`
|
|
1166
|
+
context 'given one of my own videos for which I want to upload a thumbnail' do
|
|
1167
|
+
let(:id) { $account.videos.where(order: 'viewCount').first.id }
|
|
1168
|
+
let(:update) { video.upload_thumbnail path_or_url }
|
|
1169
|
+
|
|
1170
|
+
context 'given the path to a local JPG image file' do
|
|
1171
|
+
let(:path_or_url) { File.expand_path '../thumbnail.jpg', __FILE__ }
|
|
1172
|
+
|
|
1173
|
+
it { expect{update}.not_to raise_error }
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
context 'given the path to a remote PNG image file' do
|
|
1177
|
+
let(:path_or_url) { 'https://bit.ly/yt_thumbnail' }
|
|
1178
|
+
|
|
1179
|
+
it { expect{update}.not_to raise_error }
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
context 'given an invalid URL' do
|
|
1183
|
+
let(:path_or_url) { 'this-is-not-a-url' }
|
|
1184
|
+
|
|
1185
|
+
it { expect{update}.to raise_error Yt::Errors::RequestError }
|
|
1186
|
+
end
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
# @note: This test is separated from the block above because YouTube only
|
|
1190
|
+
# returns file details for *some videos*: "The fileDetails object will
|
|
1191
|
+
# only be returned if the processingDetails.fileAvailability property
|
|
1192
|
+
# has a value of available.". Therefore, just to test fileDetails, we use a
|
|
1193
|
+
# different video that (for some unknown reason) is marked as 'available'.
|
|
1194
|
+
# Also note that I was not able to find a single video returning fileName,
|
|
1195
|
+
# therefore video.file_name is not returned by Yt, until it can be tested.
|
|
1196
|
+
# @see https://developers.google.com/youtube/v3/docs/videos#processingDetails.fileDetailsAvailability
|
|
1197
|
+
context 'given one of my own *available* videos' do
|
|
1198
|
+
let(:id) { 'yCmaOvUFhlI' }
|
|
1199
|
+
|
|
1200
|
+
it 'returns valid file details' do
|
|
1201
|
+
expect(video.file_size).to be_an Integer
|
|
1202
|
+
expect(video.file_type).to be_a String
|
|
1203
|
+
expect(video.container).to be_a String
|
|
1204
|
+
end
|
|
1205
|
+
end
|
|
1206
|
+
end
|
|
1207
|
+
# encoding: UTF-8
|
|
1208
|
+
|
|
1209
|
+
require 'spec_helper'
|
|
1210
|
+
require 'yt/models/video'
|
|
1211
|
+
|
|
1212
|
+
describe Yt::Video, :device_app do
|
|
1213
|
+
subject(:video) { Yt::Video.new id: id, auth: $account }
|
|
1214
|
+
|
|
1215
|
+
context 'given someone else’s video' do
|
|
1216
|
+
let(:id) { '9bZkp7q19f0' }
|
|
1217
|
+
|
|
1218
|
+
it { expect(video.content_detail).to be_a Yt::ContentDetail }
|
|
1219
|
+
|
|
1220
|
+
it 'returns valid metadata' do
|
|
1221
|
+
expect(video.title).to be_a String
|
|
1222
|
+
expect(video.description).to be_a String
|
|
1223
|
+
expect(video.thumbnail_url).to be_a String
|
|
1224
|
+
expect(video.published_at).to be_a Time
|
|
1225
|
+
expect(video.privacy_status).to be_a String
|
|
1226
|
+
expect(video.tags).to be_an Array
|
|
1227
|
+
expect(video.channel_id).to be_a String
|
|
1228
|
+
expect(video.channel_title).to be_a String
|
|
1229
|
+
expect(video.channel_url).to be_a String
|
|
1230
|
+
expect(video.category_id).to be_a String
|
|
1231
|
+
expect(video.live_broadcast_content).to be_a String
|
|
1232
|
+
expect(video.view_count).to be_an Integer
|
|
1233
|
+
expect(video.like_count).to be_an Integer
|
|
1234
|
+
expect(video.dislike_count).to be_an Integer
|
|
1235
|
+
expect(video.favorite_count).to be_an Integer
|
|
1236
|
+
expect(video.comment_count).to be_an Integer
|
|
1237
|
+
expect(video.duration).to be_an Integer
|
|
1238
|
+
expect(video.hd?).to be_in [true, false]
|
|
1239
|
+
expect(video.stereoscopic?).to be_in [true, false]
|
|
1240
|
+
expect(video.captioned?).to be_in [true, false]
|
|
1241
|
+
expect(video.licensed?).to be_in [true, false]
|
|
1242
|
+
expect(video.deleted?).to be_in [true, false]
|
|
1243
|
+
expect(video.failed?).to be_in [true, false]
|
|
1244
|
+
expect(video.processed?).to be_in [true, false]
|
|
1245
|
+
expect(video.rejected?).to be_in [true, false]
|
|
1246
|
+
expect(video.uploading?).to be_in [true, false]
|
|
1247
|
+
expect(video.uses_unsupported_codec?).to be_in [true, false]
|
|
1248
|
+
expect(video.has_failed_conversion?).to be_in [true, false]
|
|
1249
|
+
expect(video.empty?).to be_in [true, false]
|
|
1250
|
+
expect(video.invalid?).to be_in [true, false]
|
|
1251
|
+
expect(video.too_small?).to be_in [true, false]
|
|
1252
|
+
expect(video.aborted?).to be_in [true, false]
|
|
1253
|
+
expect(video.claimed?).to be_in [true, false]
|
|
1254
|
+
expect(video.infringes_copyright?).to be_in [true, false]
|
|
1255
|
+
expect(video.duplicate?).to be_in [true, false]
|
|
1256
|
+
expect(video.scheduled_at.class).to be_in [NilClass, Time]
|
|
1257
|
+
expect(video.scheduled?).to be_in [true, false]
|
|
1258
|
+
expect(video.too_long?).to be_in [true, false]
|
|
1259
|
+
expect(video.violates_terms_of_use?).to be_in [true, false]
|
|
1260
|
+
expect(video.inappropriate?).to be_in [true, false]
|
|
1261
|
+
expect(video.infringes_trademark?).to be_in [true, false]
|
|
1262
|
+
expect(video.belongs_to_closed_account?).to be_in [true, false]
|
|
1263
|
+
expect(video.belongs_to_suspended_account?).to be_in [true, false]
|
|
1264
|
+
expect(video.licensed_as_creative_commons?).to be_in [true, false]
|
|
1265
|
+
expect(video.licensed_as_standard_youtube?).to be_in [true, false]
|
|
1266
|
+
expect(video.has_public_stats_viewable?).to be_in [true, false]
|
|
1267
|
+
expect(video.embeddable?).to be_in [true, false]
|
|
1268
|
+
expect(video.actual_start_time).to be_nil
|
|
1269
|
+
expect(video.actual_end_time).to be_nil
|
|
1270
|
+
expect(video.scheduled_start_time).to be_nil
|
|
1271
|
+
expect(video.scheduled_end_time).to be_nil
|
|
1272
|
+
expect(video.concurrent_viewers).to be_nil
|
|
1273
|
+
expect(video.embed_html).to be_a String
|
|
1274
|
+
expect(video.category_title).to be_a String
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
it { expect{video.update}.to fail }
|
|
1278
|
+
it { expect{video.delete}.to fail.with 'forbidden' }
|
|
1279
|
+
|
|
1280
|
+
context 'that I like' do
|
|
1281
|
+
before { video.like }
|
|
1282
|
+
it { expect(video).to be_liked }
|
|
1283
|
+
it { expect(video.dislike).to be true }
|
|
1284
|
+
end
|
|
1285
|
+
|
|
1286
|
+
context 'that I dislike' do
|
|
1287
|
+
before { video.dislike }
|
|
1288
|
+
it { expect(video).not_to be_liked }
|
|
1289
|
+
it { expect(video.like).to be true }
|
|
1290
|
+
end
|
|
1291
|
+
|
|
1292
|
+
context 'that I am indifferent to' do
|
|
1293
|
+
before { video.unlike }
|
|
1294
|
+
it { expect(video).not_to be_liked }
|
|
1295
|
+
it { expect(video.like).to be true }
|
|
1296
|
+
end
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
context 'given someone else’s live video broadcast scheduled in the future' do
|
|
1300
|
+
let(:id) { 'PqzGI8gO_gk' }
|
|
1301
|
+
|
|
1302
|
+
it 'returns valid live streaming details' do
|
|
1303
|
+
expect(video.actual_start_time).to be_nil
|
|
1304
|
+
expect(video.actual_end_time).to be_nil
|
|
1305
|
+
expect(video.scheduled_start_time).to be_a Time
|
|
1306
|
+
expect(video.scheduled_end_time).to be_nil
|
|
1307
|
+
end
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
context 'given someone else’s past live video broadcast' do
|
|
1311
|
+
let(:id) { 'COOM8_tOy6U' }
|
|
1312
|
+
|
|
1313
|
+
it 'returns valid live streaming details' do
|
|
1314
|
+
expect(video.actual_start_time).to be_a Time
|
|
1315
|
+
expect(video.actual_end_time).to be_a Time
|
|
1316
|
+
expect(video.scheduled_start_time).to be_a Time
|
|
1317
|
+
expect(video.scheduled_end_time).to be_a Time
|
|
1318
|
+
expect(video.concurrent_viewers).to be_nil
|
|
1319
|
+
end
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
context 'given an unknown video' do
|
|
1323
|
+
let(:id) { 'not-a-video-id' }
|
|
1324
|
+
|
|
1325
|
+
it { expect{video.content_detail}.to raise_error Yt::Errors::NoItems }
|
|
1326
|
+
it { expect{video.snippet}.to raise_error Yt::Errors::NoItems }
|
|
1327
|
+
it { expect{video.rating}.to raise_error Yt::Errors::NoItems }
|
|
1328
|
+
it { expect{video.status}.to raise_error Yt::Errors::NoItems }
|
|
1329
|
+
it { expect{video.statistics_set}.to raise_error Yt::Errors::NoItems }
|
|
1330
|
+
it { expect{video.file_detail}.to raise_error Yt::Errors::NoItems }
|
|
1331
|
+
end
|
|
1332
|
+
|
|
1333
|
+
context 'given one of my own videos that I want to delete' do
|
|
1334
|
+
before(:all) { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: "Yt Test Delete Video #{rand}" }
|
|
1335
|
+
let(:id) { @tmp_video.id }
|
|
1336
|
+
|
|
1337
|
+
it { expect(video.delete).to be true }
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
context 'given one of my own videos that I want to update' do
|
|
1341
|
+
let(:id) { $account.videos.where(order: 'viewCount').first.id }
|
|
1342
|
+
let!(:old_title) { video.title }
|
|
1343
|
+
let!(:old_privacy_status) { video.privacy_status }
|
|
1344
|
+
let(:update) { video.update attrs }
|
|
1345
|
+
|
|
1346
|
+
context 'given I update the title' do
|
|
1347
|
+
# NOTE: The use of UTF-8 characters is to test that we can pass up to
|
|
1348
|
+
# 50 characters, independently of their representation
|
|
1349
|
+
let(:attrs) { {title: "Yt Example Update Video #{rand} - ®•♡❥❦❧☙"} }
|
|
1350
|
+
|
|
1351
|
+
specify 'only updates the title' do
|
|
1352
|
+
expect(update).to be true
|
|
1353
|
+
expect(video.title).not_to eq old_title
|
|
1354
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1355
|
+
end
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1358
|
+
context 'given I update the description' do
|
|
1359
|
+
let!(:old_description) { video.description }
|
|
1360
|
+
let(:attrs) { {description: "Yt Example Description #{rand} - ®•♡❥❦❧☙"} }
|
|
1361
|
+
|
|
1362
|
+
specify 'only updates the description' do
|
|
1363
|
+
expect(update).to be true
|
|
1364
|
+
expect(video.description).not_to eq old_description
|
|
1365
|
+
expect(video.title).to eq old_title
|
|
1366
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1367
|
+
end
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
context 'given I update the tags' do
|
|
1371
|
+
let!(:old_tags) { video.tags }
|
|
1372
|
+
let(:attrs) { {tags: ["Yt Test Tag #{rand}"]} }
|
|
1373
|
+
|
|
1374
|
+
specify 'only updates the tag' do
|
|
1375
|
+
expect(update).to be true
|
|
1376
|
+
expect(video.tags).not_to eq old_tags
|
|
1377
|
+
expect(video.title).to eq old_title
|
|
1378
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1379
|
+
end
|
|
1380
|
+
end
|
|
1381
|
+
|
|
1382
|
+
context 'given I update the category ID' do
|
|
1383
|
+
let!(:old_category_id) { video.category_id }
|
|
1384
|
+
let!(:new_category_id) { old_category_id == '22' ? '21' : '22' }
|
|
1385
|
+
|
|
1386
|
+
context 'passing the parameter in underscore syntax' do
|
|
1387
|
+
let(:attrs) { {category_id: new_category_id} }
|
|
1388
|
+
|
|
1389
|
+
specify 'only updates the category ID' do
|
|
1390
|
+
expect(update).to be true
|
|
1391
|
+
expect(video.category_id).not_to eq old_category_id
|
|
1392
|
+
expect(video.title).to eq old_title
|
|
1393
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1394
|
+
end
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1398
|
+
let(:attrs) { {categoryId: new_category_id} }
|
|
1399
|
+
|
|
1400
|
+
specify 'only updates the category ID' do
|
|
1401
|
+
expect(update).to be true
|
|
1402
|
+
expect(video.category_id).not_to eq old_category_id
|
|
1403
|
+
end
|
|
1404
|
+
end
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1407
|
+
context 'given I update title, description and/or tags using angle brackets' do
|
|
1408
|
+
let(:attrs) { {title: "Example Yt Test < >", description: '< >', tags: ['<tag>']} }
|
|
1409
|
+
|
|
1410
|
+
specify 'updates them replacing angle brackets with similar unicode characters accepted by YouTube' do
|
|
1411
|
+
expect(update).to be true
|
|
1412
|
+
expect(video.title).to eq 'Example Yt Test ‹ ›'
|
|
1413
|
+
expect(video.description).to eq '‹ ›'
|
|
1414
|
+
expect(video.tags).to eq ['‹tag›']
|
|
1415
|
+
end
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
# note: 'scheduled' videos cannot be set to 'unlisted'
|
|
1419
|
+
context 'given I update the privacy status' do
|
|
1420
|
+
before { video.update publish_at: nil if video.scheduled? }
|
|
1421
|
+
let!(:new_privacy_status) { old_privacy_status == 'private' ? 'unlisted' : 'private' }
|
|
1422
|
+
|
|
1423
|
+
context 'passing the parameter in underscore syntax' do
|
|
1424
|
+
let(:attrs) { {privacy_status: new_privacy_status} }
|
|
1425
|
+
|
|
1426
|
+
specify 'only updates the privacy status' do
|
|
1427
|
+
expect(update).to be true
|
|
1428
|
+
expect(video.privacy_status).not_to eq old_privacy_status
|
|
1429
|
+
expect(video.title).to eq old_title
|
|
1430
|
+
end
|
|
1431
|
+
end
|
|
1432
|
+
|
|
1433
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1434
|
+
let(:attrs) { {privacyStatus: new_privacy_status} }
|
|
1435
|
+
|
|
1436
|
+
specify 'only updates the privacy status' do
|
|
1437
|
+
expect(update).to be true
|
|
1438
|
+
expect(video.privacy_status).not_to eq old_privacy_status
|
|
1439
|
+
expect(video.title).to eq old_title
|
|
1440
|
+
end
|
|
1441
|
+
end
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1444
|
+
context 'given I update the embeddable status' do
|
|
1445
|
+
let!(:old_embeddable) { video.embeddable? }
|
|
1446
|
+
let!(:new_embeddable) { !old_embeddable }
|
|
1447
|
+
|
|
1448
|
+
let(:attrs) { {embeddable: new_embeddable} }
|
|
1449
|
+
|
|
1450
|
+
# @note: This test is a reflection of another irrational behavior of
|
|
1451
|
+
# YouTube API. Although 'embeddable' can be passed as an 'update'
|
|
1452
|
+
# attribute according to the documentation, it simply does not work.
|
|
1453
|
+
# The day YouTube fixes it, then this test will finally fail and will
|
|
1454
|
+
# be removed, documenting how to update 'embeddable' too.
|
|
1455
|
+
# @see https://developers.google.com/youtube/v3/docs/videos/update
|
|
1456
|
+
# @see https://code.google.com/p/gdata-issues/issues/detail?id=4861
|
|
1457
|
+
specify 'does not update the embeddable status' do
|
|
1458
|
+
expect(update).to be true
|
|
1459
|
+
expect(video.embeddable?).to eq old_embeddable
|
|
1460
|
+
end
|
|
1461
|
+
end
|
|
1462
|
+
|
|
1463
|
+
context 'given I update the public stats viewable setting' do
|
|
1464
|
+
let!(:old_public_stats_viewable) { video.has_public_stats_viewable? }
|
|
1465
|
+
let!(:new_public_stats_viewable) { !old_public_stats_viewable }
|
|
1466
|
+
|
|
1467
|
+
context 'passing the parameter in underscore syntax' do
|
|
1468
|
+
let(:attrs) { {public_stats_viewable: new_public_stats_viewable} }
|
|
1469
|
+
|
|
1470
|
+
specify 'only updates the public stats viewable setting' do
|
|
1471
|
+
expect(update).to be true
|
|
1472
|
+
expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
|
|
1473
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1474
|
+
expect(video.title).to eq old_title
|
|
1475
|
+
end
|
|
1476
|
+
end
|
|
1477
|
+
|
|
1478
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1479
|
+
let(:attrs) { {publicStatsViewable: new_public_stats_viewable} }
|
|
1480
|
+
|
|
1481
|
+
specify 'only updates the public stats viewable setting' do
|
|
1482
|
+
expect(update).to be true
|
|
1483
|
+
expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
|
|
1484
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1485
|
+
expect(video.title).to eq old_title
|
|
1486
|
+
end
|
|
1487
|
+
end
|
|
1488
|
+
end
|
|
1489
|
+
|
|
1490
|
+
it 'returns valid reports for video-related metrics' do
|
|
1491
|
+
# Some reports are only available to Content Owners.
|
|
1492
|
+
# See content owner test for more details about what the methods return.
|
|
1493
|
+
expect{video.views}.not_to raise_error
|
|
1494
|
+
expect{video.comments}.not_to raise_error
|
|
1495
|
+
expect{video.likes}.not_to raise_error
|
|
1496
|
+
expect{video.dislikes}.not_to raise_error
|
|
1497
|
+
expect{video.shares}.not_to raise_error
|
|
1498
|
+
expect{video.subscribers_gained}.not_to raise_error
|
|
1499
|
+
expect{video.subscribers_lost}.not_to raise_error
|
|
1500
|
+
expect{video.videos_added_to_playlists}.not_to raise_error
|
|
1501
|
+
expect{video.videos_removed_from_playlists}.not_to raise_error
|
|
1502
|
+
expect{video.estimated_minutes_watched}.not_to raise_error
|
|
1503
|
+
expect{video.average_view_duration}.not_to raise_error
|
|
1504
|
+
expect{video.average_view_percentage}.not_to raise_error
|
|
1505
|
+
expect{video.annotation_clicks}.not_to raise_error
|
|
1506
|
+
expect{video.annotation_click_through_rate}.not_to raise_error
|
|
1507
|
+
expect{video.annotation_close_rate}.not_to raise_error
|
|
1508
|
+
expect{video.viewer_percentage}.not_to raise_error
|
|
1509
|
+
expect{video.estimated_revenue}.to raise_error Yt::Errors::Unauthorized
|
|
1510
|
+
expect{video.ad_impressions}.to raise_error Yt::Errors::Unauthorized
|
|
1511
|
+
expect{video.monetized_playbacks}.to raise_error Yt::Errors::Unauthorized
|
|
1512
|
+
expect{video.playback_based_cpm}.to raise_error Yt::Errors::Unauthorized
|
|
1513
|
+
expect{video.advertising_options_set}.to raise_error Yt::Errors::Forbidden
|
|
1514
|
+
end
|
|
1515
|
+
end
|
|
1516
|
+
|
|
1517
|
+
# @note: This test is separated from the block above because, for some
|
|
1518
|
+
# undocumented reasons, if an existing video was private, then set to
|
|
1519
|
+
# unlisted, then set to private again, YouTube _sometimes_ raises a
|
|
1520
|
+
# 400 Error when trying to set the publishAt timestamp.
|
|
1521
|
+
# Therefore, just to test the updating of publishAt, we use a brand new
|
|
1522
|
+
# video (set to private), rather than reusing an existing one as above.
|
|
1523
|
+
context 'given one of my own *private* videos that I want to update' do
|
|
1524
|
+
before { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: old_title, privacy_status: old_privacy_status }
|
|
1525
|
+
let(:id) { @tmp_video.id }
|
|
1526
|
+
let!(:old_title) { "Yt Test Update publishAt Video #{rand}" }
|
|
1527
|
+
let!(:old_privacy_status) { 'private' }
|
|
1528
|
+
after { video.delete }
|
|
1529
|
+
|
|
1530
|
+
let!(:new_scheduled_at) { Yt::Timestamp.parse("#{rand(30) + 1} Jan 2020", Time.now) }
|
|
1531
|
+
|
|
1532
|
+
context 'passing the parameter in underscore syntax' do
|
|
1533
|
+
let(:attrs) { {publish_at: new_scheduled_at} }
|
|
1534
|
+
|
|
1535
|
+
specify 'only updates the timestamp to publish the video' do
|
|
1536
|
+
expect(video.update attrs).to be true
|
|
1537
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1538
|
+
expect(video.title).to eq old_title
|
|
1539
|
+
# NOTE: This is another irrational behavior of YouTube API. In short,
|
|
1540
|
+
# the response of Video#update *does not* include the publishAt value
|
|
1541
|
+
# even if it exists. You need to call Video#list again to get it.
|
|
1542
|
+
video = Yt::Video.new id: id, auth: $account
|
|
1543
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
1544
|
+
# Setting a private (scheduled) video to private has no effect:
|
|
1545
|
+
expect(video.update privacy_status: 'private').to be true
|
|
1546
|
+
video = Yt::Video.new id: id, auth: $account
|
|
1547
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
1548
|
+
# Setting a private (scheduled) video to unlisted/public removes publishAt:
|
|
1549
|
+
expect(video.update privacy_status: 'unlisted').to be true
|
|
1550
|
+
video = Yt::Video.new id: id, auth: $account
|
|
1551
|
+
expect(video.scheduled_at).to be_nil
|
|
1552
|
+
end
|
|
1553
|
+
end
|
|
1554
|
+
|
|
1555
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1556
|
+
let(:attrs) { {publishAt: new_scheduled_at} }
|
|
1557
|
+
|
|
1558
|
+
specify 'only updates the timestamp to publish the video' do
|
|
1559
|
+
expect(video.update attrs).to be true
|
|
1560
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
1561
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1562
|
+
expect(video.title).to eq old_title
|
|
1563
|
+
end
|
|
1564
|
+
end
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
# @note: This should somehow test that the thumbnail *changes*. However,
|
|
1568
|
+
# YouTube does not change the URL of the thumbnail even though the content
|
|
1569
|
+
# changes. A full test would have to *download* the thumbnails before and
|
|
1570
|
+
# after, and compare the files. For now, not raising error is enough.
|
|
1571
|
+
# Eventually, change to `expect{update}.to change{video.thumbnail_url}`
|
|
1572
|
+
context 'given one of my own videos for which I want to upload a thumbnail' do
|
|
1573
|
+
let(:id) { $account.videos.where(order: 'viewCount').first.id }
|
|
1574
|
+
let(:update) { video.upload_thumbnail path_or_url }
|
|
1575
|
+
|
|
1576
|
+
context 'given the path to a local JPG image file' do
|
|
1577
|
+
let(:path_or_url) { File.expand_path '../thumbnail.jpg', __FILE__ }
|
|
1578
|
+
|
|
1579
|
+
it { expect{update}.not_to raise_error }
|
|
1580
|
+
end
|
|
1581
|
+
|
|
1582
|
+
context 'given the path to a remote PNG image file' do
|
|
1583
|
+
let(:path_or_url) { 'https://bit.ly/yt_thumbnail' }
|
|
1584
|
+
|
|
1585
|
+
it { expect{update}.not_to raise_error }
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1588
|
+
context 'given an invalid URL' do
|
|
1589
|
+
let(:path_or_url) { 'this-is-not-a-url' }
|
|
1590
|
+
|
|
1591
|
+
it { expect{update}.to raise_error Yt::Errors::RequestError }
|
|
1592
|
+
end
|
|
1593
|
+
end
|
|
1594
|
+
|
|
1595
|
+
# @note: This test is separated from the block above because YouTube only
|
|
1596
|
+
# returns file details for *some videos*: "The fileDetails object will
|
|
1597
|
+
# only be returned if the processingDetails.fileAvailability property
|
|
1598
|
+
# has a value of available.". Therefore, just to test fileDetails, we use a
|
|
1599
|
+
# different video that (for some unknown reason) is marked as 'available'.
|
|
1600
|
+
# Also note that I was not able to find a single video returning fileName,
|
|
1601
|
+
# therefore video.file_name is not returned by Yt, until it can be tested.
|
|
1602
|
+
# @see https://developers.google.com/youtube/v3/docs/videos#processingDetails.fileDetailsAvailability
|
|
1603
|
+
context 'given one of my own *available* videos' do
|
|
1604
|
+
let(:id) { 'yCmaOvUFhlI' }
|
|
1605
|
+
|
|
1606
|
+
it 'returns valid file details' do
|
|
1607
|
+
expect(video.file_size).to be_an Integer
|
|
1608
|
+
expect(video.file_type).to be_a String
|
|
1609
|
+
expect(video.container).to be_a String
|
|
1610
|
+
end
|
|
1611
|
+
end
|
|
1612
|
+
end
|
|
1613
|
+
# encoding: UTF-8
|
|
1614
|
+
|
|
1615
|
+
require 'spec_helper'
|
|
1616
|
+
require 'yt/models/video'
|
|
1617
|
+
|
|
1618
|
+
describe Yt::Video, :device_app do
|
|
1619
|
+
subject(:video) { Yt::Video.new id: id, auth: $account }
|
|
1620
|
+
|
|
1621
|
+
context 'given someone else’s video' do
|
|
1622
|
+
let(:id) { '9bZkp7q19f0' }
|
|
1623
|
+
|
|
1624
|
+
it { expect(video.content_detail).to be_a Yt::ContentDetail }
|
|
1625
|
+
|
|
1626
|
+
it 'returns valid metadata' do
|
|
1627
|
+
expect(video.title).to be_a String
|
|
1628
|
+
expect(video.description).to be_a String
|
|
1629
|
+
expect(video.thumbnail_url).to be_a String
|
|
1630
|
+
expect(video.published_at).to be_a Time
|
|
1631
|
+
expect(video.privacy_status).to be_a String
|
|
1632
|
+
expect(video.tags).to be_an Array
|
|
1633
|
+
expect(video.channel_id).to be_a String
|
|
1634
|
+
expect(video.channel_title).to be_a String
|
|
1635
|
+
expect(video.channel_url).to be_a String
|
|
1636
|
+
expect(video.category_id).to be_a String
|
|
1637
|
+
expect(video.live_broadcast_content).to be_a String
|
|
1638
|
+
expect(video.view_count).to be_an Integer
|
|
1639
|
+
expect(video.like_count).to be_an Integer
|
|
1640
|
+
expect(video.dislike_count).to be_an Integer
|
|
1641
|
+
expect(video.favorite_count).to be_an Integer
|
|
1642
|
+
expect(video.comment_count).to be_an Integer
|
|
1643
|
+
expect(video.duration).to be_an Integer
|
|
1644
|
+
expect(video.hd?).to be_in [true, false]
|
|
1645
|
+
expect(video.stereoscopic?).to be_in [true, false]
|
|
1646
|
+
expect(video.captioned?).to be_in [true, false]
|
|
1647
|
+
expect(video.licensed?).to be_in [true, false]
|
|
1648
|
+
expect(video.deleted?).to be_in [true, false]
|
|
1649
|
+
expect(video.failed?).to be_in [true, false]
|
|
1650
|
+
expect(video.processed?).to be_in [true, false]
|
|
1651
|
+
expect(video.rejected?).to be_in [true, false]
|
|
1652
|
+
expect(video.uploading?).to be_in [true, false]
|
|
1653
|
+
expect(video.uses_unsupported_codec?).to be_in [true, false]
|
|
1654
|
+
expect(video.has_failed_conversion?).to be_in [true, false]
|
|
1655
|
+
expect(video.empty?).to be_in [true, false]
|
|
1656
|
+
expect(video.invalid?).to be_in [true, false]
|
|
1657
|
+
expect(video.too_small?).to be_in [true, false]
|
|
1658
|
+
expect(video.aborted?).to be_in [true, false]
|
|
1659
|
+
expect(video.claimed?).to be_in [true, false]
|
|
1660
|
+
expect(video.infringes_copyright?).to be_in [true, false]
|
|
1661
|
+
expect(video.duplicate?).to be_in [true, false]
|
|
1662
|
+
expect(video.scheduled_at.class).to be_in [NilClass, Time]
|
|
1663
|
+
expect(video.scheduled?).to be_in [true, false]
|
|
1664
|
+
expect(video.too_long?).to be_in [true, false]
|
|
1665
|
+
expect(video.violates_terms_of_use?).to be_in [true, false]
|
|
1666
|
+
expect(video.inappropriate?).to be_in [true, false]
|
|
1667
|
+
expect(video.infringes_trademark?).to be_in [true, false]
|
|
1668
|
+
expect(video.belongs_to_closed_account?).to be_in [true, false]
|
|
1669
|
+
expect(video.belongs_to_suspended_account?).to be_in [true, false]
|
|
1670
|
+
expect(video.licensed_as_creative_commons?).to be_in [true, false]
|
|
1671
|
+
expect(video.licensed_as_standard_youtube?).to be_in [true, false]
|
|
1672
|
+
expect(video.has_public_stats_viewable?).to be_in [true, false]
|
|
1673
|
+
expect(video.embeddable?).to be_in [true, false]
|
|
1674
|
+
expect(video.actual_start_time).to be_nil
|
|
1675
|
+
expect(video.actual_end_time).to be_nil
|
|
1676
|
+
expect(video.scheduled_start_time).to be_nil
|
|
1677
|
+
expect(video.scheduled_end_time).to be_nil
|
|
1678
|
+
expect(video.concurrent_viewers).to be_nil
|
|
1679
|
+
expect(video.embed_html).to be_a String
|
|
1680
|
+
expect(video.category_title).to be_a String
|
|
1681
|
+
end
|
|
1682
|
+
|
|
1683
|
+
it { expect{video.update}.to fail }
|
|
1684
|
+
it { expect{video.delete}.to fail.with 'forbidden' }
|
|
1685
|
+
|
|
1686
|
+
context 'that I like' do
|
|
1687
|
+
before { video.like }
|
|
1688
|
+
it { expect(video).to be_liked }
|
|
1689
|
+
it { expect(video.dislike).to be true }
|
|
1690
|
+
end
|
|
1691
|
+
|
|
1692
|
+
context 'that I dislike' do
|
|
1693
|
+
before { video.dislike }
|
|
1694
|
+
it { expect(video).not_to be_liked }
|
|
1695
|
+
it { expect(video.like).to be true }
|
|
1696
|
+
end
|
|
1697
|
+
|
|
1698
|
+
context 'that I am indifferent to' do
|
|
1699
|
+
before { video.unlike }
|
|
1700
|
+
it { expect(video).not_to be_liked }
|
|
1701
|
+
it { expect(video.like).to be true }
|
|
1702
|
+
end
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
context 'given someone else’s live video broadcast scheduled in the future' do
|
|
1706
|
+
let(:id) { 'PqzGI8gO_gk' }
|
|
1707
|
+
|
|
1708
|
+
it 'returns valid live streaming details' do
|
|
1709
|
+
expect(video.actual_start_time).to be_nil
|
|
1710
|
+
expect(video.actual_end_time).to be_nil
|
|
1711
|
+
expect(video.scheduled_start_time).to be_a Time
|
|
1712
|
+
expect(video.scheduled_end_time).to be_nil
|
|
1713
|
+
end
|
|
1714
|
+
end
|
|
1715
|
+
|
|
1716
|
+
context 'given someone else’s past live video broadcast' do
|
|
1717
|
+
let(:id) { 'COOM8_tOy6U' }
|
|
1718
|
+
|
|
1719
|
+
it 'returns valid live streaming details' do
|
|
1720
|
+
expect(video.actual_start_time).to be_a Time
|
|
1721
|
+
expect(video.actual_end_time).to be_a Time
|
|
1722
|
+
expect(video.scheduled_start_time).to be_a Time
|
|
1723
|
+
expect(video.scheduled_end_time).to be_a Time
|
|
1724
|
+
expect(video.concurrent_viewers).to be_nil
|
|
1725
|
+
end
|
|
1726
|
+
end
|
|
1727
|
+
|
|
1728
|
+
context 'given an unknown video' do
|
|
1729
|
+
let(:id) { 'not-a-video-id' }
|
|
1730
|
+
|
|
1731
|
+
it { expect{video.content_detail}.to raise_error Yt::Errors::NoItems }
|
|
1732
|
+
it { expect{video.snippet}.to raise_error Yt::Errors::NoItems }
|
|
1733
|
+
it { expect{video.rating}.to raise_error Yt::Errors::NoItems }
|
|
1734
|
+
it { expect{video.status}.to raise_error Yt::Errors::NoItems }
|
|
1735
|
+
it { expect{video.statistics_set}.to raise_error Yt::Errors::NoItems }
|
|
1736
|
+
it { expect{video.file_detail}.to raise_error Yt::Errors::NoItems }
|
|
1737
|
+
end
|
|
1738
|
+
|
|
1739
|
+
context 'given one of my own videos that I want to delete' do
|
|
1740
|
+
before(:all) { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: "Yt Test Delete Video #{rand}" }
|
|
1741
|
+
let(:id) { @tmp_video.id }
|
|
1742
|
+
|
|
1743
|
+
it { expect(video.delete).to be true }
|
|
1744
|
+
end
|
|
1745
|
+
|
|
1746
|
+
context 'given one of my own videos that I want to update' do
|
|
1747
|
+
let(:id) { $account.videos.where(order: 'viewCount').first.id }
|
|
1748
|
+
let!(:old_title) { video.title }
|
|
1749
|
+
let!(:old_privacy_status) { video.privacy_status }
|
|
1750
|
+
let(:update) { video.update attrs }
|
|
1751
|
+
|
|
1752
|
+
context 'given I update the title' do
|
|
1753
|
+
# NOTE: The use of UTF-8 characters is to test that we can pass up to
|
|
1754
|
+
# 50 characters, independently of their representation
|
|
1755
|
+
let(:attrs) { {title: "Yt Example Update Video #{rand} - ®•♡❥❦❧☙"} }
|
|
1756
|
+
|
|
1757
|
+
specify 'only updates the title' do
|
|
1758
|
+
expect(update).to be true
|
|
1759
|
+
expect(video.title).not_to eq old_title
|
|
1760
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1761
|
+
end
|
|
1762
|
+
end
|
|
1763
|
+
|
|
1764
|
+
context 'given I update the description' do
|
|
1765
|
+
let!(:old_description) { video.description }
|
|
1766
|
+
let(:attrs) { {description: "Yt Example Description #{rand} - ®•♡❥❦❧☙"} }
|
|
1767
|
+
|
|
1768
|
+
specify 'only updates the description' do
|
|
1769
|
+
expect(update).to be true
|
|
1770
|
+
expect(video.description).not_to eq old_description
|
|
1771
|
+
expect(video.title).to eq old_title
|
|
1772
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1773
|
+
end
|
|
1774
|
+
end
|
|
1775
|
+
|
|
1776
|
+
context 'given I update the tags' do
|
|
1777
|
+
let!(:old_tags) { video.tags }
|
|
1778
|
+
let(:attrs) { {tags: ["Yt Test Tag #{rand}"]} }
|
|
1779
|
+
|
|
1780
|
+
specify 'only updates the tag' do
|
|
1781
|
+
expect(update).to be true
|
|
1782
|
+
expect(video.tags).not_to eq old_tags
|
|
1783
|
+
expect(video.title).to eq old_title
|
|
1784
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1785
|
+
end
|
|
1786
|
+
end
|
|
1787
|
+
|
|
1788
|
+
context 'given I update the category ID' do
|
|
1789
|
+
let!(:old_category_id) { video.category_id }
|
|
1790
|
+
let!(:new_category_id) { old_category_id == '22' ? '21' : '22' }
|
|
1791
|
+
|
|
1792
|
+
context 'passing the parameter in underscore syntax' do
|
|
1793
|
+
let(:attrs) { {category_id: new_category_id} }
|
|
1794
|
+
|
|
1795
|
+
specify 'only updates the category ID' do
|
|
1796
|
+
expect(update).to be true
|
|
1797
|
+
expect(video.category_id).not_to eq old_category_id
|
|
1798
|
+
expect(video.title).to eq old_title
|
|
1799
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1800
|
+
end
|
|
1801
|
+
end
|
|
1802
|
+
|
|
1803
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1804
|
+
let(:attrs) { {categoryId: new_category_id} }
|
|
1805
|
+
|
|
1806
|
+
specify 'only updates the category ID' do
|
|
1807
|
+
expect(update).to be true
|
|
1808
|
+
expect(video.category_id).not_to eq old_category_id
|
|
1809
|
+
end
|
|
1810
|
+
end
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
context 'given I update title, description and/or tags using angle brackets' do
|
|
1814
|
+
let(:attrs) { {title: "Example Yt Test < >", description: '< >', tags: ['<tag>']} }
|
|
1815
|
+
|
|
1816
|
+
specify 'updates them replacing angle brackets with similar unicode characters accepted by YouTube' do
|
|
1817
|
+
expect(update).to be true
|
|
1818
|
+
expect(video.title).to eq 'Example Yt Test ‹ ›'
|
|
1819
|
+
expect(video.description).to eq '‹ ›'
|
|
1820
|
+
expect(video.tags).to eq ['‹tag›']
|
|
1821
|
+
end
|
|
1822
|
+
end
|
|
1823
|
+
|
|
1824
|
+
# note: 'scheduled' videos cannot be set to 'unlisted'
|
|
1825
|
+
context 'given I update the privacy status' do
|
|
1826
|
+
before { video.update publish_at: nil if video.scheduled? }
|
|
1827
|
+
let!(:new_privacy_status) { old_privacy_status == 'private' ? 'unlisted' : 'private' }
|
|
1828
|
+
|
|
1829
|
+
context 'passing the parameter in underscore syntax' do
|
|
1830
|
+
let(:attrs) { {privacy_status: new_privacy_status} }
|
|
1831
|
+
|
|
1832
|
+
specify 'only updates the privacy status' do
|
|
1833
|
+
expect(update).to be true
|
|
1834
|
+
expect(video.privacy_status).not_to eq old_privacy_status
|
|
1835
|
+
expect(video.title).to eq old_title
|
|
1836
|
+
end
|
|
1837
|
+
end
|
|
1838
|
+
|
|
1839
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1840
|
+
let(:attrs) { {privacyStatus: new_privacy_status} }
|
|
1841
|
+
|
|
1842
|
+
specify 'only updates the privacy status' do
|
|
1843
|
+
expect(update).to be true
|
|
1844
|
+
expect(video.privacy_status).not_to eq old_privacy_status
|
|
1845
|
+
expect(video.title).to eq old_title
|
|
1846
|
+
end
|
|
1847
|
+
end
|
|
1848
|
+
end
|
|
1849
|
+
|
|
1850
|
+
context 'given I update the embeddable status' do
|
|
1851
|
+
let!(:old_embeddable) { video.embeddable? }
|
|
1852
|
+
let!(:new_embeddable) { !old_embeddable }
|
|
1853
|
+
|
|
1854
|
+
let(:attrs) { {embeddable: new_embeddable} }
|
|
1855
|
+
|
|
1856
|
+
# @note: This test is a reflection of another irrational behavior of
|
|
1857
|
+
# YouTube API. Although 'embeddable' can be passed as an 'update'
|
|
1858
|
+
# attribute according to the documentation, it simply does not work.
|
|
1859
|
+
# The day YouTube fixes it, then this test will finally fail and will
|
|
1860
|
+
# be removed, documenting how to update 'embeddable' too.
|
|
1861
|
+
# @see https://developers.google.com/youtube/v3/docs/videos/update
|
|
1862
|
+
# @see https://code.google.com/p/gdata-issues/issues/detail?id=4861
|
|
1863
|
+
specify 'does not update the embeddable status' do
|
|
1864
|
+
expect(update).to be true
|
|
1865
|
+
expect(video.embeddable?).to eq old_embeddable
|
|
1866
|
+
end
|
|
1867
|
+
end
|
|
1868
|
+
|
|
1869
|
+
context 'given I update the public stats viewable setting' do
|
|
1870
|
+
let!(:old_public_stats_viewable) { video.has_public_stats_viewable? }
|
|
1871
|
+
let!(:new_public_stats_viewable) { !old_public_stats_viewable }
|
|
1872
|
+
|
|
1873
|
+
context 'passing the parameter in underscore syntax' do
|
|
1874
|
+
let(:attrs) { {public_stats_viewable: new_public_stats_viewable} }
|
|
1875
|
+
|
|
1876
|
+
specify 'only updates the public stats viewable setting' do
|
|
1877
|
+
expect(update).to be true
|
|
1878
|
+
expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
|
|
1879
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1880
|
+
expect(video.title).to eq old_title
|
|
1881
|
+
end
|
|
1882
|
+
end
|
|
1883
|
+
|
|
1884
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1885
|
+
let(:attrs) { {publicStatsViewable: new_public_stats_viewable} }
|
|
1886
|
+
|
|
1887
|
+
specify 'only updates the public stats viewable setting' do
|
|
1888
|
+
expect(update).to be true
|
|
1889
|
+
expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
|
|
1890
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1891
|
+
expect(video.title).to eq old_title
|
|
1892
|
+
end
|
|
1893
|
+
end
|
|
1894
|
+
end
|
|
1895
|
+
|
|
1896
|
+
it 'returns valid reports for video-related metrics' do
|
|
1897
|
+
# Some reports are only available to Content Owners.
|
|
1898
|
+
# See content owner test for more details about what the methods return.
|
|
1899
|
+
expect{video.views}.not_to raise_error
|
|
1900
|
+
expect{video.comments}.not_to raise_error
|
|
1901
|
+
expect{video.likes}.not_to raise_error
|
|
1902
|
+
expect{video.dislikes}.not_to raise_error
|
|
1903
|
+
expect{video.shares}.not_to raise_error
|
|
1904
|
+
expect{video.subscribers_gained}.not_to raise_error
|
|
1905
|
+
expect{video.subscribers_lost}.not_to raise_error
|
|
1906
|
+
expect{video.videos_added_to_playlists}.not_to raise_error
|
|
1907
|
+
expect{video.videos_removed_from_playlists}.not_to raise_error
|
|
1908
|
+
expect{video.estimated_minutes_watched}.not_to raise_error
|
|
1909
|
+
expect{video.average_view_duration}.not_to raise_error
|
|
1910
|
+
expect{video.average_view_percentage}.not_to raise_error
|
|
1911
|
+
expect{video.annotation_clicks}.not_to raise_error
|
|
1912
|
+
expect{video.annotation_click_through_rate}.not_to raise_error
|
|
1913
|
+
expect{video.annotation_close_rate}.not_to raise_error
|
|
1914
|
+
expect{video.viewer_percentage}.not_to raise_error
|
|
1915
|
+
expect{video.estimated_revenue}.to raise_error Yt::Errors::Unauthorized
|
|
1916
|
+
expect{video.ad_impressions}.to raise_error Yt::Errors::Unauthorized
|
|
1917
|
+
expect{video.monetized_playbacks}.to raise_error Yt::Errors::Unauthorized
|
|
1918
|
+
expect{video.playback_based_cpm}.to raise_error Yt::Errors::Unauthorized
|
|
1919
|
+
expect{video.advertising_options_set}.to raise_error Yt::Errors::Forbidden
|
|
1920
|
+
|
|
1921
|
+
end
|
|
1922
|
+
end
|
|
1923
|
+
|
|
1924
|
+
# @note: This test is separated from the block above because, for some
|
|
1925
|
+
# undocumented reasons, if an existing video was private, then set to
|
|
1926
|
+
# unlisted, then set to private again, YouTube _sometimes_ raises a
|
|
1927
|
+
# 400 Error when trying to set the publishAt timestamp.
|
|
1928
|
+
# Therefore, just to test the updating of publishAt, we use a brand new
|
|
1929
|
+
# video (set to private), rather than reusing an existing one as above.
|
|
1930
|
+
context 'given one of my own *private* videos that I want to update' do
|
|
1931
|
+
before { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: old_title, privacy_status: old_privacy_status }
|
|
1932
|
+
let(:id) { @tmp_video.id }
|
|
1933
|
+
let!(:old_title) { "Yt Test Update publishAt Video #{rand}" }
|
|
1934
|
+
let!(:old_privacy_status) { 'private' }
|
|
1935
|
+
after { video.delete }
|
|
1936
|
+
|
|
1937
|
+
let!(:new_scheduled_at) { Yt::Timestamp.parse("#{rand(30) + 1} Jan 2020", Time.now) }
|
|
1938
|
+
|
|
1939
|
+
context 'passing the parameter in underscore syntax' do
|
|
1940
|
+
let(:attrs) { {publish_at: new_scheduled_at} }
|
|
1941
|
+
|
|
1942
|
+
specify 'only updates the timestamp to publish the video' do
|
|
1943
|
+
expect(video.update attrs).to be true
|
|
1944
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1945
|
+
expect(video.title).to eq old_title
|
|
1946
|
+
# NOTE: This is another irrational behavior of YouTube API. In short,
|
|
1947
|
+
# the response of Video#update *does not* include the publishAt value
|
|
1948
|
+
# even if it exists. You need to call Video#list again to get it.
|
|
1949
|
+
video = Yt::Video.new id: id, auth: $account
|
|
1950
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
1951
|
+
# Setting a private (scheduled) video to private has no effect:
|
|
1952
|
+
expect(video.update privacy_status: 'private').to be true
|
|
1953
|
+
video = Yt::Video.new id: id, auth: $account
|
|
1954
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
1955
|
+
# Setting a private (scheduled) video to unlisted/public removes publishAt:
|
|
1956
|
+
expect(video.update privacy_status: 'unlisted').to be true
|
|
1957
|
+
video = Yt::Video.new id: id, auth: $account
|
|
1958
|
+
expect(video.scheduled_at).to be_nil
|
|
1959
|
+
end
|
|
1960
|
+
end
|
|
1961
|
+
|
|
1962
|
+
context 'passing the parameter in camel-case syntax' do
|
|
1963
|
+
let(:attrs) { {publishAt: new_scheduled_at} }
|
|
1964
|
+
|
|
1965
|
+
specify 'only updates the timestamp to publish the video' do
|
|
1966
|
+
expect(video.update attrs).to be true
|
|
1967
|
+
expect(video.scheduled_at).to eq new_scheduled_at
|
|
1968
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
1969
|
+
expect(video.title).to eq old_title
|
|
1970
|
+
end
|
|
1971
|
+
end
|
|
1972
|
+
end
|
|
1973
|
+
|
|
1974
|
+
# @note: This should somehow test that the thumbnail *changes*. However,
|
|
1975
|
+
# YouTube does not change the URL of the thumbnail even though the content
|
|
1976
|
+
# changes. A full test would have to *download* the thumbnails before and
|
|
1977
|
+
# after, and compare the files. For now, not raising error is enough.
|
|
1978
|
+
# Eventually, change to `expect{update}.to change{video.thumbnail_url}`
|
|
1979
|
+
context 'given one of my own videos for which I want to upload a thumbnail' do
|
|
1980
|
+
let(:id) { $account.videos.where(order: 'viewCount').first.id }
|
|
1981
|
+
let(:update) { video.upload_thumbnail path_or_url }
|
|
1982
|
+
|
|
1983
|
+
context 'given the path to a local JPG image file' do
|
|
1984
|
+
let(:path_or_url) { File.expand_path '../thumbnail.jpg', __FILE__ }
|
|
1985
|
+
|
|
1986
|
+
it { expect{update}.not_to raise_error }
|
|
1987
|
+
end
|
|
1988
|
+
|
|
1989
|
+
context 'given the path to a remote PNG image file' do
|
|
1990
|
+
let(:path_or_url) { 'https://bit.ly/yt_thumbnail' }
|
|
1991
|
+
|
|
1992
|
+
it { expect{update}.not_to raise_error }
|
|
1993
|
+
end
|
|
1994
|
+
|
|
1995
|
+
context 'given an invalid URL' do
|
|
1996
|
+
let(:path_or_url) { 'this-is-not-a-url' }
|
|
1997
|
+
|
|
1998
|
+
it { expect{update}.to raise_error Yt::Errors::RequestError }
|
|
1999
|
+
end
|
|
2000
|
+
end
|
|
2001
|
+
|
|
2002
|
+
# @note: This test is separated from the block above because YouTube only
|
|
2003
|
+
# returns file details for *some videos*: "The fileDetails object will
|
|
2004
|
+
# only be returned if the processingDetails.fileAvailability property
|
|
2005
|
+
# has a value of available.". Therefore, just to test fileDetails, we use a
|
|
2006
|
+
# different video that (for some unknown reason) is marked as 'available'.
|
|
2007
|
+
# Also note that I was not able to find a single video returning fileName,
|
|
2008
|
+
# therefore video.file_name is not returned by Yt, until it can be tested.
|
|
2009
|
+
# @see https://developers.google.com/youtube/v3/docs/videos#processingDetails.fileDetailsAvailability
|
|
2010
|
+
context 'given one of my own *available* videos' do
|
|
2011
|
+
let(:id) { 'yCmaOvUFhlI' }
|
|
2012
|
+
|
|
2013
|
+
it 'returns valid file details' do
|
|
2014
|
+
expect(video.file_size).to be_an Integer
|
|
2015
|
+
expect(video.file_type).to be_a String
|
|
2016
|
+
expect(video.container).to be_a String
|
|
2017
|
+
end
|
|
2018
|
+
end
|
|
2019
|
+
end
|
|
2020
|
+
# encoding: UTF-8
|
|
2021
|
+
|
|
2022
|
+
require 'spec_helper'
|
|
2023
|
+
require 'yt/models/video'
|
|
2024
|
+
|
|
2025
|
+
describe Yt::Video, :device_app do
|
|
2026
|
+
subject(:video) { Yt::Video.new id: id, auth: $account }
|
|
2027
|
+
|
|
2028
|
+
context 'given someone else’s video' do
|
|
2029
|
+
let(:id) { '9bZkp7q19f0' }
|
|
2030
|
+
|
|
2031
|
+
it { expect(video.content_detail).to be_a Yt::ContentDetail }
|
|
2032
|
+
|
|
2033
|
+
it 'returns valid metadata' do
|
|
2034
|
+
expect(video.title).to be_a String
|
|
2035
|
+
expect(video.description).to be_a String
|
|
2036
|
+
expect(video.thumbnail_url).to be_a String
|
|
2037
|
+
expect(video.published_at).to be_a Time
|
|
2038
|
+
expect(video.privacy_status).to be_a String
|
|
2039
|
+
expect(video.tags).to be_an Array
|
|
2040
|
+
expect(video.channel_id).to be_a String
|
|
2041
|
+
expect(video.channel_title).to be_a String
|
|
2042
|
+
expect(video.channel_url).to be_a String
|
|
2043
|
+
expect(video.category_id).to be_a String
|
|
2044
|
+
expect(video.live_broadcast_content).to be_a String
|
|
2045
|
+
expect(video.view_count).to be_an Integer
|
|
2046
|
+
expect(video.like_count).to be_an Integer
|
|
2047
|
+
expect(video.dislike_count).to be_an Integer
|
|
2048
|
+
expect(video.favorite_count).to be_an Integer
|
|
2049
|
+
expect(video.comment_count).to be_an Integer
|
|
2050
|
+
expect(video.duration).to be_an Integer
|
|
2051
|
+
expect(video.hd?).to be_in [true, false]
|
|
2052
|
+
expect(video.stereoscopic?).to be_in [true, false]
|
|
2053
|
+
expect(video.captioned?).to be_in [true, false]
|
|
2054
|
+
expect(video.licensed?).to be_in [true, false]
|
|
2055
|
+
expect(video.deleted?).to be_in [true, false]
|
|
2056
|
+
expect(video.failed?).to be_in [true, false]
|
|
2057
|
+
expect(video.processed?).to be_in [true, false]
|
|
2058
|
+
expect(video.rejected?).to be_in [true, false]
|
|
2059
|
+
expect(video.uploading?).to be_in [true, false]
|
|
2060
|
+
expect(video.uses_unsupported_codec?).to be_in [true, false]
|
|
2061
|
+
expect(video.has_failed_conversion?).to be_in [true, false]
|
|
2062
|
+
expect(video.empty?).to be_in [true, false]
|
|
2063
|
+
expect(video.invalid?).to be_in [true, false]
|
|
2064
|
+
expect(video.too_small?).to be_in [true, false]
|
|
2065
|
+
expect(video.aborted?).to be_in [true, false]
|
|
2066
|
+
expect(video.claimed?).to be_in [true, false]
|
|
2067
|
+
expect(video.infringes_copyright?).to be_in [true, false]
|
|
2068
|
+
expect(video.duplicate?).to be_in [true, false]
|
|
2069
|
+
expect(video.scheduled_at.class).to be_in [NilClass, Time]
|
|
2070
|
+
expect(video.scheduled?).to be_in [true, false]
|
|
2071
|
+
expect(video.too_long?).to be_in [true, false]
|
|
2072
|
+
expect(video.violates_terms_of_use?).to be_in [true, false]
|
|
2073
|
+
expect(video.inappropriate?).to be_in [true, false]
|
|
2074
|
+
expect(video.infringes_trademark?).to be_in [true, false]
|
|
2075
|
+
expect(video.belongs_to_closed_account?).to be_in [true, false]
|
|
2076
|
+
expect(video.belongs_to_suspended_account?).to be_in [true, false]
|
|
2077
|
+
expect(video.licensed_as_creative_commons?).to be_in [true, false]
|
|
2078
|
+
expect(video.licensed_as_standard_youtube?).to be_in [true, false]
|
|
2079
|
+
expect(video.has_public_stats_viewable?).to be_in [true, false]
|
|
2080
|
+
expect(video.embeddable?).to be_in [true, false]
|
|
2081
|
+
expect(video.actual_start_time).to be_nil
|
|
2082
|
+
expect(video.actual_end_time).to be_nil
|
|
2083
|
+
expect(video.scheduled_start_time).to be_nil
|
|
2084
|
+
expect(video.scheduled_end_time).to be_nil
|
|
2085
|
+
expect(video.concurrent_viewers).to be_nil
|
|
2086
|
+
expect(video.embed_html).to be_a String
|
|
2087
|
+
expect(video.category_title).to be_a String
|
|
2088
|
+
end
|
|
2089
|
+
|
|
2090
|
+
it { expect{video.update}.to fail }
|
|
2091
|
+
it { expect{video.delete}.to fail.with 'forbidden' }
|
|
2092
|
+
|
|
2093
|
+
context 'that I like' do
|
|
2094
|
+
before { video.like }
|
|
2095
|
+
it { expect(video).to be_liked }
|
|
2096
|
+
it { expect(video.dislike).to be true }
|
|
2097
|
+
end
|
|
2098
|
+
|
|
2099
|
+
context 'that I dislike' do
|
|
2100
|
+
before { video.dislike }
|
|
2101
|
+
it { expect(video).not_to be_liked }
|
|
2102
|
+
it { expect(video.like).to be true }
|
|
2103
|
+
end
|
|
2104
|
+
|
|
2105
|
+
context 'that I am indifferent to' do
|
|
2106
|
+
before { video.unlike }
|
|
2107
|
+
it { expect(video).not_to be_liked }
|
|
2108
|
+
it { expect(video.like).to be true }
|
|
2109
|
+
end
|
|
2110
|
+
end
|
|
2111
|
+
|
|
2112
|
+
context 'given someone else’s live video broadcast scheduled in the future' do
|
|
2113
|
+
let(:id) { 'PqzGI8gO_gk' }
|
|
2114
|
+
|
|
2115
|
+
it 'returns valid live streaming details' do
|
|
2116
|
+
expect(video.actual_start_time).to be_nil
|
|
2117
|
+
expect(video.actual_end_time).to be_nil
|
|
2118
|
+
expect(video.scheduled_start_time).to be_a Time
|
|
2119
|
+
expect(video.scheduled_end_time).to be_nil
|
|
2120
|
+
end
|
|
2121
|
+
end
|
|
2122
|
+
|
|
2123
|
+
context 'given someone else’s past live video broadcast' do
|
|
2124
|
+
let(:id) { 'COOM8_tOy6U' }
|
|
2125
|
+
|
|
2126
|
+
it 'returns valid live streaming details' do
|
|
2127
|
+
expect(video.actual_start_time).to be_a Time
|
|
2128
|
+
expect(video.actual_end_time).to be_a Time
|
|
2129
|
+
expect(video.scheduled_start_time).to be_a Time
|
|
2130
|
+
expect(video.scheduled_end_time).to be_a Time
|
|
2131
|
+
expect(video.concurrent_viewers).to be_nil
|
|
2132
|
+
end
|
|
2133
|
+
end
|
|
2134
|
+
|
|
2135
|
+
context 'given an unknown video' do
|
|
2136
|
+
let(:id) { 'not-a-video-id' }
|
|
2137
|
+
|
|
2138
|
+
it { expect{video.content_detail}.to raise_error Yt::Errors::NoItems }
|
|
2139
|
+
it { expect{video.snippet}.to raise_error Yt::Errors::NoItems }
|
|
2140
|
+
it { expect{video.rating}.to raise_error Yt::Errors::NoItems }
|
|
2141
|
+
it { expect{video.status}.to raise_error Yt::Errors::NoItems }
|
|
2142
|
+
it { expect{video.statistics_set}.to raise_error Yt::Errors::NoItems }
|
|
2143
|
+
it { expect{video.file_detail}.to raise_error Yt::Errors::NoItems }
|
|
2144
|
+
end
|
|
2145
|
+
|
|
2146
|
+
context 'given one of my own videos that I want to delete' do
|
|
2147
|
+
before(:all) { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: "Yt Test Delete Video #{rand}" }
|
|
2148
|
+
let(:id) { @tmp_video.id }
|
|
2149
|
+
|
|
2150
|
+
it { expect(video.delete).to be true }
|
|
2151
|
+
end
|
|
2152
|
+
|
|
2153
|
+
context 'given one of my own videos that I want to update' do
|
|
2154
|
+
let(:id) { $account.videos.where(order: 'viewCount').first.id }
|
|
2155
|
+
let!(:old_title) { video.title }
|
|
2156
|
+
let!(:old_privacy_status) { video.privacy_status }
|
|
2157
|
+
let(:update) { video.update attrs }
|
|
2158
|
+
|
|
2159
|
+
context 'given I update the title' do
|
|
2160
|
+
# NOTE: The use of UTF-8 characters is to test that we can pass up to
|
|
2161
|
+
# 50 characters, independently of their representation
|
|
2162
|
+
let(:attrs) { {title: "Yt Example Update Video #{rand} - ®•♡❥❦❧☙"} }
|
|
2163
|
+
|
|
2164
|
+
specify 'only updates the title' do
|
|
2165
|
+
expect(update).to be true
|
|
2166
|
+
expect(video.title).not_to eq old_title
|
|
2167
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
2168
|
+
end
|
|
2169
|
+
end
|
|
2170
|
+
|
|
2171
|
+
context 'given I update the description' do
|
|
2172
|
+
let!(:old_description) { video.description }
|
|
2173
|
+
let(:attrs) { {description: "Yt Example Description #{rand} - ®•♡❥❦❧☙"} }
|
|
2174
|
+
|
|
2175
|
+
specify 'only updates the description' do
|
|
2176
|
+
expect(update).to be true
|
|
2177
|
+
expect(video.description).not_to eq old_description
|
|
2178
|
+
expect(video.title).to eq old_title
|
|
2179
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
2180
|
+
end
|
|
2181
|
+
end
|
|
2182
|
+
|
|
2183
|
+
context 'given I update the tags' do
|
|
2184
|
+
let!(:old_tags) { video.tags }
|
|
2185
|
+
let(:attrs) { {tags: ["Yt Test Tag #{rand}"]} }
|
|
2186
|
+
|
|
2187
|
+
specify 'only updates the tag' do
|
|
2188
|
+
expect(update).to be true
|
|
2189
|
+
expect(video.tags).not_to eq old_tags
|
|
2190
|
+
expect(video.title).to eq old_title
|
|
2191
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
2192
|
+
end
|
|
2193
|
+
end
|
|
2194
|
+
|
|
2195
|
+
context 'given I update the category ID' do
|
|
2196
|
+
let!(:old_category_id) { video.category_id }
|
|
2197
|
+
let!(:new_category_id) { old_category_id == '22' ? '21' : '22' }
|
|
2198
|
+
|
|
2199
|
+
context 'passing the parameter in underscore syntax' do
|
|
2200
|
+
let(:attrs) { {category_id: new_category_id} }
|
|
2201
|
+
|
|
2202
|
+
specify 'only updates the category ID' do
|
|
2203
|
+
expect(update).to be true
|
|
2204
|
+
expect(video.category_id).not_to eq old_category_id
|
|
2205
|
+
expect(video.title).to eq old_title
|
|
2206
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
2207
|
+
end
|
|
2208
|
+
end
|
|
2209
|
+
|
|
2210
|
+
context 'passing the parameter in camel-case syntax' do
|
|
2211
|
+
let(:attrs) { {categoryId: new_category_id} }
|
|
2212
|
+
|
|
2213
|
+
specify 'only updates the category ID' do
|
|
2214
|
+
expect(update).to be true
|
|
2215
|
+
expect(video.category_id).not_to eq old_category_id
|
|
2216
|
+
end
|
|
2217
|
+
end
|
|
2218
|
+
end
|
|
2219
|
+
|
|
2220
|
+
context 'given I update title, description and/or tags using angle brackets' do
|
|
2221
|
+
let(:attrs) { {title: "Example Yt Test < >", description: '< >', tags: ['<tag>']} }
|
|
2222
|
+
|
|
2223
|
+
specify 'updates them replacing angle brackets with similar unicode characters accepted by YouTube' do
|
|
2224
|
+
expect(update).to be true
|
|
2225
|
+
expect(video.title).to eq 'Example Yt Test ‹ ›'
|
|
2226
|
+
expect(video.description).to eq '‹ ›'
|
|
2227
|
+
expect(video.tags).to eq ['‹tag›']
|
|
2228
|
+
end
|
|
2229
|
+
end
|
|
2230
|
+
|
|
2231
|
+
# note: 'scheduled' videos cannot be set to 'unlisted'
|
|
2232
|
+
context 'given I update the privacy status' do
|
|
2233
|
+
before { video.update publish_at: nil if video.scheduled? }
|
|
2234
|
+
let!(:new_privacy_status) { old_privacy_status == 'private' ? 'unlisted' : 'private' }
|
|
2235
|
+
|
|
2236
|
+
context 'passing the parameter in underscore syntax' do
|
|
2237
|
+
let(:attrs) { {privacy_status: new_privacy_status} }
|
|
2238
|
+
|
|
2239
|
+
specify 'only updates the privacy status' do
|
|
2240
|
+
expect(update).to be true
|
|
2241
|
+
expect(video.privacy_status).not_to eq old_privacy_status
|
|
2242
|
+
expect(video.title).to eq old_title
|
|
2243
|
+
end
|
|
2244
|
+
end
|
|
2245
|
+
|
|
2246
|
+
context 'passing the parameter in camel-case syntax' do
|
|
2247
|
+
let(:attrs) { {privacyStatus: new_privacy_status} }
|
|
2248
|
+
|
|
2249
|
+
specify 'only updates the privacy status' do
|
|
2250
|
+
expect(update).to be true
|
|
2251
|
+
expect(video.privacy_status).not_to eq old_privacy_status
|
|
2252
|
+
expect(video.title).to eq old_title
|
|
2253
|
+
end
|
|
2254
|
+
end
|
|
2255
|
+
end
|
|
2256
|
+
|
|
2257
|
+
context 'given I update the embeddable status' do
|
|
2258
|
+
let!(:old_embeddable) { video.embeddable? }
|
|
2259
|
+
let!(:new_embeddable) { !old_embeddable }
|
|
309
2260
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
2261
|
+
let(:attrs) { {embeddable: new_embeddable} }
|
|
2262
|
+
|
|
2263
|
+
# @note: This test is a reflection of another irrational behavior of
|
|
2264
|
+
# YouTube API. Although 'embeddable' can be passed as an 'update'
|
|
2265
|
+
# attribute according to the documentation, it simply does not work.
|
|
2266
|
+
# The day YouTube fixes it, then this test will finally fail and will
|
|
2267
|
+
# be removed, documenting how to update 'embeddable' too.
|
|
2268
|
+
# @see https://developers.google.com/youtube/v3/docs/videos/update
|
|
2269
|
+
# @see https://code.google.com/p/gdata-issues/issues/detail?id=4861
|
|
2270
|
+
specify 'does not update the embeddable status' do
|
|
2271
|
+
expect(update).to be true
|
|
2272
|
+
expect(video.embeddable?).to eq old_embeddable
|
|
2273
|
+
end
|
|
2274
|
+
end
|
|
2275
|
+
|
|
2276
|
+
context 'given I update the public stats viewable setting' do
|
|
2277
|
+
let!(:old_public_stats_viewable) { video.has_public_stats_viewable? }
|
|
2278
|
+
let!(:new_public_stats_viewable) { !old_public_stats_viewable }
|
|
2279
|
+
|
|
2280
|
+
context 'passing the parameter in underscore syntax' do
|
|
2281
|
+
let(:attrs) { {public_stats_viewable: new_public_stats_viewable} }
|
|
2282
|
+
|
|
2283
|
+
specify 'only updates the public stats viewable setting' do
|
|
2284
|
+
expect(update).to be true
|
|
2285
|
+
expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
|
|
2286
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
2287
|
+
expect(video.title).to eq old_title
|
|
2288
|
+
end
|
|
2289
|
+
end
|
|
2290
|
+
|
|
2291
|
+
context 'passing the parameter in camel-case syntax' do
|
|
2292
|
+
let(:attrs) { {publicStatsViewable: new_public_stats_viewable} }
|
|
2293
|
+
|
|
2294
|
+
specify 'only updates the public stats viewable setting' do
|
|
2295
|
+
expect(update).to be true
|
|
2296
|
+
expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
|
|
2297
|
+
expect(video.privacy_status).to eq old_privacy_status
|
|
2298
|
+
expect(video.title).to eq old_title
|
|
2299
|
+
end
|
|
2300
|
+
end
|
|
2301
|
+
end
|
|
2302
|
+
|
|
2303
|
+
it 'returns valid reports for video-related metrics' do
|
|
2304
|
+
# Some reports are only available to Content Owners.
|
|
2305
|
+
# See content owner test for more details about what the methods return.
|
|
2306
|
+
expect{video.views}.not_to raise_error
|
|
2307
|
+
expect{video.comments}.not_to raise_error
|
|
2308
|
+
expect{video.likes}.not_to raise_error
|
|
2309
|
+
expect{video.dislikes}.not_to raise_error
|
|
2310
|
+
expect{video.shares}.not_to raise_error
|
|
2311
|
+
expect{video.subscribers_gained}.not_to raise_error
|
|
2312
|
+
expect{video.subscribers_lost}.not_to raise_error
|
|
2313
|
+
expect{video.videos_added_to_playlists}.not_to raise_error
|
|
2314
|
+
expect{video.videos_removed_from_playlists}.not_to raise_error
|
|
2315
|
+
expect{video.estimated_minutes_watched}.not_to raise_error
|
|
2316
|
+
expect{video.average_view_duration}.not_to raise_error
|
|
2317
|
+
expect{video.average_view_percentage}.not_to raise_error
|
|
2318
|
+
expect{video.annotation_clicks}.not_to raise_error
|
|
2319
|
+
expect{video.annotation_click_through_rate}.not_to raise_error
|
|
2320
|
+
expect{video.annotation_close_rate}.not_to raise_error
|
|
2321
|
+
expect{video.viewer_percentage}.not_to raise_error
|
|
2322
|
+
expect{video.estimated_revenue}.to raise_error Yt::Errors::Unauthorized
|
|
2323
|
+
expect{video.ad_impressions}.to raise_error Yt::Errors::Unauthorized
|
|
2324
|
+
expect{video.monetized_playbacks}.to raise_error Yt::Errors::Unauthorized
|
|
2325
|
+
expect{video.playback_based_cpm}.to raise_error Yt::Errors::Unauthorized
|
|
2326
|
+
expect{video.advertising_options_set}.to raise_error Yt::Errors::Forbidden
|
|
326
2327
|
end
|
|
327
2328
|
end
|
|
328
2329
|
|