yt 0.25.40 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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