yt 0.25.40 → 0.26.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4b0ee675025a4e3373b1281cfa5097498aec15ab
4
- data.tar.gz: ea6970b4d354cebcc440ad32e0d7855ec437b936
3
+ metadata.gz: 84c5ba72d6991b9fb6c57f489bd30a5647bf4930
4
+ data.tar.gz: 28c51f4ae24aaa298b08983762cc85ea687057af
5
5
  SHA512:
6
- metadata.gz: 2c3a000be6dc7c31d03964b9dc5f27442079e177fd4f867bc0a663833c709653d30c7c6dde977e4df8d6b675c45cb8fa63c8331537b1e0215a36bf19bb392221
7
- data.tar.gz: 9203eb0e4dc5e84926e49b6c14830507753d7e52656c48da58816fb6637e14d3d332be9041f1acdd76d41ccace0f0aa8bf5e8e722ba3a00d110924d0df15a6ab
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
@@ -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
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Yt
2
- VERSION = '0.25.40'
2
+ VERSION = '0.26.0'
3
3
  end
@@ -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.favorites_added_on 3.days.ago}.not_to raise_error
318
- expect{video.favorites_removed_on 3.days.ago}.not_to raise_error
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