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,18 @@
1
+ # encoding: UTF-8
2
+ require 'spec_helper'
3
+ require 'yt/collections/channels'
4
+
5
+ describe Yt::Collections::Channels, :device_app do
6
+ subject(:channels) { Yt::Collections::Channels.new auth: $account }
7
+
8
+ context 'with a list of parts' do
9
+ let(:part) { 'statistics' }
10
+ let(:channel) { channels.where(part: part, id: 'UCxO1tY8h1AhOz0T4ENwmpow').first }
11
+
12
+ specify 'load ONLY the specified parts of the channels' do
13
+ expect(channel.instance_variable_defined? :@snippet).to be false
14
+ expect(channel.instance_variable_defined? :@status).to be false
15
+ expect(channel.instance_variable_defined? :@statistics_set).to be true
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+ require 'yt/models/playlist_item'
3
+
4
+ describe Yt::PlaylistItem, :device_app do
5
+ subject(:item) { Yt::PlaylistItem.new id: id, auth: $account }
6
+
7
+ context 'given an existing playlist item' do
8
+ let(:id) { 'PLjW_GNR5Ir0GMlbJzA-aW0UV8TchJFb8p3uzrLNcZKPY' }
9
+
10
+ it 'returns valid metadata' do
11
+ expect(item.title).to be_a String
12
+ expect(item.description).to be_a String
13
+ expect(item.thumbnail_url).to be_a String
14
+ expect(item.published_at).to be_a Time
15
+ expect(item.channel_id).to be_a String
16
+ expect(item.channel_title).to be_a String
17
+ expect(item.playlist_id).to be_a String
18
+ expect(item.position).to be_an Integer
19
+ expect(item.video_id).to be_a String
20
+ expect(item.video).to be_a Yt::Video
21
+ expect(item.privacy_status).to be_a String
22
+ end
23
+ end
24
+
25
+ context 'given an unknown playlist item' do
26
+ let(:id) { 'not-a-playlist-item-id' }
27
+
28
+ it { expect{item.snippet}.to raise_error Yt::Errors::RequestError }
29
+ end
30
+
31
+
32
+ context 'given one of my own playlist items that I want to update' do
33
+ before(:all) do
34
+ @my_playlist = $account.create_playlist title: "Yt Test Update Playlist Item #{rand}"
35
+ @my_playlist.add_video 'MESycYJytkU'
36
+ @my_playlist_item = @my_playlist.add_video 'MESycYJytkU'
37
+ end
38
+ after(:all) { @my_playlist.delete }
39
+
40
+ let(:id) { @my_playlist_item.id }
41
+ let!(:old_title) { @my_playlist_item.title }
42
+ let!(:old_privacy_status) { @my_playlist_item.privacy_status }
43
+ let(:update) { @my_playlist_item.update attrs }
44
+
45
+ context 'given I update the position' do
46
+ let(:attrs) { {position: 0} }
47
+
48
+ specify 'only updates the position' do
49
+ expect(update).to be true
50
+ expect(@my_playlist_item.position).to be 0
51
+ expect(@my_playlist_item.title).to eq old_title
52
+ expect(@my_playlist_item.privacy_status).to eq old_privacy_status
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,244 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+ require 'yt/models/playlist'
5
+
6
+ describe Yt::Playlist, :device_app do
7
+ subject(:playlist) { Yt::Playlist.new id: id, auth: $account }
8
+
9
+ context 'given an existing playlist' do
10
+ let(:id) { 'PLSWYkYzOrPMT9pJG5St5G0WDalhRzGkU4' }
11
+
12
+ it 'returns valid metadata' do
13
+ expect(playlist.title).to be_a String
14
+ expect(playlist.description).to be_a String
15
+ expect(playlist.thumbnail_url).to be_a String
16
+ expect(playlist.published_at).to be_a Time
17
+ expect(playlist.tags).to be_an Array
18
+ expect(playlist.channel_id).to be_a String
19
+ expect(playlist.channel_title).to be_a String
20
+ expect(playlist.privacy_status).to be_a String
21
+ end
22
+
23
+ describe '.playlist_items' do
24
+ let(:item) { playlist.playlist_items.first }
25
+
26
+ specify 'returns the playlist item with the complete snippet' do
27
+ expect(item).to be_a Yt::PlaylistItem
28
+ expect(item.snippet).to be_complete
29
+ expect(item.position).not_to be_nil
30
+ end
31
+
32
+ specify 'does not eager-load the attributes of the item’s video' do
33
+ expect(item.video.instance_variable_defined? :@snippet).to be false
34
+ expect(item.video.instance_variable_defined? :@status).to be false
35
+ expect(item.video.instance_variable_defined? :@statistics_set).to be false
36
+ end
37
+ end
38
+
39
+ describe '.playlist_items.includes(:video)' do
40
+ let(:item) { playlist.playlist_items.includes(:video).first }
41
+
42
+ specify 'eager-loads the snippet, status and statistics of each video' do
43
+ expect(item.video.instance_variable_defined? :@snippet).to be true
44
+ expect(item.video.instance_variable_defined? :@status).to be true
45
+ expect(item.video.instance_variable_defined? :@statistics_set).to be true
46
+ end
47
+ end
48
+ end
49
+
50
+ context 'given a playlist that only includes other people’s private or deleted videos' do
51
+ let(:id) { 'PLsnYEvcCzABOsJdehqkIDhwz8CPGWzX59' }
52
+
53
+ describe '.playlist_items.includes(:video)' do
54
+ let(:items) { playlist.playlist_items.includes(:video).map{|i| i} }
55
+
56
+ specify 'returns nil (without running an infinite loop)' do
57
+ expect(items.size).to be 2
58
+ end
59
+ end
60
+ end
61
+
62
+ context 'given an unknown playlist' do
63
+ let(:id) { 'not-a-playlist-id' }
64
+
65
+ it { expect{playlist.snippet}.to raise_error Yt::Errors::NoItems }
66
+ it { expect{playlist.status}.to raise_error Yt::Errors::NoItems }
67
+ end
68
+
69
+ context 'given someone else’s playlist' do
70
+ let(:id) { 'PLSWYkYzOrPMT9pJG5St5G0WDalhRzGkU4' }
71
+ let(:video_id) { 'MESycYJytkU' }
72
+
73
+ it { expect{playlist.delete}.to fail.with 'forbidden' }
74
+ it { expect{playlist.update}.to fail.with 'forbidden' }
75
+ it { expect{playlist.add_video! video_id}.to raise_error Yt::Errors::RequestError }
76
+ it { expect{playlist.delete_playlist_items}.to raise_error Yt::Errors::RequestError }
77
+ end
78
+
79
+ context 'given one of my own playlists that I want to delete' do
80
+ before(:all) { @my_playlist = $account.create_playlist title: "Yt Test Delete Playlist #{rand}" }
81
+ let(:id) { @my_playlist.id }
82
+
83
+ it { expect(playlist.delete).to be true }
84
+ end
85
+
86
+ context 'given one of my own playlists that I want to update' do
87
+ before(:all) { @my_playlist = $account.create_playlist title: "Yt Test Update Playlist #{rand}" }
88
+ after(:all) { @my_playlist.delete }
89
+ let(:id) { @my_playlist.id }
90
+ let!(:old_title) { @my_playlist.title }
91
+ let!(:old_privacy_status) { @my_playlist.privacy_status }
92
+ let(:update) { @my_playlist.update attrs }
93
+
94
+ context 'given I update the title' do
95
+ # NOTE: The use of UTF-8 characters is to test that we can pass up to
96
+ # 50 characters, independently of their representation
97
+ let(:attrs) { {title: "Yt Example Update Playlist #{rand} - ®•♡❥❦❧☙"} }
98
+
99
+ specify 'only updates the title' do
100
+ expect(update).to be true
101
+ expect(@my_playlist.title).not_to eq old_title
102
+ expect(@my_playlist.privacy_status).to eq old_privacy_status
103
+ end
104
+ end
105
+
106
+ context 'given I update the description' do
107
+ let!(:old_description) { @my_playlist.description }
108
+ let(:attrs) { {description: "Yt Example Description #{rand} - ®•♡❥❦❧☙"} }
109
+
110
+ specify 'only updates the description' do
111
+ expect(update).to be true
112
+ expect(@my_playlist.description).not_to eq old_description
113
+ expect(@my_playlist.title).to eq old_title
114
+ expect(@my_playlist.privacy_status).to eq old_privacy_status
115
+ end
116
+ end
117
+
118
+ context 'given I update the tags' do
119
+ let!(:old_tags) { @my_playlist.tags }
120
+ let(:attrs) { {tags: ["Yt Test Tag #{rand}"]} }
121
+
122
+ specify 'only updates the tag' do
123
+ expect(update).to be true
124
+ expect(@my_playlist.tags).not_to eq old_tags
125
+ expect(@my_playlist.title).to eq old_title
126
+ expect(@my_playlist.privacy_status).to eq old_privacy_status
127
+ end
128
+ end
129
+
130
+ context 'given I update title, description and/or tags using angle brackets' do
131
+ let(:attrs) { {title: "Yt Test < >", description: '< >', tags: ['<tag>']} }
132
+
133
+ specify 'updates them replacing angle brackets with similar unicode characters accepted by YouTube' do
134
+ expect(update).to be true
135
+ expect(playlist.title).to eq 'Yt Test ‹ ›'
136
+ expect(playlist.description).to eq '‹ ›'
137
+ expect(playlist.tags).to eq ['‹tag›']
138
+ end
139
+ end
140
+
141
+ context 'given I update the privacy status' do
142
+ let!(:new_privacy_status) { old_privacy_status == 'private' ? 'unlisted' : 'private' }
143
+
144
+ context 'passing the parameter in underscore syntax' do
145
+ let(:attrs) { {privacy_status: new_privacy_status} }
146
+
147
+ specify 'only updates the privacy status' do
148
+ expect(update).to be true
149
+ expect(@my_playlist.privacy_status).not_to eq old_privacy_status
150
+ expect(@my_playlist.title).to eq old_title
151
+ end
152
+ end
153
+
154
+ context 'passing the parameter in camel-case syntax' do
155
+ let(:attrs) { {privacyStatus: new_privacy_status} }
156
+
157
+ specify 'only updates the privacy status' do
158
+ expect(update).to be true
159
+ expect(@my_playlist.privacy_status).not_to eq old_privacy_status
160
+ expect(@my_playlist.title).to eq old_title
161
+ end
162
+ end
163
+ end
164
+
165
+ context 'given an existing video' do
166
+ let(:video_id) { 'MESycYJytkU' }
167
+
168
+ describe 'can be added' do
169
+ it { expect(playlist.add_video video_id).to be_a Yt::PlaylistItem }
170
+ it { expect{playlist.add_video video_id}.to change{playlist.playlist_items.count}.by(1) }
171
+ it { expect(playlist.add_video! video_id).to be_a Yt::PlaylistItem }
172
+ it { expect{playlist.add_video! video_id}.to change{playlist.playlist_items.count}.by(1) }
173
+ it { expect(playlist.add_video(video_id, position: 0).position).to be 0 }
174
+ end
175
+
176
+ # NOTE: This test sounds redundant, but it’s actually a reflection of
177
+ # another irrational behavior of YouTube API. In short, if you add a new
178
+ # video to a playlist, the returned item does not have the "position"
179
+ # information. You need an extra call to get it. When YouTube fixes this
180
+ # behavior, this test (and related code) will go away.
181
+ describe 'adding the video' do
182
+ let(:item) { playlist.add_video video_id }
183
+
184
+ specify 'returns an item without its position' do
185
+ expect(item.snippet).not_to be_complete
186
+ expect(item.position).not_to be_nil # after reloading
187
+ end
188
+ end
189
+
190
+ describe 'can be removed' do
191
+ before { playlist.add_video video_id }
192
+
193
+ it { expect(playlist.delete_playlist_items.uniq).to eq [true] }
194
+ it { expect{playlist.delete_playlist_items}.to change{playlist.playlist_items.count} }
195
+ end
196
+ end
197
+
198
+ context 'given an unknown video' do
199
+ let(:video_id) { 'not-a-video' }
200
+
201
+ describe 'cannot be added' do
202
+ it { expect(playlist.add_video video_id).to be_nil }
203
+ it { expect{playlist.add_video video_id}.not_to change{playlist.playlist_items.count} }
204
+ it { expect{playlist.add_video! video_id}.to fail.with 'videoNotFound' }
205
+ end
206
+ end
207
+
208
+ context 'given a video of a terminated account' do
209
+ let(:video_id) { 'kDCpdKeTe5g' }
210
+
211
+ describe 'cannot be added' do
212
+ it { expect(playlist.add_video video_id).to be_nil }
213
+ it { expect{playlist.add_video video_id}.not_to change{playlist.playlist_items.count} }
214
+ it { expect{playlist.add_video! video_id}.to fail.with 'forbidden' }
215
+ end
216
+ end
217
+
218
+ context 'given one existing and one unknown video' do
219
+ let(:video_ids) { ['MESycYJytkU', 'not-a-video'] }
220
+
221
+ describe 'only one can be added' do
222
+ it { expect(playlist.add_videos(video_ids).length).to eq 2 }
223
+ it { expect{playlist.add_videos video_ids}.to change{playlist.playlist_items.count}.by(1) }
224
+ it { expect{playlist.add_videos! video_ids}.to fail.with 'videoNotFound' }
225
+ end
226
+ end
227
+ end
228
+
229
+ context 'given one of my own playlists that I want to get reports for' do
230
+ let(:id) { $account.channel.playlists.first.id }
231
+
232
+ it 'returns valid reports for playlist-related metrics' do
233
+ expect{playlist.views}.not_to raise_error
234
+ expect{playlist.playlist_starts}.not_to raise_error
235
+ expect{playlist.average_time_in_playlist}.not_to raise_error
236
+ expect{playlist.views_per_playlist_start}.not_to raise_error
237
+
238
+ expect{playlist.views_on 3.days.ago}.not_to raise_error
239
+ expect{playlist.playlist_starts_on 3.days.ago}.not_to raise_error
240
+ expect{playlist.average_time_in_playlist_on 3.days.ago}.not_to raise_error
241
+ expect{playlist.views_per_playlist_start_on 3.days.ago}.not_to raise_error
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'yt/models/resource'
3
+
4
+ describe Yt::Resource, :device_app do
5
+ subject(:resource) { Yt::Resource.new url: url, auth: $account }
6
+
7
+ describe '#id' do
8
+ context 'given a URL containing an existing username' do
9
+ let(:url) { 'youtube.com/fullscreen' }
10
+ it { expect(resource.id).to eq 'UCxO1tY8h1AhOz0T4ENwmpow' }
11
+ end
12
+
13
+ context 'given a URL containing an unknown username' do
14
+ let(:url) { 'youtube.com/--not--a--valid--username' }
15
+ it { expect{resource.id}.to raise_error Yt::Errors::NoItems }
16
+ end
17
+ end
18
+ end
Binary file
@@ -0,0 +1,408 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+ require 'yt/models/video'
5
+
6
+ describe Yt::Video, :device_app do
7
+ subject(:video) { Yt::Video.new id: id, auth: $account }
8
+
9
+ context 'given someone else’s video' do
10
+ let(:id) { 'MESycYJytkU' }
11
+
12
+ it { expect(video.content_detail).to be_a Yt::ContentDetail }
13
+
14
+ it 'returns valid metadata' do
15
+ expect(video.title).to be_a String
16
+ expect(video.description).to be_a String
17
+ expect(video.thumbnail_url).to be_a String
18
+ expect(video.published_at).to be_a Time
19
+ expect(video.privacy_status).to be_a String
20
+ expect(video.tags).to be_an Array
21
+ expect(video.channel_id).to be_a String
22
+ expect(video.channel_title).to be_a String
23
+ expect(video.category_id).to be_a String
24
+ expect(video.live_broadcast_content).to be_a String
25
+ expect(video.view_count).to be_an Integer
26
+ expect(video.like_count).to be_an Integer
27
+ expect(video.dislike_count).to be_an Integer
28
+ expect(video.favorite_count).to be_an Integer
29
+ expect(video.comment_count).to be_an Integer
30
+ expect(video.duration).to be_an Integer
31
+ expect(video.hd?).to be_in [true, false]
32
+ expect(video.stereoscopic?).to be_in [true, false]
33
+ expect(video.captioned?).to be_in [true, false]
34
+ expect(video.licensed?).to be_in [true, false]
35
+ expect(video.deleted?).to be_in [true, false]
36
+ expect(video.failed?).to be_in [true, false]
37
+ expect(video.processed?).to be_in [true, false]
38
+ expect(video.rejected?).to be_in [true, false]
39
+ expect(video.uploading?).to be_in [true, false]
40
+ expect(video.uses_unsupported_codec?).to be_in [true, false]
41
+ expect(video.has_failed_conversion?).to be_in [true, false]
42
+ expect(video.empty?).to be_in [true, false]
43
+ expect(video.invalid?).to be_in [true, false]
44
+ expect(video.too_small?).to be_in [true, false]
45
+ expect(video.aborted?).to be_in [true, false]
46
+ expect(video.claimed?).to be_in [true, false]
47
+ expect(video.infringes_copyright?).to be_in [true, false]
48
+ expect(video.duplicate?).to be_in [true, false]
49
+ expect(video.scheduled_at.class).to be_in [NilClass, Time]
50
+ expect(video.scheduled?).to be_in [true, false]
51
+ expect(video.too_long?).to be_in [true, false]
52
+ expect(video.violates_terms_of_use?).to be_in [true, false]
53
+ expect(video.inappropriate?).to be_in [true, false]
54
+ expect(video.infringes_trademark?).to be_in [true, false]
55
+ expect(video.belongs_to_closed_account?).to be_in [true, false]
56
+ expect(video.belongs_to_suspended_account?).to be_in [true, false]
57
+ expect(video.licensed_as_creative_commons?).to be_in [true, false]
58
+ expect(video.licensed_as_standard_youtube?).to be_in [true, false]
59
+ expect(video.has_public_stats_viewable?).to be_in [true, false]
60
+ expect(video.embeddable?).to be_in [true, false]
61
+ expect(video.actual_start_time).to be_nil
62
+ expect(video.actual_end_time).to be_nil
63
+ expect(video.scheduled_start_time).to be_nil
64
+ expect(video.scheduled_end_time).to be_nil
65
+ expect(video.concurrent_viewers).to be_nil
66
+ expect(video.embed_html).to be_a String
67
+ expect(video.category_title).to be_a String
68
+ end
69
+
70
+ it { expect{video.update}.to fail }
71
+ it { expect{video.delete}.to fail.with 'forbidden' }
72
+
73
+ context 'that I like' do
74
+ before { video.like }
75
+ it { expect(video).to be_liked }
76
+ it { expect(video.dislike).to be true }
77
+ end
78
+
79
+ context 'that I dislike' do
80
+ before { video.dislike }
81
+ it { expect(video).not_to be_liked }
82
+ it { expect(video.like).to be true }
83
+ end
84
+
85
+ context 'that I am indifferent to' do
86
+ before { video.unlike }
87
+ it { expect(video).not_to be_liked }
88
+ it { expect(video.like).to be true }
89
+ end
90
+ end
91
+
92
+ context 'given someone else’s live video broadcast scheduled in the future' do
93
+ let(:id) { 'PqzGI8gO_gk' }
94
+
95
+ it 'returns valid live streaming details' do
96
+ expect(video.actual_start_time).to be_nil
97
+ expect(video.actual_end_time).to be_nil
98
+ expect(video.scheduled_start_time).to be_a Time
99
+ expect(video.scheduled_end_time).to be_nil
100
+ end
101
+ end
102
+
103
+ context 'given someone else’s past live video broadcast' do
104
+ let(:id) { 'COOM8_tOy6U' }
105
+
106
+ it 'returns valid live streaming details' do
107
+ expect(video.actual_start_time).to be_a Time
108
+ expect(video.actual_end_time).to be_a Time
109
+ expect(video.scheduled_start_time).to be_a Time
110
+ expect(video.scheduled_end_time).to be_a Time
111
+ expect(video.concurrent_viewers).to be_nil
112
+ end
113
+ end
114
+
115
+ context 'given an unknown video' do
116
+ let(:id) { 'not-a-video-id' }
117
+
118
+ it { expect{video.content_detail}.to raise_error Yt::Errors::NoItems }
119
+ it { expect{video.snippet}.to raise_error Yt::Errors::NoItems }
120
+ it { expect{video.rating}.to raise_error Yt::Errors::NoItems }
121
+ it { expect{video.status}.to raise_error Yt::Errors::NoItems }
122
+ it { expect{video.statistics_set}.to raise_error Yt::Errors::NoItems }
123
+ it { expect{video.file_detail}.to raise_error Yt::Errors::NoItems }
124
+ end
125
+
126
+ context 'given one of my own videos that I want to delete' do
127
+ before(:all) { @tmp_video = $account.upload_video 'https://bit.ly/yt_test', title: "Yt Test Delete Video #{rand}" }
128
+ let(:id) { @tmp_video.id }
129
+
130
+ it { expect(video.delete).to be true }
131
+ end
132
+
133
+ context 'given one of my own videos that I want to update' do
134
+ let(:id) { $account.videos.where(order: 'viewCount').first.id }
135
+ let!(:old_title) { video.title }
136
+ let!(:old_privacy_status) { video.privacy_status }
137
+ let(:update) { video.update attrs }
138
+
139
+ context 'given I update the title' do
140
+ # NOTE: The use of UTF-8 characters is to test that we can pass up to
141
+ # 50 characters, independently of their representation
142
+ let(:attrs) { {title: "Yt Example Update Video #{rand} - ®•♡❥❦❧☙"} }
143
+
144
+ specify 'only updates the title' do
145
+ expect(update).to be true
146
+ expect(video.title).not_to eq old_title
147
+ expect(video.privacy_status).to eq old_privacy_status
148
+ end
149
+ end
150
+
151
+ context 'given I update the description' do
152
+ let!(:old_description) { video.description }
153
+ let(:attrs) { {description: "Yt Example Description #{rand} - ®•♡❥❦❧☙"} }
154
+
155
+ specify 'only updates the description' do
156
+ expect(update).to be true
157
+ expect(video.description).not_to eq old_description
158
+ expect(video.title).to eq old_title
159
+ expect(video.privacy_status).to eq old_privacy_status
160
+ end
161
+ end
162
+
163
+ context 'given I update the tags' do
164
+ let!(:old_tags) { video.tags }
165
+ let(:attrs) { {tags: ["Yt Test Tag #{rand}"]} }
166
+
167
+ specify 'only updates the tag' do
168
+ expect(update).to be true
169
+ expect(video.tags).not_to eq old_tags
170
+ expect(video.title).to eq old_title
171
+ expect(video.privacy_status).to eq old_privacy_status
172
+ end
173
+ end
174
+
175
+ context 'given I update the category ID' do
176
+ let!(:old_category_id) { video.category_id }
177
+ let!(:new_category_id) { old_category_id == '22' ? '21' : '22' }
178
+
179
+ context 'passing the parameter in underscore syntax' do
180
+ let(:attrs) { {category_id: new_category_id} }
181
+
182
+ specify 'only updates the category ID' do
183
+ expect(update).to be true
184
+ expect(video.category_id).not_to eq old_category_id
185
+ expect(video.title).to eq old_title
186
+ expect(video.privacy_status).to eq old_privacy_status
187
+ end
188
+ end
189
+
190
+ context 'passing the parameter in camel-case syntax' do
191
+ let(:attrs) { {categoryId: new_category_id} }
192
+
193
+ specify 'only updates the category ID' do
194
+ expect(update).to be true
195
+ expect(video.category_id).not_to eq old_category_id
196
+ end
197
+ end
198
+ end
199
+
200
+ context 'given I update title, description and/or tags using angle brackets' do
201
+ let(:attrs) { {title: "Example Yt Test < >", description: '< >', tags: ['<tag>']} }
202
+
203
+ specify 'updates them replacing angle brackets with similar unicode characters accepted by YouTube' do
204
+ expect(update).to be true
205
+ expect(video.title).to eq 'Example Yt Test ‹ ›'
206
+ expect(video.description).to eq '‹ ›'
207
+ expect(video.tags).to eq ['‹tag›']
208
+ end
209
+ end
210
+
211
+ # note: 'scheduled' videos cannot be set to 'unlisted'
212
+ context 'given I update the privacy status' do
213
+ before { video.update publish_at: nil if video.scheduled? }
214
+ let!(:new_privacy_status) { old_privacy_status == 'private' ? 'unlisted' : 'private' }
215
+
216
+ context 'passing the parameter in underscore syntax' do
217
+ let(:attrs) { {privacy_status: new_privacy_status} }
218
+
219
+ specify 'only updates the privacy status' do
220
+ expect(update).to be true
221
+ expect(video.privacy_status).not_to eq old_privacy_status
222
+ expect(video.title).to eq old_title
223
+ end
224
+ end
225
+
226
+ context 'passing the parameter in camel-case syntax' do
227
+ let(:attrs) { {privacyStatus: new_privacy_status} }
228
+
229
+ specify 'only updates the privacy status' do
230
+ expect(update).to be true
231
+ expect(video.privacy_status).not_to eq old_privacy_status
232
+ expect(video.title).to eq old_title
233
+ end
234
+ end
235
+ end
236
+
237
+ context 'given I update the embeddable status' do
238
+ let!(:old_embeddable) { video.embeddable? }
239
+ let!(:new_embeddable) { !old_embeddable }
240
+
241
+ let(:attrs) { {embeddable: new_embeddable} }
242
+
243
+ # @note: This test is a reflection of another irrational behavior of
244
+ # YouTube API. Although 'embeddable' can be passed as an 'update'
245
+ # attribute according to the documentation, it simply does not work.
246
+ # The day YouTube fixes it, then this test will finally fail and will
247
+ # be removed, documenting how to update 'embeddable' too.
248
+ # @see https://developers.google.com/youtube/v3/docs/videos/update
249
+ # @see https://code.google.com/p/gdata-issues/issues/detail?id=4861
250
+ specify 'does not update the embeddable status' do
251
+ expect(update).to be true
252
+ expect(video.embeddable?).to eq old_embeddable
253
+ end
254
+ end
255
+
256
+ context 'given I update the public stats viewable setting' do
257
+ let!(:old_public_stats_viewable) { video.has_public_stats_viewable? }
258
+ let!(:new_public_stats_viewable) { !old_public_stats_viewable }
259
+
260
+ context 'passing the parameter in underscore syntax' do
261
+ let(:attrs) { {public_stats_viewable: new_public_stats_viewable} }
262
+
263
+ specify 'only updates the public stats viewable setting' do
264
+ expect(update).to be true
265
+ expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
266
+ expect(video.privacy_status).to eq old_privacy_status
267
+ expect(video.title).to eq old_title
268
+ end
269
+ end
270
+
271
+ context 'passing the parameter in camel-case syntax' do
272
+ let(:attrs) { {publicStatsViewable: new_public_stats_viewable} }
273
+
274
+ specify 'only updates the public stats viewable setting' do
275
+ expect(update).to be true
276
+ expect(video.has_public_stats_viewable?).to eq new_public_stats_viewable
277
+ expect(video.privacy_status).to eq old_privacy_status
278
+ expect(video.title).to eq old_title
279
+ end
280
+ end
281
+ end
282
+
283
+ it 'returns valid reports for video-related metrics' do
284
+ # Some reports are only available to Content Owners.
285
+ # See content owner test for more details about what the methods return.
286
+ expect{video.views}.not_to raise_error
287
+ expect{video.comments}.not_to raise_error
288
+ expect{video.likes}.not_to raise_error
289
+ expect{video.dislikes}.not_to raise_error
290
+ expect{video.shares}.not_to raise_error
291
+ expect{video.subscribers_gained}.not_to raise_error
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
+ expect{video.estimated_minutes_watched}.not_to raise_error
296
+ expect{video.average_view_duration}.not_to raise_error
297
+ expect{video.average_view_percentage}.not_to raise_error
298
+ expect{video.annotation_clicks}.not_to raise_error
299
+ expect{video.annotation_click_through_rate}.not_to raise_error
300
+ expect{video.annotation_close_rate}.not_to raise_error
301
+ expect{video.viewer_percentage}.not_to raise_error
302
+ expect{video.earnings}.to raise_error Yt::Errors::Unauthorized
303
+ expect{video.impressions}.to raise_error Yt::Errors::Unauthorized
304
+ expect{video.monetized_playbacks}.to raise_error Yt::Errors::Unauthorized
305
+ expect{video.playback_based_cpm}.to raise_error Yt::Errors::Unauthorized
306
+ expect{video.advertising_options_set}.to raise_error Yt::Errors::Forbidden
307
+
308
+ expect{video.views_on 3.days.ago}.not_to raise_error
309
+ expect{video.comments_on 3.days.ago}.not_to raise_error
310
+ expect{video.likes_on 3.days.ago}.not_to raise_error
311
+ expect{video.dislikes_on 3.days.ago}.not_to raise_error
312
+ expect{video.shares_on 3.days.ago}.not_to raise_error
313
+ expect{video.subscribers_gained_on 3.days.ago}.not_to raise_error
314
+ expect{video.subscribers_lost_on 3.days.ago}.not_to raise_error
315
+ expect{video.favorites_added_on 3.days.ago}.not_to raise_error
316
+ expect{video.favorites_removed_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.scheduled_at).to eq new_scheduled_at
346
+ expect(video.privacy_status).to eq old_privacy_status
347
+ expect(video.title).to eq old_title
348
+ end
349
+ end
350
+
351
+ context 'passing the parameter in camel-case syntax' do
352
+ let(:attrs) { {publishAt: new_scheduled_at} }
353
+
354
+ specify 'only updates the timestamp to publish the video' do
355
+ expect(video.update attrs).to be true
356
+ expect(video.scheduled_at).to eq new_scheduled_at
357
+ expect(video.privacy_status).to eq old_privacy_status
358
+ expect(video.title).to eq old_title
359
+ end
360
+ end
361
+ end
362
+
363
+ # @note: This should somehow test that the thumbnail *changes*. However,
364
+ # YouTube does not change the URL of the thumbnail even though the content
365
+ # changes. A full test would have to *download* the thumbnails before and
366
+ # after, and compare the files. For now, not raising error is enough.
367
+ # Eventually, change to `expect{update}.to change{video.thumbnail_url}`
368
+ context 'given one of my own videos for which I want to upload a thumbnail' do
369
+ let(:id) { $account.videos.where(order: 'viewCount').first.id }
370
+ let(:update) { video.upload_thumbnail path_or_url }
371
+
372
+ context 'given the path to a local JPG image file' do
373
+ let(:path_or_url) { File.expand_path '../thumbnail.jpg', __FILE__ }
374
+
375
+ it { expect{update}.not_to raise_error }
376
+ end
377
+
378
+ context 'given the path to a remote PNG image file' do
379
+ let(:path_or_url) { 'https://bit.ly/yt_thumbnail' }
380
+
381
+ it { expect{update}.not_to raise_error }
382
+ end
383
+
384
+ context 'given an invalid URL' do
385
+ let(:path_or_url) { 'this-is-not-a-url' }
386
+
387
+ it { expect{update}.to raise_error Yt::Errors::RequestError }
388
+ end
389
+ end
390
+
391
+ # @note: This test is separated from the block above because YouTube only
392
+ # returns file details for *some videos*: "The fileDetails object will
393
+ # only be returned if the processingDetails.fileAvailability property
394
+ # has a value of available.". Therefore, just to test fileDetails, we use a
395
+ # different video that (for some unknown reason) is marked as 'available'.
396
+ # Also note that I was not able to find a single video returning fileName,
397
+ # therefore video.file_name is not returned by Yt, until it can be tested.
398
+ # @see https://developers.google.com/youtube/v3/docs/videos#processingDetails.fileDetailsAvailability
399
+ context 'given one of my own *available* videos' do
400
+ let(:id) { 'yCmaOvUFhlI' }
401
+
402
+ it 'returns valid file details' do
403
+ expect(video.file_size).to be_an Integer
404
+ expect(video.file_type).to be_a String
405
+ expect(video.container).to be_a String
406
+ end
407
+ end
408
+ end