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.
Files changed (96) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +305 -1
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +86 -5
  5. data/YOUTUBE_IT.md +3 -3
  6. data/lib/yt.rb +5 -2
  7. data/lib/yt/actions/list.rb +3 -3
  8. data/lib/yt/associations/has_authentication.rb +33 -1
  9. data/lib/yt/associations/has_reports.rb +13 -18
  10. data/lib/yt/collections/assets.rb +2 -2
  11. data/lib/yt/collections/authentications.rb +9 -2
  12. data/lib/yt/collections/base.rb +3 -3
  13. data/lib/yt/collections/bulk_report_jobs.rb +28 -0
  14. data/lib/yt/collections/bulk_reports.rb +24 -0
  15. data/lib/yt/collections/claims.rb +22 -1
  16. data/lib/yt/collections/comment_threads.rb +41 -0
  17. data/lib/yt/collections/content_owners.rb +1 -1
  18. data/lib/yt/collections/group_infos.rb +27 -0
  19. data/lib/yt/collections/group_items.rb +45 -0
  20. data/lib/yt/collections/reports.rb +75 -13
  21. data/lib/yt/collections/revocations.rb +30 -0
  22. data/lib/yt/collections/video_groups.rb +29 -0
  23. data/lib/yt/collections/videos.rb +34 -9
  24. data/lib/yt/constants/geography.rb +326 -0
  25. data/lib/yt/errors/forbidden.rb +1 -3
  26. data/lib/yt/errors/no_items.rb +1 -3
  27. data/lib/yt/errors/request_error.rb +10 -7
  28. data/lib/yt/errors/server_error.rb +1 -3
  29. data/lib/yt/errors/unauthorized.rb +3 -3
  30. data/lib/yt/models/account.rb +12 -0
  31. data/lib/yt/models/advertising_options_set.rb +4 -4
  32. data/lib/yt/models/bulk_report.rb +23 -0
  33. data/lib/yt/models/bulk_report_job.rb +23 -0
  34. data/lib/yt/models/channel.rb +21 -12
  35. data/lib/yt/models/claim.rb +13 -2
  36. data/lib/yt/models/comment.rb +37 -0
  37. data/lib/yt/models/comment_thread.rb +50 -0
  38. data/lib/yt/models/content_detail.rb +6 -0
  39. data/lib/yt/models/content_owner.rb +31 -1
  40. data/lib/yt/models/group_info.rb +16 -0
  41. data/lib/yt/models/group_item.rb +15 -0
  42. data/lib/yt/models/resource.rb +3 -10
  43. data/lib/yt/models/revocation.rb +12 -0
  44. data/lib/yt/models/right_owner.rb +0 -2
  45. data/lib/yt/models/snippet.rb +24 -3
  46. data/lib/yt/models/video.rb +42 -11
  47. data/lib/yt/models/video_group.rb +186 -0
  48. data/lib/yt/request.rb +5 -3
  49. data/lib/yt/version.rb +2 -2
  50. data/spec/collections/comment_threads_spec.rb +46 -0
  51. data/spec/collections/playlist_items_spec.rb +1 -1
  52. data/spec/collections/reports_spec.rb +2 -2
  53. data/spec/constants/geography_spec.rb +16 -0
  54. data/spec/models/annotation_spec.rb +1 -1
  55. data/spec/models/claim_spec.rb +15 -3
  56. data/spec/models/comment_spec.rb +40 -0
  57. data/spec/models/comment_thread_spec.rb +93 -0
  58. data/spec/models/content_detail_spec.rb +7 -0
  59. data/spec/models/reference_spec.rb +2 -2
  60. data/spec/models/request_spec.rb +21 -0
  61. data/spec/models/resource_spec.rb +0 -15
  62. data/spec/models/video_spec.rb +1 -1
  63. data/spec/requests/as_account/account_spec.rb +16 -4
  64. data/spec/requests/as_account/authentications_spec.rb +1 -13
  65. data/spec/requests/as_account/channel_spec.rb +15 -45
  66. data/spec/requests/as_account/playlist_item_spec.rb +3 -3
  67. data/spec/requests/as_account/playlist_spec.rb +5 -32
  68. data/spec/requests/as_account/video_spec.rb +2022 -21
  69. data/spec/requests/as_content_owner/account_spec.rb +4 -0
  70. data/spec/requests/as_content_owner/bulk_report_job_spec.rb +19 -0
  71. data/spec/requests/as_content_owner/channel_spec.rb +59 -270
  72. data/spec/requests/as_content_owner/content_owner_spec.rb +89 -1
  73. data/spec/requests/as_content_owner/playlist_spec.rb +0 -15
  74. data/spec/requests/as_content_owner/video_group_spec.rb +112 -0
  75. data/spec/requests/as_content_owner/video_spec.rb +72 -146
  76. data/spec/requests/as_server_app/channel_spec.rb +1 -21
  77. data/spec/requests/as_server_app/comment_spec.rb +22 -0
  78. data/spec/requests/as_server_app/comment_thread_spec.rb +27 -0
  79. data/spec/requests/as_server_app/comment_threads_spec.rb +41 -0
  80. data/spec/requests/as_server_app/playlist_item_spec.rb +2 -2
  81. data/spec/requests/as_server_app/playlist_spec.rb +1 -22
  82. data/spec/requests/as_server_app/video_spec.rb +21 -19
  83. data/spec/requests/as_server_app/videos_spec.rb +5 -5
  84. data/spec/requests/unauthenticated/video_spec.rb +1 -9
  85. data/spec/spec_helper.rb +1 -1
  86. data/yt.gemspec +2 -1
  87. metadata +51 -17
  88. data/lib/yt/collections/ids.rb +0 -27
  89. data/lib/yt/config.rb +0 -54
  90. data/lib/yt/models/configuration.rb +0 -70
  91. data/lib/yt/models/description.rb +0 -58
  92. data/lib/yt/models/url.rb +0 -91
  93. data/spec/models/configuration_spec.rb +0 -44
  94. data/spec/models/description_spec.rb +0 -94
  95. data/spec/models/url_spec.rb +0 -84
  96. 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
- it { expect(channel.subscribe).to be_falsey }
168
- it { expect{channel.subscribe!}.to raise_error Yt::Errors::RequestError }
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.earnings}.to raise_error Yt::Errors::Unauthorized
227
- expect{channel.impressions}.to raise_error Yt::Errors::Unauthorized
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) { 'PLjW_GNR5Ir0GMlbJzA-aW0UV8TchJFb8p3uzrLNcZKPY' }
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 'MESycYJytkU'
36
- @my_playlist_item = @my_playlist.add_video 'MESycYJytkU'
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) { 'MESycYJytkU' }
60
+ let(:video_id) { '9bZkp7q19f0' }
73
61
 
74
- it { expect{playlist.delete}.to fail.with 'forbidden' }
75
- it { expect{playlist.update}.to fail.with 'forbidden' }
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) { 'MESycYJytkU' }
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) { ['MESycYJytkU', 'not-a-video'] }
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) { 'MESycYJytkU' }
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.earnings}.to raise_error Yt::Errors::Unauthorized
305
- expect{video.impressions}.to raise_error Yt::Errors::Unauthorized
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
- expect{video.views_on 3.days.ago}.not_to raise_error
311
- expect{video.comments_on 3.days.ago}.not_to raise_error
312
- expect{video.likes_on 3.days.ago}.not_to raise_error
313
- expect{video.dislikes_on 3.days.ago}.not_to raise_error
314
- expect{video.shares_on 3.days.ago}.not_to raise_error
315
- expect{video.subscribers_gained_on 3.days.ago}.not_to raise_error
316
- expect{video.subscribers_lost_on 3.days.ago}.not_to raise_error
317
- expect{video.favorites_added_on 3.days.ago}.not_to raise_error
318
- expect{video.favorites_removed_on 3.days.ago}.not_to raise_error
319
- expect{video.videos_added_to_playlists_on 3.days.ago}.not_to raise_error
320
- expect{video.videos_removed_from_playlists_on 3.days.ago}.not_to raise_error
321
- expect{video.estimated_minutes_watched_on 3.days.ago}.not_to raise_error
322
- expect{video.average_view_duration_on 3.days.ago}.not_to raise_error
323
- expect{video.average_view_percentage_on 3.days.ago}.not_to raise_error
324
- expect{video.earnings_on 3.days.ago}.to raise_error Yt::Errors::Unauthorized
325
- expect{video.impressions_on 3.days.ago}.to raise_error Yt::Errors::Unauthorized
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