yt-andrewroth 0.25.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (191) hide show
  1. data/.gitignore +27 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +9 -0
  4. data/.yardopts +5 -0
  5. data/CHANGELOG.md +732 -0
  6. data/Gemfile +4 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +489 -0
  9. data/Rakefile +11 -0
  10. data/YOUTUBE_IT.md +835 -0
  11. data/bin/yt +30 -0
  12. data/gemfiles/Gemfile.activesupport-3.x +4 -0
  13. data/gemfiles/Gemfile.activesupport-4.x +4 -0
  14. data/lib/yt.rb +21 -0
  15. data/lib/yt/actions/base.rb +32 -0
  16. data/lib/yt/actions/delete.rb +19 -0
  17. data/lib/yt/actions/delete_all.rb +32 -0
  18. data/lib/yt/actions/insert.rb +42 -0
  19. data/lib/yt/actions/list.rb +139 -0
  20. data/lib/yt/actions/modify.rb +37 -0
  21. data/lib/yt/actions/patch.rb +19 -0
  22. data/lib/yt/actions/update.rb +19 -0
  23. data/lib/yt/associations/has_attribute.rb +55 -0
  24. data/lib/yt/associations/has_authentication.rb +214 -0
  25. data/lib/yt/associations/has_many.rb +22 -0
  26. data/lib/yt/associations/has_one.rb +22 -0
  27. data/lib/yt/associations/has_reports.rb +320 -0
  28. data/lib/yt/collections/advertising_options_sets.rb +34 -0
  29. data/lib/yt/collections/annotations.rb +62 -0
  30. data/lib/yt/collections/assets.rb +58 -0
  31. data/lib/yt/collections/authentications.rb +47 -0
  32. data/lib/yt/collections/base.rb +62 -0
  33. data/lib/yt/collections/channels.rb +31 -0
  34. data/lib/yt/collections/claim_histories.rb +34 -0
  35. data/lib/yt/collections/claims.rb +56 -0
  36. data/lib/yt/collections/content_details.rb +30 -0
  37. data/lib/yt/collections/content_owner_details.rb +34 -0
  38. data/lib/yt/collections/content_owners.rb +32 -0
  39. data/lib/yt/collections/device_flows.rb +23 -0
  40. data/lib/yt/collections/file_details.rb +30 -0
  41. data/lib/yt/collections/ids.rb +27 -0
  42. data/lib/yt/collections/live_streaming_details.rb +30 -0
  43. data/lib/yt/collections/ownerships.rb +34 -0
  44. data/lib/yt/collections/partnered_channels.rb +28 -0
  45. data/lib/yt/collections/players.rb +30 -0
  46. data/lib/yt/collections/playlist_items.rb +53 -0
  47. data/lib/yt/collections/playlists.rb +28 -0
  48. data/lib/yt/collections/policies.rb +28 -0
  49. data/lib/yt/collections/ratings.rb +23 -0
  50. data/lib/yt/collections/references.rb +46 -0
  51. data/lib/yt/collections/related_playlists.rb +43 -0
  52. data/lib/yt/collections/reports.rb +161 -0
  53. data/lib/yt/collections/resources.rb +57 -0
  54. data/lib/yt/collections/resumable_sessions.rb +51 -0
  55. data/lib/yt/collections/snippets.rb +27 -0
  56. data/lib/yt/collections/statistics_sets.rb +30 -0
  57. data/lib/yt/collections/statuses.rb +27 -0
  58. data/lib/yt/collections/subscribed_channels.rb +46 -0
  59. data/lib/yt/collections/subscribers.rb +33 -0
  60. data/lib/yt/collections/subscriptions.rb +50 -0
  61. data/lib/yt/collections/user_infos.rb +36 -0
  62. data/lib/yt/collections/video_categories.rb +35 -0
  63. data/lib/yt/collections/videos.rb +137 -0
  64. data/lib/yt/config.rb +54 -0
  65. data/lib/yt/errors/forbidden.rb +13 -0
  66. data/lib/yt/errors/missing_auth.rb +81 -0
  67. data/lib/yt/errors/no_items.rb +13 -0
  68. data/lib/yt/errors/request_error.rb +74 -0
  69. data/lib/yt/errors/server_error.rb +13 -0
  70. data/lib/yt/errors/unauthorized.rb +50 -0
  71. data/lib/yt/models/account.rb +216 -0
  72. data/lib/yt/models/advertising_options_set.rb +38 -0
  73. data/lib/yt/models/annotation.rb +132 -0
  74. data/lib/yt/models/asset.rb +111 -0
  75. data/lib/yt/models/asset_metadata.rb +38 -0
  76. data/lib/yt/models/asset_snippet.rb +46 -0
  77. data/lib/yt/models/authentication.rb +83 -0
  78. data/lib/yt/models/base.rb +32 -0
  79. data/lib/yt/models/channel.rb +302 -0
  80. data/lib/yt/models/claim.rb +156 -0
  81. data/lib/yt/models/claim_event.rb +67 -0
  82. data/lib/yt/models/claim_history.rb +29 -0
  83. data/lib/yt/models/configuration.rb +70 -0
  84. data/lib/yt/models/content_detail.rb +65 -0
  85. data/lib/yt/models/content_owner.rb +48 -0
  86. data/lib/yt/models/content_owner_detail.rb +18 -0
  87. data/lib/yt/models/description.rb +58 -0
  88. data/lib/yt/models/device_flow.rb +16 -0
  89. data/lib/yt/models/file_detail.rb +21 -0
  90. data/lib/yt/models/id.rb +9 -0
  91. data/lib/yt/models/iterator.rb +16 -0
  92. data/lib/yt/models/live_streaming_detail.rb +23 -0
  93. data/lib/yt/models/match_policy.rb +34 -0
  94. data/lib/yt/models/ownership.rb +75 -0
  95. data/lib/yt/models/player.rb +18 -0
  96. data/lib/yt/models/playlist.rb +218 -0
  97. data/lib/yt/models/playlist_item.rb +112 -0
  98. data/lib/yt/models/policy.rb +36 -0
  99. data/lib/yt/models/policy_rule.rb +124 -0
  100. data/lib/yt/models/rating.rb +37 -0
  101. data/lib/yt/models/reference.rb +172 -0
  102. data/lib/yt/models/resource.rb +136 -0
  103. data/lib/yt/models/resumable_session.rb +52 -0
  104. data/lib/yt/models/right_owner.rb +58 -0
  105. data/lib/yt/models/snippet.rb +50 -0
  106. data/lib/yt/models/statistics_set.rb +26 -0
  107. data/lib/yt/models/status.rb +32 -0
  108. data/lib/yt/models/subscription.rb +38 -0
  109. data/lib/yt/models/timestamp.rb +13 -0
  110. data/lib/yt/models/url.rb +90 -0
  111. data/lib/yt/models/user_info.rb +26 -0
  112. data/lib/yt/models/video.rb +630 -0
  113. data/lib/yt/models/video_category.rb +12 -0
  114. data/lib/yt/request.rb +278 -0
  115. data/lib/yt/version.rb +3 -0
  116. data/spec/collections/claims_spec.rb +30 -0
  117. data/spec/collections/playlist_items_spec.rb +44 -0
  118. data/spec/collections/playlists_spec.rb +27 -0
  119. data/spec/collections/policies_spec.rb +30 -0
  120. data/spec/collections/references_spec.rb +30 -0
  121. data/spec/collections/reports_spec.rb +30 -0
  122. data/spec/collections/subscriptions_spec.rb +25 -0
  123. data/spec/collections/videos_spec.rb +43 -0
  124. data/spec/errors/forbidden_spec.rb +10 -0
  125. data/spec/errors/missing_auth_spec.rb +24 -0
  126. data/spec/errors/no_items_spec.rb +10 -0
  127. data/spec/errors/request_error_spec.rb +44 -0
  128. data/spec/errors/server_error_spec.rb +10 -0
  129. data/spec/errors/unauthorized_spec.rb +10 -0
  130. data/spec/models/account_spec.rb +138 -0
  131. data/spec/models/annotation_spec.rb +180 -0
  132. data/spec/models/asset_spec.rb +20 -0
  133. data/spec/models/channel_spec.rb +127 -0
  134. data/spec/models/claim_event_spec.rb +62 -0
  135. data/spec/models/claim_history_spec.rb +27 -0
  136. data/spec/models/claim_spec.rb +211 -0
  137. data/spec/models/configuration_spec.rb +44 -0
  138. data/spec/models/content_detail_spec.rb +45 -0
  139. data/spec/models/content_owner_detail_spec.rb +6 -0
  140. data/spec/models/description_spec.rb +94 -0
  141. data/spec/models/file_detail_spec.rb +13 -0
  142. data/spec/models/live_streaming_detail_spec.rb +6 -0
  143. data/spec/models/ownership_spec.rb +59 -0
  144. data/spec/models/player_spec.rb +13 -0
  145. data/spec/models/playlist_item_spec.rb +120 -0
  146. data/spec/models/playlist_spec.rb +138 -0
  147. data/spec/models/policy_rule_spec.rb +63 -0
  148. data/spec/models/policy_spec.rb +41 -0
  149. data/spec/models/rating_spec.rb +12 -0
  150. data/spec/models/reference_spec.rb +249 -0
  151. data/spec/models/request_spec.rb +163 -0
  152. data/spec/models/resource_spec.rb +57 -0
  153. data/spec/models/right_owner_spec.rb +71 -0
  154. data/spec/models/snippet_spec.rb +13 -0
  155. data/spec/models/statistics_set_spec.rb +13 -0
  156. data/spec/models/status_spec.rb +13 -0
  157. data/spec/models/subscription_spec.rb +30 -0
  158. data/spec/models/url_spec.rb +78 -0
  159. data/spec/models/video_category_spec.rb +21 -0
  160. data/spec/models/video_spec.rb +669 -0
  161. data/spec/requests/as_account/account_spec.rb +125 -0
  162. data/spec/requests/as_account/authentications_spec.rb +139 -0
  163. data/spec/requests/as_account/channel_spec.rb +259 -0
  164. data/spec/requests/as_account/channels_spec.rb +18 -0
  165. data/spec/requests/as_account/playlist_item_spec.rb +56 -0
  166. data/spec/requests/as_account/playlist_spec.rb +244 -0
  167. data/spec/requests/as_account/resource_spec.rb +18 -0
  168. data/spec/requests/as_account/thumbnail.jpg +0 -0
  169. data/spec/requests/as_account/video.mp4 +0 -0
  170. data/spec/requests/as_account/video_spec.rb +408 -0
  171. data/spec/requests/as_content_owner/account_spec.rb +25 -0
  172. data/spec/requests/as_content_owner/advertising_options_set_spec.rb +15 -0
  173. data/spec/requests/as_content_owner/asset_spec.rb +20 -0
  174. data/spec/requests/as_content_owner/channel_spec.rb +1934 -0
  175. data/spec/requests/as_content_owner/claim_history_spec.rb +20 -0
  176. data/spec/requests/as_content_owner/content_owner_spec.rb +241 -0
  177. data/spec/requests/as_content_owner/match_policy_spec.rb +17 -0
  178. data/spec/requests/as_content_owner/ownership_spec.rb +25 -0
  179. data/spec/requests/as_content_owner/playlist_spec.rb +782 -0
  180. data/spec/requests/as_content_owner/video_spec.rb +1239 -0
  181. data/spec/requests/as_server_app/channel_spec.rb +74 -0
  182. data/spec/requests/as_server_app/playlist_item_spec.rb +30 -0
  183. data/spec/requests/as_server_app/playlist_spec.rb +53 -0
  184. data/spec/requests/as_server_app/video_spec.rb +58 -0
  185. data/spec/requests/as_server_app/videos_spec.rb +40 -0
  186. data/spec/requests/unauthenticated/video_spec.rb +22 -0
  187. data/spec/spec_helper.rb +20 -0
  188. data/spec/support/fail_matcher.rb +15 -0
  189. data/spec/support/global_hooks.rb +48 -0
  190. data/yt.gemspec +32 -0
  191. 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