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