yt-andrewroth 0.25.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +27 -0
- data/.rspec +3 -0
- data/.travis.yml +9 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +732 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +489 -0
- data/Rakefile +11 -0
- data/YOUTUBE_IT.md +835 -0
- data/bin/yt +30 -0
- data/gemfiles/Gemfile.activesupport-3.x +4 -0
- data/gemfiles/Gemfile.activesupport-4.x +4 -0
- data/lib/yt.rb +21 -0
- data/lib/yt/actions/base.rb +32 -0
- data/lib/yt/actions/delete.rb +19 -0
- data/lib/yt/actions/delete_all.rb +32 -0
- data/lib/yt/actions/insert.rb +42 -0
- data/lib/yt/actions/list.rb +139 -0
- data/lib/yt/actions/modify.rb +37 -0
- data/lib/yt/actions/patch.rb +19 -0
- data/lib/yt/actions/update.rb +19 -0
- data/lib/yt/associations/has_attribute.rb +55 -0
- data/lib/yt/associations/has_authentication.rb +214 -0
- data/lib/yt/associations/has_many.rb +22 -0
- data/lib/yt/associations/has_one.rb +22 -0
- data/lib/yt/associations/has_reports.rb +320 -0
- data/lib/yt/collections/advertising_options_sets.rb +34 -0
- data/lib/yt/collections/annotations.rb +62 -0
- data/lib/yt/collections/assets.rb +58 -0
- data/lib/yt/collections/authentications.rb +47 -0
- data/lib/yt/collections/base.rb +62 -0
- data/lib/yt/collections/channels.rb +31 -0
- data/lib/yt/collections/claim_histories.rb +34 -0
- data/lib/yt/collections/claims.rb +56 -0
- data/lib/yt/collections/content_details.rb +30 -0
- data/lib/yt/collections/content_owner_details.rb +34 -0
- data/lib/yt/collections/content_owners.rb +32 -0
- data/lib/yt/collections/device_flows.rb +23 -0
- data/lib/yt/collections/file_details.rb +30 -0
- data/lib/yt/collections/ids.rb +27 -0
- data/lib/yt/collections/live_streaming_details.rb +30 -0
- data/lib/yt/collections/ownerships.rb +34 -0
- data/lib/yt/collections/partnered_channels.rb +28 -0
- data/lib/yt/collections/players.rb +30 -0
- data/lib/yt/collections/playlist_items.rb +53 -0
- data/lib/yt/collections/playlists.rb +28 -0
- data/lib/yt/collections/policies.rb +28 -0
- data/lib/yt/collections/ratings.rb +23 -0
- data/lib/yt/collections/references.rb +46 -0
- data/lib/yt/collections/related_playlists.rb +43 -0
- data/lib/yt/collections/reports.rb +161 -0
- data/lib/yt/collections/resources.rb +57 -0
- data/lib/yt/collections/resumable_sessions.rb +51 -0
- data/lib/yt/collections/snippets.rb +27 -0
- data/lib/yt/collections/statistics_sets.rb +30 -0
- data/lib/yt/collections/statuses.rb +27 -0
- data/lib/yt/collections/subscribed_channels.rb +46 -0
- data/lib/yt/collections/subscribers.rb +33 -0
- data/lib/yt/collections/subscriptions.rb +50 -0
- data/lib/yt/collections/user_infos.rb +36 -0
- data/lib/yt/collections/video_categories.rb +35 -0
- data/lib/yt/collections/videos.rb +137 -0
- data/lib/yt/config.rb +54 -0
- data/lib/yt/errors/forbidden.rb +13 -0
- data/lib/yt/errors/missing_auth.rb +81 -0
- data/lib/yt/errors/no_items.rb +13 -0
- data/lib/yt/errors/request_error.rb +74 -0
- data/lib/yt/errors/server_error.rb +13 -0
- data/lib/yt/errors/unauthorized.rb +50 -0
- data/lib/yt/models/account.rb +216 -0
- data/lib/yt/models/advertising_options_set.rb +38 -0
- data/lib/yt/models/annotation.rb +132 -0
- data/lib/yt/models/asset.rb +111 -0
- data/lib/yt/models/asset_metadata.rb +38 -0
- data/lib/yt/models/asset_snippet.rb +46 -0
- data/lib/yt/models/authentication.rb +83 -0
- data/lib/yt/models/base.rb +32 -0
- data/lib/yt/models/channel.rb +302 -0
- data/lib/yt/models/claim.rb +156 -0
- data/lib/yt/models/claim_event.rb +67 -0
- data/lib/yt/models/claim_history.rb +29 -0
- data/lib/yt/models/configuration.rb +70 -0
- data/lib/yt/models/content_detail.rb +65 -0
- data/lib/yt/models/content_owner.rb +48 -0
- data/lib/yt/models/content_owner_detail.rb +18 -0
- data/lib/yt/models/description.rb +58 -0
- data/lib/yt/models/device_flow.rb +16 -0
- data/lib/yt/models/file_detail.rb +21 -0
- data/lib/yt/models/id.rb +9 -0
- data/lib/yt/models/iterator.rb +16 -0
- data/lib/yt/models/live_streaming_detail.rb +23 -0
- data/lib/yt/models/match_policy.rb +34 -0
- data/lib/yt/models/ownership.rb +75 -0
- data/lib/yt/models/player.rb +18 -0
- data/lib/yt/models/playlist.rb +218 -0
- data/lib/yt/models/playlist_item.rb +112 -0
- data/lib/yt/models/policy.rb +36 -0
- data/lib/yt/models/policy_rule.rb +124 -0
- data/lib/yt/models/rating.rb +37 -0
- data/lib/yt/models/reference.rb +172 -0
- data/lib/yt/models/resource.rb +136 -0
- data/lib/yt/models/resumable_session.rb +52 -0
- data/lib/yt/models/right_owner.rb +58 -0
- data/lib/yt/models/snippet.rb +50 -0
- data/lib/yt/models/statistics_set.rb +26 -0
- data/lib/yt/models/status.rb +32 -0
- data/lib/yt/models/subscription.rb +38 -0
- data/lib/yt/models/timestamp.rb +13 -0
- data/lib/yt/models/url.rb +90 -0
- data/lib/yt/models/user_info.rb +26 -0
- data/lib/yt/models/video.rb +630 -0
- data/lib/yt/models/video_category.rb +12 -0
- data/lib/yt/request.rb +278 -0
- data/lib/yt/version.rb +3 -0
- data/spec/collections/claims_spec.rb +30 -0
- data/spec/collections/playlist_items_spec.rb +44 -0
- data/spec/collections/playlists_spec.rb +27 -0
- data/spec/collections/policies_spec.rb +30 -0
- data/spec/collections/references_spec.rb +30 -0
- data/spec/collections/reports_spec.rb +30 -0
- data/spec/collections/subscriptions_spec.rb +25 -0
- data/spec/collections/videos_spec.rb +43 -0
- data/spec/errors/forbidden_spec.rb +10 -0
- data/spec/errors/missing_auth_spec.rb +24 -0
- data/spec/errors/no_items_spec.rb +10 -0
- data/spec/errors/request_error_spec.rb +44 -0
- data/spec/errors/server_error_spec.rb +10 -0
- data/spec/errors/unauthorized_spec.rb +10 -0
- data/spec/models/account_spec.rb +138 -0
- data/spec/models/annotation_spec.rb +180 -0
- data/spec/models/asset_spec.rb +20 -0
- data/spec/models/channel_spec.rb +127 -0
- data/spec/models/claim_event_spec.rb +62 -0
- data/spec/models/claim_history_spec.rb +27 -0
- data/spec/models/claim_spec.rb +211 -0
- data/spec/models/configuration_spec.rb +44 -0
- data/spec/models/content_detail_spec.rb +45 -0
- data/spec/models/content_owner_detail_spec.rb +6 -0
- data/spec/models/description_spec.rb +94 -0
- data/spec/models/file_detail_spec.rb +13 -0
- data/spec/models/live_streaming_detail_spec.rb +6 -0
- data/spec/models/ownership_spec.rb +59 -0
- data/spec/models/player_spec.rb +13 -0
- data/spec/models/playlist_item_spec.rb +120 -0
- data/spec/models/playlist_spec.rb +138 -0
- data/spec/models/policy_rule_spec.rb +63 -0
- data/spec/models/policy_spec.rb +41 -0
- data/spec/models/rating_spec.rb +12 -0
- data/spec/models/reference_spec.rb +249 -0
- data/spec/models/request_spec.rb +163 -0
- data/spec/models/resource_spec.rb +57 -0
- data/spec/models/right_owner_spec.rb +71 -0
- data/spec/models/snippet_spec.rb +13 -0
- data/spec/models/statistics_set_spec.rb +13 -0
- data/spec/models/status_spec.rb +13 -0
- data/spec/models/subscription_spec.rb +30 -0
- data/spec/models/url_spec.rb +78 -0
- data/spec/models/video_category_spec.rb +21 -0
- data/spec/models/video_spec.rb +669 -0
- data/spec/requests/as_account/account_spec.rb +125 -0
- data/spec/requests/as_account/authentications_spec.rb +139 -0
- data/spec/requests/as_account/channel_spec.rb +259 -0
- data/spec/requests/as_account/channels_spec.rb +18 -0
- data/spec/requests/as_account/playlist_item_spec.rb +56 -0
- data/spec/requests/as_account/playlist_spec.rb +244 -0
- data/spec/requests/as_account/resource_spec.rb +18 -0
- data/spec/requests/as_account/thumbnail.jpg +0 -0
- data/spec/requests/as_account/video.mp4 +0 -0
- data/spec/requests/as_account/video_spec.rb +408 -0
- data/spec/requests/as_content_owner/account_spec.rb +25 -0
- data/spec/requests/as_content_owner/advertising_options_set_spec.rb +15 -0
- data/spec/requests/as_content_owner/asset_spec.rb +20 -0
- data/spec/requests/as_content_owner/channel_spec.rb +1934 -0
- data/spec/requests/as_content_owner/claim_history_spec.rb +20 -0
- data/spec/requests/as_content_owner/content_owner_spec.rb +241 -0
- data/spec/requests/as_content_owner/match_policy_spec.rb +17 -0
- data/spec/requests/as_content_owner/ownership_spec.rb +25 -0
- data/spec/requests/as_content_owner/playlist_spec.rb +782 -0
- data/spec/requests/as_content_owner/video_spec.rb +1239 -0
- data/spec/requests/as_server_app/channel_spec.rb +74 -0
- data/spec/requests/as_server_app/playlist_item_spec.rb +30 -0
- data/spec/requests/as_server_app/playlist_spec.rb +53 -0
- data/spec/requests/as_server_app/video_spec.rb +58 -0
- data/spec/requests/as_server_app/videos_spec.rb +40 -0
- data/spec/requests/unauthenticated/video_spec.rb +22 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/fail_matcher.rb +15 -0
- data/spec/support/global_hooks.rb +48 -0
- data/yt.gemspec +32 -0
- metadata +416 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'yt/models/account'
|
4
|
+
|
5
|
+
describe Yt::Account, :device_app do
|
6
|
+
describe 'can create playlists' do
|
7
|
+
let(:params) { {title: 'Test Yt playlist', privacy_status: 'unlisted'} }
|
8
|
+
before { @playlist = $account.create_playlist params }
|
9
|
+
it { expect(@playlist).to be_a Yt::Playlist }
|
10
|
+
after { @playlist.delete }
|
11
|
+
end
|
12
|
+
|
13
|
+
it { expect($account.channel).to be_a Yt::Channel }
|
14
|
+
it { expect($account.playlists.first).to be_a Yt::Playlist }
|
15
|
+
it { expect($account.subscribed_channels.first).to be_a Yt::Channel }
|
16
|
+
it { expect($account.user_info).to be_a Yt::UserInfo }
|
17
|
+
|
18
|
+
describe '.related_playlists' do
|
19
|
+
let(:related_playlists) { $account.related_playlists }
|
20
|
+
|
21
|
+
specify 'returns the list of associated playlist (Liked Videos, Uploads, ...)' do
|
22
|
+
expect(related_playlists.first).to be_a Yt::Playlist
|
23
|
+
end
|
24
|
+
|
25
|
+
specify 'includes public related playlists (such as Liked Videos)' do
|
26
|
+
uploads = related_playlists.select{|p| p.title.starts_with? 'Uploads'}
|
27
|
+
expect(uploads).not_to be_empty
|
28
|
+
end
|
29
|
+
|
30
|
+
specify 'includes private playlists (such as Watch Later or History)' do
|
31
|
+
watch_later = related_playlists.select{|p| p.title == 'Watch Later'}
|
32
|
+
expect(watch_later).not_to be_empty
|
33
|
+
|
34
|
+
history = related_playlists.select{|p| p.title == 'History'}
|
35
|
+
expect(history).not_to be_empty
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '.videos' do
|
40
|
+
let(:video) { $account.videos.where(order: 'viewCount').first }
|
41
|
+
|
42
|
+
specify 'returns the videos uploaded by the account with their tags and category ID' do
|
43
|
+
expect(video).to be_a Yt::Video
|
44
|
+
expect(video.tags).not_to be_empty
|
45
|
+
expect(video.category_id).not_to be_nil
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '.where(q: query_string)' do
|
49
|
+
let(:count) { $account.videos.where(q: query).count }
|
50
|
+
|
51
|
+
context 'given a query string that matches any video owned by the account' do
|
52
|
+
let(:query) { ENV['YT_TEST_MATCHING_QUERY_STRING'] }
|
53
|
+
it { expect(count).to be > 0 }
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'given a query string that does not match any video owned by the account' do
|
57
|
+
let(:query) { '--not-a-matching-query-string--' }
|
58
|
+
it { expect(count).to be_zero }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'ignores filters by ID (all the videos uploaded by the account are returned)' do
|
63
|
+
let(:other_video) { $account.videos.where(order: 'viewCount', id: 'invalid').first }
|
64
|
+
it { expect(other_video.id).to eq video.id }
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'ignores filters by chart (all the videos uploaded by the account are returned)' do
|
68
|
+
let(:other_video) { $account.videos.where(order: 'viewCount', chart: 'invalid').first }
|
69
|
+
it { expect(other_video.id).to eq video.id }
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '.includes(:snippet)' do
|
73
|
+
let(:video) { $account.videos.includes(:snippet).first }
|
74
|
+
|
75
|
+
specify 'eager-loads the *full* snippet of each video' do
|
76
|
+
expect(video.instance_variable_defined? :@snippet).to be true
|
77
|
+
expect(video.channel_title).to be
|
78
|
+
expect(video.snippet).to be_complete
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '.includes(:statistics, :status)' do
|
83
|
+
let(:video) { $account.videos.includes(:statistics, :status).first }
|
84
|
+
|
85
|
+
specify 'eager-loads the statistics and status of each video' do
|
86
|
+
expect(video.instance_variable_defined? :@statistics_set).to be true
|
87
|
+
expect(video.instance_variable_defined? :@status).to be true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '.includes(:content_details)' do
|
92
|
+
let(:video) { $account.videos.includes(:content_details).first }
|
93
|
+
|
94
|
+
specify 'eager-loads the statistics of each video' do
|
95
|
+
expect(video.instance_variable_defined? :@content_detail).to be true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '.upload_video' do
|
101
|
+
let(:video_params) { {title: 'Test Yt upload', privacy_status: 'private', category_id: 17} }
|
102
|
+
let(:video) { $account.upload_video path_or_url, video_params }
|
103
|
+
after { video.delete }
|
104
|
+
|
105
|
+
context 'given the path to a local video file' do
|
106
|
+
let(:path_or_url) { File.expand_path '../video.mp4', __FILE__ }
|
107
|
+
|
108
|
+
it { expect(video).to be_a Yt::Video }
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'given the URL of a remote video file' do
|
112
|
+
let(:path_or_url) { 'https://bit.ly/yt_test' }
|
113
|
+
|
114
|
+
it { expect(video).to be_a Yt::Video }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '.subscribers' do
|
119
|
+
let(:subscriber) { $account.subscribers.first }
|
120
|
+
|
121
|
+
specify 'returns the channels who are subscribed to me' do
|
122
|
+
expect(subscriber).to be_a Yt::Channel
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'yt/models/account'
|
3
|
+
|
4
|
+
describe Yt::Account, :device_app do
|
5
|
+
subject(:account) { Yt::Account.new attrs }
|
6
|
+
|
7
|
+
describe '#refresh' do
|
8
|
+
context 'given a valid refresh token' do
|
9
|
+
let(:attrs) { {refresh_token: ENV['YT_TEST_DEVICE_REFRESH_TOKEN']} }
|
10
|
+
|
11
|
+
# NOTE: When the token is refreshed, YouTube *might* actually return
|
12
|
+
# the *same* access token if it is still valid. Typically, within the
|
13
|
+
# same second, refreshing the token returns the same token. Still,
|
14
|
+
# testing that *expires_at* changes is a guarantee that we attempted
|
15
|
+
# to get a new token, which is what refresh is meant to do.
|
16
|
+
it { expect{account.refreshed_access_token?}.to change{account.expires_at} }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#authentication' do
|
21
|
+
context 'given a refresh token' do
|
22
|
+
let(:attrs) { {refresh_token: refresh_token} }
|
23
|
+
|
24
|
+
context 'that is valid' do
|
25
|
+
let(:refresh_token) { ENV['YT_TEST_DEVICE_REFRESH_TOKEN'] }
|
26
|
+
it { expect(account.authentication).to be_a Yt::Authentication }
|
27
|
+
it { expect(account.refresh_token).to eq refresh_token }
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'that is invalid' do
|
31
|
+
let(:refresh_token) { '--not-a-valid-refresh-token--' }
|
32
|
+
it { expect{account.authentication}.to raise_error Yt::Errors::Unauthorized }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'given a redirect URI and an authorization code' do
|
37
|
+
let(:attrs) { {authorization_code: authorization_code, redirect_uri: 'http://localhost/'} }
|
38
|
+
|
39
|
+
context 'that is valid' do
|
40
|
+
# cannot be tested "live"
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'that is invalid' do
|
44
|
+
let(:authorization_code) { '--not-a-valid-authorization-code--' }
|
45
|
+
it { expect{account.authentication}.to raise_error Yt::Errors::Unauthorized }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'given an access token' do
|
50
|
+
let(:attrs) { {access_token: access_token, expires_at: expires_at} }
|
51
|
+
|
52
|
+
context 'that is valid' do
|
53
|
+
let(:access_token) { $account.access_token }
|
54
|
+
|
55
|
+
context 'that does not have an expiration date' do
|
56
|
+
let(:expires_at) { nil }
|
57
|
+
it { expect(account.authentication).to be_a Yt::Authentication }
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'that has not expired' do
|
61
|
+
let(:expires_at) { 1.day.from_now.to_s }
|
62
|
+
it { expect(account.authentication).to be_a Yt::Authentication }
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'that has expired' do
|
66
|
+
let(:expires_at) { 1.day.ago.to_s }
|
67
|
+
|
68
|
+
context 'and no refresh token' do
|
69
|
+
it { expect{account.authentication}.to raise_error Yt::Errors::MissingAuth }
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'and an invalid refresh token' do
|
73
|
+
before { attrs[:refresh_token] = '--not-a-valid-refresh-token--' }
|
74
|
+
it { expect{account.authentication}.to raise_error Yt::Errors::Unauthorized }
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'and a valid refresh token' do
|
78
|
+
before { attrs[:refresh_token] = ENV['YT_TEST_DEVICE_REFRESH_TOKEN'] }
|
79
|
+
it { expect(account.authentication).to be_a Yt::Authentication }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'that is invalid' do
|
85
|
+
let(:access_token) { '--not-a-valid-access-token--' }
|
86
|
+
let(:expires_at) { 1.day.from_now }
|
87
|
+
|
88
|
+
context 'and no refresh token' do
|
89
|
+
it { expect{account.channel}.to raise_error Yt::Errors::Unauthorized }
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'and a valid refresh token' do
|
93
|
+
before { attrs[:refresh_token] = ENV['YT_TEST_DEVICE_REFRESH_TOKEN'] }
|
94
|
+
it { expect{account.channel}.not_to raise_error }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'given scopes' do
|
100
|
+
let(:attrs) { {scopes: ['userinfo.email', 'youtube']} }
|
101
|
+
|
102
|
+
context 'and a redirect_uri' do
|
103
|
+
before { attrs[:redirect_uri] = 'http://localhost/' }
|
104
|
+
|
105
|
+
it { expect{account.authentication}.to raise_error Yt::Errors::MissingAuth }
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'and no device token' do
|
109
|
+
it { expect{account.authentication}.to raise_error Yt::Errors::MissingAuth }
|
110
|
+
end
|
111
|
+
|
112
|
+
# NOTE: This test is commented out because of YouTube irrational behavior
|
113
|
+
# of using to return "MissingAuth" when passing a wrong device code, and
|
114
|
+
# now randomly returning `{"error"=>"internal_failure"}` instead.
|
115
|
+
# context 'and an invalid device code' do
|
116
|
+
# before { attrs[:device_code] = '--not-a-valid-device-code--' }
|
117
|
+
# it { expect{account.authentication}.to raise_error Yt::Errors::MissingAuth }
|
118
|
+
# end
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'given no token or code' do
|
122
|
+
let(:attrs) { {} }
|
123
|
+
it { expect{account.authentication}.to raise_error Yt::Errors::MissingAuth }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe '#authentication_url' do
|
128
|
+
let(:auth_attrs) { {redirect_uri: 'http://localhost/', scopes: ['userinfo.email', 'userinfo.profile']} }
|
129
|
+
context 'given a redirect URI and scopes' do
|
130
|
+
let(:attrs) { auth_attrs }
|
131
|
+
it { expect(account.authentication_url).to match 'access_type=offline' }
|
132
|
+
|
133
|
+
context 'given a forced approval prompt' do
|
134
|
+
let(:attrs) { auth_attrs.merge force: true }
|
135
|
+
it { expect(account.authentication_url).to match 'approval_prompt=force' }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'yt/models/channel'
|
4
|
+
|
5
|
+
describe Yt::Channel, :device_app do
|
6
|
+
subject(:channel) { Yt::Channel.new id: id, auth: $account }
|
7
|
+
|
8
|
+
context 'given someone else’s channel' do
|
9
|
+
let(:id) { 'UCxO1tY8h1AhOz0T4ENwmpow' }
|
10
|
+
|
11
|
+
it 'returns valid metadata' do
|
12
|
+
expect(channel.title).to be_a String
|
13
|
+
expect(channel.description).to be_a String
|
14
|
+
expect(channel.thumbnail_url).to be_a String
|
15
|
+
expect(channel.published_at).to be_a Time
|
16
|
+
expect(channel.privacy_status).to be_a String
|
17
|
+
expect(channel.view_count).to be_an Integer
|
18
|
+
expect(channel.comment_count).to be_an Integer
|
19
|
+
expect(channel.video_count).to be_an Integer
|
20
|
+
expect(channel.subscriber_count).to be_an Integer
|
21
|
+
expect(channel.subscriber_count_visible?).to be_in [true, false]
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '.videos' do
|
25
|
+
let(:video) { channel.videos.first }
|
26
|
+
|
27
|
+
specify 'returns the videos in the channel without tags or category ID' do
|
28
|
+
expect(video).to be_a Yt::Video
|
29
|
+
expect(video.snippet).not_to be_complete
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '.where(id: *anything*)' do
|
33
|
+
let(:video) { channel.videos.where(id: 'invalid').first }
|
34
|
+
|
35
|
+
specify 'is ignored (all the channel’s videos are returned)' do
|
36
|
+
expect(video).to be_a Yt::Video
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '.where(chart: *anything*)' do
|
41
|
+
let(:video) { channel.videos.where(chart: 'invalid').first }
|
42
|
+
|
43
|
+
specify 'is ignored (all the channel’s videos are returned)' do
|
44
|
+
expect(video).to be_a Yt::Video
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '.includes(:statistics, :status)' do
|
49
|
+
let(:video) { channel.videos.includes(:statistics, :status).first }
|
50
|
+
|
51
|
+
specify 'eager-loads the statistics and status of each video' do
|
52
|
+
expect(video.instance_variable_defined? :@statistics_set).to be true
|
53
|
+
expect(video.instance_variable_defined? :@status).to be true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '.includes(:content_details)' do
|
58
|
+
let(:video) { channel.videos.includes(:content_details).first }
|
59
|
+
|
60
|
+
specify 'eager-loads the statistics of each video' do
|
61
|
+
expect(video.instance_variable_defined? :@content_detail).to be true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '.includes(:category)' do
|
66
|
+
let(:video) { channel.videos.includes(:category, :status).first }
|
67
|
+
|
68
|
+
specify 'eager-loads the category (id and title) of each video' do
|
69
|
+
expect(video.instance_variable_defined? :@snippet).to be true
|
70
|
+
expect(video.instance_variable_defined? :@video_category).to be true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'when the channel has more than 500 videos' do
|
75
|
+
let(:id) { 'UC0v-tlzsn0QZwJnkiaUSJVQ' }
|
76
|
+
|
77
|
+
specify 'the estimated and actual number of videos can be retrieved' do
|
78
|
+
# @note: in principle, the following three counters should match, but
|
79
|
+
# in reality +video_count+ and +size+ are only approximations.
|
80
|
+
expect(channel.video_count).to be > 500
|
81
|
+
expect(channel.videos.size).to be > 500
|
82
|
+
end
|
83
|
+
|
84
|
+
specify 'over 500 videos can only be retrieved when sorting by date' do
|
85
|
+
# @note: these tests are slow because they go through multiple pages
|
86
|
+
# of results to test that we can overcome YouTube’s limitation of only
|
87
|
+
# returning the first 500 results when ordered by date.
|
88
|
+
expect(channel.videos.count).to be > 500
|
89
|
+
expect(channel.videos.where(order: 'viewCount').count).to be 500
|
90
|
+
end
|
91
|
+
|
92
|
+
specify 'over 500 videos can be retrieved even with a publishedBefore condition' do
|
93
|
+
# @note: these tests are slow because they go through multiple pages
|
94
|
+
# of results to test that we can overcome YouTube’s limitation of only
|
95
|
+
# returning the first 500 results when ordered by date.
|
96
|
+
today = Date.today.beginning_of_day.iso8601(0)
|
97
|
+
expect(channel.videos.where(published_before: today).count).to be > 500
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
it { expect(channel.playlists.first).to be_a Yt::Playlist }
|
103
|
+
it { expect{channel.delete_playlists}.to raise_error Yt::Errors::RequestError }
|
104
|
+
|
105
|
+
describe '.related_playlists' do
|
106
|
+
let(:related_playlists) { channel.related_playlists }
|
107
|
+
|
108
|
+
specify 'returns the list of associated playlist (Liked Videos, Uploads, ...)' do
|
109
|
+
expect(related_playlists.first).to be_a Yt::Playlist
|
110
|
+
end
|
111
|
+
|
112
|
+
specify 'includes public related playlists (such as Liked Videos)' do
|
113
|
+
uploads = related_playlists.select{|p| p.title.starts_with? 'Uploads'}
|
114
|
+
expect(uploads).not_to be_empty
|
115
|
+
end
|
116
|
+
|
117
|
+
specify 'does not includes private playlists (such as Watch Later)' do
|
118
|
+
watch_later = related_playlists.select{|p| p.title.starts_with? 'Watch'}
|
119
|
+
expect(watch_later).to be_empty
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
specify 'with a public list of subscriptions' do
|
124
|
+
expect(channel.subscribed_channels.first).to be_a Yt::Channel
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'with a hidden list of subscriptions' do
|
128
|
+
let(:id) { 'UCG0hw7n_v0sr8MXgb6oel6w' }
|
129
|
+
it { expect{channel.subscribed_channels.size}.to raise_error Yt::Errors::Forbidden }
|
130
|
+
end
|
131
|
+
|
132
|
+
# NOTE: These tests are slow because we *must* wait some seconds between
|
133
|
+
# subscribing and unsubscribing to a channel, otherwise YouTube will show
|
134
|
+
# wrong (cached) data, such as a user is subscribed when he is not.
|
135
|
+
context 'that I am not subscribed to', :slow do
|
136
|
+
let(:id) { 'UCCj956IF62FbT7Gouszaj9w' }
|
137
|
+
before { channel.throttle_subscriptions }
|
138
|
+
|
139
|
+
it { expect(channel.subscribed?).to be false }
|
140
|
+
it { expect(channel.unsubscribe).to be_falsey }
|
141
|
+
it { expect{channel.unsubscribe!}.to raise_error Yt::Errors::RequestError }
|
142
|
+
|
143
|
+
context 'when I subscribe' do
|
144
|
+
before { channel.subscribe }
|
145
|
+
after { channel.unsubscribe }
|
146
|
+
|
147
|
+
it { expect(channel.subscribed?).to be true }
|
148
|
+
it { expect(channel.unsubscribe!).to be_truthy }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'that I am subscribed to', :slow do
|
153
|
+
let(:id) { 'UCxO1tY8h1AhOz0T4ENwmpow' }
|
154
|
+
before { channel.throttle_subscriptions }
|
155
|
+
|
156
|
+
it { expect(channel.subscribed?).to be true }
|
157
|
+
it { expect(channel.subscribe).to be_falsey }
|
158
|
+
it { expect{channel.subscribe!}.to raise_error Yt::Errors::RequestError }
|
159
|
+
|
160
|
+
context 'when I unsubscribe' do
|
161
|
+
before { channel.unsubscribe }
|
162
|
+
after { channel.subscribe }
|
163
|
+
|
164
|
+
it { expect(channel.subscribed?).to be false }
|
165
|
+
it { expect(channel.subscribe!).to be_truthy }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context 'given my own channel' do
|
171
|
+
let(:id) { $account.channel.id }
|
172
|
+
let(:title) { 'Yt Test <title>' }
|
173
|
+
let(:description) { 'Yt Test <description>' }
|
174
|
+
let(:tags) { ['Yt Test Tag 1', 'Yt Test <Tag> 2'] }
|
175
|
+
let(:privacy_status) { 'unlisted' }
|
176
|
+
let(:params) { {title: title, description: description, tags: tags, privacy_status: privacy_status} }
|
177
|
+
|
178
|
+
specify 'subscriptions can be listed (hidden or public)' do
|
179
|
+
expect(channel.subscriptions.size).to be
|
180
|
+
end
|
181
|
+
|
182
|
+
describe 'playlists can be deleted' do
|
183
|
+
let(:title) { "Yt Test Delete All Playlists #{rand}" }
|
184
|
+
before { $account.create_playlist params }
|
185
|
+
|
186
|
+
it { expect(channel.delete_playlists title: %r{#{params[:title]}}).to eq [true] }
|
187
|
+
it { expect(channel.delete_playlists params).to eq [true] }
|
188
|
+
it { expect{channel.delete_playlists params}.to change{channel.playlists.count}.by(-1) }
|
189
|
+
end
|
190
|
+
|
191
|
+
# Can't subscribe to your own channel.
|
192
|
+
it { expect{channel.subscribe!}.to raise_error Yt::Errors::RequestError }
|
193
|
+
it { expect(channel.subscribe).to be_falsey }
|
194
|
+
|
195
|
+
it 'returns valid reports for channel-related metrics' do
|
196
|
+
# Some reports are only available to Content Owners.
|
197
|
+
# See content owner test for more details about what the methods return.
|
198
|
+
expect{channel.views}.not_to raise_error
|
199
|
+
expect{channel.comments}.not_to raise_error
|
200
|
+
expect{channel.likes}.not_to raise_error
|
201
|
+
expect{channel.dislikes}.not_to raise_error
|
202
|
+
expect{channel.shares}.not_to raise_error
|
203
|
+
expect{channel.subscribers_gained}.not_to raise_error
|
204
|
+
expect{channel.subscribers_lost}.not_to raise_error
|
205
|
+
expect{channel.favorites_added}.not_to raise_error
|
206
|
+
expect{channel.favorites_removed}.not_to raise_error
|
207
|
+
expect{channel.estimated_minutes_watched}.not_to raise_error
|
208
|
+
expect{channel.average_view_duration}.not_to raise_error
|
209
|
+
expect{channel.average_view_percentage}.not_to raise_error
|
210
|
+
expect{channel.annotation_clicks}.not_to raise_error
|
211
|
+
expect{channel.annotation_click_through_rate}.not_to raise_error
|
212
|
+
expect{channel.annotation_close_rate}.not_to raise_error
|
213
|
+
expect{channel.viewer_percentage}.not_to raise_error
|
214
|
+
expect{channel.earnings}.to raise_error Yt::Errors::Unauthorized
|
215
|
+
expect{channel.impressions}.to raise_error Yt::Errors::Unauthorized
|
216
|
+
expect{channel.monetized_playbacks}.to raise_error Yt::Errors::Unauthorized
|
217
|
+
expect{channel.playback_based_cpm}.to raise_error Yt::Errors::Unauthorized
|
218
|
+
|
219
|
+
expect{channel.views_on 3.days.ago}.not_to raise_error
|
220
|
+
expect{channel.comments_on 3.days.ago}.not_to raise_error
|
221
|
+
expect{channel.likes_on 3.days.ago}.not_to raise_error
|
222
|
+
expect{channel.dislikes_on 3.days.ago}.not_to raise_error
|
223
|
+
expect{channel.shares_on 3.days.ago}.not_to raise_error
|
224
|
+
expect{channel.subscribers_gained_on 3.days.ago}.not_to raise_error
|
225
|
+
expect{channel.subscribers_lost_on 3.days.ago}.not_to raise_error
|
226
|
+
expect{channel.favorites_added_on 3.days.ago}.not_to raise_error
|
227
|
+
expect{channel.favorites_removed_on 3.days.ago}.not_to raise_error
|
228
|
+
expect{channel.estimated_minutes_watched_on 3.days.ago}.not_to raise_error
|
229
|
+
expect{channel.average_view_duration_on 3.days.ago}.not_to raise_error
|
230
|
+
expect{channel.average_view_percentage_on 3.days.ago}.not_to raise_error
|
231
|
+
expect{channel.earnings_on 3.days.ago}.to raise_error Yt::Errors::Unauthorized
|
232
|
+
expect{channel.impressions_on 3.days.ago}.to raise_error Yt::Errors::Unauthorized
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'cannot give information about its content owner' do
|
236
|
+
expect{channel.content_owner}.to raise_error Yt::Errors::Forbidden
|
237
|
+
expect{channel.linked_at}.to raise_error Yt::Errors::Forbidden
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
context 'given an unknown channel' do
|
242
|
+
let(:id) { 'not-a-channel-id' }
|
243
|
+
|
244
|
+
it { expect{channel.snippet}.to raise_error Yt::Errors::NoItems }
|
245
|
+
it { expect{channel.status}.to raise_error Yt::Errors::NoItems }
|
246
|
+
it { expect{channel.statistics_set}.to raise_error Yt::Errors::NoItems }
|
247
|
+
it { expect{channel.subscribe}.to raise_error Yt::Errors::RequestError }
|
248
|
+
|
249
|
+
describe 'starting with UC' do
|
250
|
+
let(:id) { 'UC-not-a-channel-id' }
|
251
|
+
|
252
|
+
# NOTE: This test is just a reflection of YouTube irrational behavior of
|
253
|
+
# returns 0 results if the name of an unknown channel starts with UC, but
|
254
|
+
# returning 100,000 results otherwise (ignoring the channel filter).
|
255
|
+
it { expect(channel.videos.count).to be_zero }
|
256
|
+
it { expect(channel.videos.size).to be_zero }
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|