yt 0.25.13 → 0.32.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +305 -1
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +86 -5
  5. data/YOUTUBE_IT.md +3 -3
  6. data/lib/yt.rb +5 -2
  7. data/lib/yt/actions/list.rb +3 -3
  8. data/lib/yt/associations/has_authentication.rb +33 -1
  9. data/lib/yt/associations/has_reports.rb +13 -18
  10. data/lib/yt/collections/assets.rb +2 -2
  11. data/lib/yt/collections/authentications.rb +9 -2
  12. data/lib/yt/collections/base.rb +3 -3
  13. data/lib/yt/collections/bulk_report_jobs.rb +28 -0
  14. data/lib/yt/collections/bulk_reports.rb +24 -0
  15. data/lib/yt/collections/claims.rb +22 -1
  16. data/lib/yt/collections/comment_threads.rb +41 -0
  17. data/lib/yt/collections/content_owners.rb +1 -1
  18. data/lib/yt/collections/group_infos.rb +27 -0
  19. data/lib/yt/collections/group_items.rb +45 -0
  20. data/lib/yt/collections/reports.rb +75 -13
  21. data/lib/yt/collections/revocations.rb +30 -0
  22. data/lib/yt/collections/video_groups.rb +29 -0
  23. data/lib/yt/collections/videos.rb +34 -9
  24. data/lib/yt/constants/geography.rb +326 -0
  25. data/lib/yt/errors/forbidden.rb +1 -3
  26. data/lib/yt/errors/no_items.rb +1 -3
  27. data/lib/yt/errors/request_error.rb +10 -7
  28. data/lib/yt/errors/server_error.rb +1 -3
  29. data/lib/yt/errors/unauthorized.rb +3 -3
  30. data/lib/yt/models/account.rb +12 -0
  31. data/lib/yt/models/advertising_options_set.rb +4 -4
  32. data/lib/yt/models/bulk_report.rb +23 -0
  33. data/lib/yt/models/bulk_report_job.rb +23 -0
  34. data/lib/yt/models/channel.rb +21 -12
  35. data/lib/yt/models/claim.rb +13 -2
  36. data/lib/yt/models/comment.rb +37 -0
  37. data/lib/yt/models/comment_thread.rb +50 -0
  38. data/lib/yt/models/content_detail.rb +6 -0
  39. data/lib/yt/models/content_owner.rb +31 -1
  40. data/lib/yt/models/group_info.rb +16 -0
  41. data/lib/yt/models/group_item.rb +15 -0
  42. data/lib/yt/models/resource.rb +3 -10
  43. data/lib/yt/models/revocation.rb +12 -0
  44. data/lib/yt/models/right_owner.rb +0 -2
  45. data/lib/yt/models/snippet.rb +24 -3
  46. data/lib/yt/models/video.rb +42 -11
  47. data/lib/yt/models/video_group.rb +186 -0
  48. data/lib/yt/request.rb +5 -3
  49. data/lib/yt/version.rb +2 -2
  50. data/spec/collections/comment_threads_spec.rb +46 -0
  51. data/spec/collections/playlist_items_spec.rb +1 -1
  52. data/spec/collections/reports_spec.rb +2 -2
  53. data/spec/constants/geography_spec.rb +16 -0
  54. data/spec/models/annotation_spec.rb +1 -1
  55. data/spec/models/claim_spec.rb +15 -3
  56. data/spec/models/comment_spec.rb +40 -0
  57. data/spec/models/comment_thread_spec.rb +93 -0
  58. data/spec/models/content_detail_spec.rb +7 -0
  59. data/spec/models/reference_spec.rb +2 -2
  60. data/spec/models/request_spec.rb +21 -0
  61. data/spec/models/resource_spec.rb +0 -15
  62. data/spec/models/video_spec.rb +1 -1
  63. data/spec/requests/as_account/account_spec.rb +16 -4
  64. data/spec/requests/as_account/authentications_spec.rb +1 -13
  65. data/spec/requests/as_account/channel_spec.rb +15 -45
  66. data/spec/requests/as_account/playlist_item_spec.rb +3 -3
  67. data/spec/requests/as_account/playlist_spec.rb +5 -32
  68. data/spec/requests/as_account/video_spec.rb +2022 -21
  69. data/spec/requests/as_content_owner/account_spec.rb +4 -0
  70. data/spec/requests/as_content_owner/bulk_report_job_spec.rb +19 -0
  71. data/spec/requests/as_content_owner/channel_spec.rb +59 -270
  72. data/spec/requests/as_content_owner/content_owner_spec.rb +89 -1
  73. data/spec/requests/as_content_owner/playlist_spec.rb +0 -15
  74. data/spec/requests/as_content_owner/video_group_spec.rb +112 -0
  75. data/spec/requests/as_content_owner/video_spec.rb +72 -146
  76. data/spec/requests/as_server_app/channel_spec.rb +1 -21
  77. data/spec/requests/as_server_app/comment_spec.rb +22 -0
  78. data/spec/requests/as_server_app/comment_thread_spec.rb +27 -0
  79. data/spec/requests/as_server_app/comment_threads_spec.rb +41 -0
  80. data/spec/requests/as_server_app/playlist_item_spec.rb +2 -2
  81. data/spec/requests/as_server_app/playlist_spec.rb +1 -22
  82. data/spec/requests/as_server_app/video_spec.rb +21 -19
  83. data/spec/requests/as_server_app/videos_spec.rb +5 -5
  84. data/spec/requests/unauthenticated/video_spec.rb +1 -9
  85. data/spec/spec_helper.rb +1 -1
  86. data/yt.gemspec +2 -1
  87. metadata +51 -17
  88. data/lib/yt/collections/ids.rb +0 -27
  89. data/lib/yt/config.rb +0 -54
  90. data/lib/yt/models/configuration.rb +0 -70
  91. data/lib/yt/models/description.rb +0 -58
  92. data/lib/yt/models/url.rb +0 -91
  93. data/spec/models/configuration_spec.rb +0 -44
  94. data/spec/models/description_spec.rb +0 -94
  95. data/spec/models/url_spec.rb +0 -84
  96. data/spec/requests/as_account/resource_spec.rb +0 -18
@@ -141,7 +141,7 @@ client = YouTubeIt::Client.new
141
141
  client.videos_by(:query => "penguin", :author => "liz")
142
142
  # with yt: the 'author' filter was removed from YouTube API V3, so the
143
143
  # request must be done using the channel of the requested author
144
- channel = Yt::Channel.new url: 'youtube.com/liz'
144
+ channel = Yt::Channel.new id: 'UCxxxxxxxxx'
145
145
  channel.videos.where(q: 'penguin')
146
146
  ```
147
147
 
@@ -176,7 +176,7 @@ client = YouTubeIt::Client.new
176
176
  client.videos_by(:user => 'liz')
177
177
  # with yt: the 'author' filter was removed from YouTube API V3, so the
178
178
  # request must be done using the channel of the requested author
179
- channel = Yt::Channel.new url: 'youtube.com/liz'
179
+ channel = Yt::Channel.new id: 'UCxxxxxxxxx'
180
180
  channel.videos.where(q: 'penguin')
181
181
  ```
182
182
 
@@ -188,7 +188,7 @@ client = YouTubeIt::Client.new
188
188
  client.videos_by(:favorites, :user => 'liz')
189
189
  # with yt: note that only *old* channels have a "Favorites" playlist, since
190
190
  # "Favorites" has been deprecated by YouTube in favor of "Liked Videos".
191
- channel = Yt::Channel.new url: 'youtube.com/liz'
191
+ channel = Yt::Channel.new id: 'UCxxxxxxxxx'
192
192
  channel.related_playlists.find{|p| p.title == 'Favorites'}
193
193
  ```
194
194
 
data/lib/yt.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'yt/config'
2
2
  require 'yt/version'
3
+ require 'yt/constants/geography'
3
4
  require 'yt/models/account'
4
5
  require 'yt/models/channel'
5
6
  require 'yt/models/claim'
@@ -9,13 +10,15 @@ require 'yt/models/match_policy'
9
10
  require 'yt/models/playlist'
10
11
  require 'yt/models/playlist_item'
11
12
  require 'yt/models/video'
13
+ require 'yt/models/video_group'
14
+ require 'yt/models/comment_thread'
12
15
  require 'yt/models/ownership'
13
16
  require 'yt/models/advertising_options_set'
14
17
 
15
18
  # An object-oriented Ruby client for YouTube.
16
19
  # Helps creating applications that need to interact with YouTube objects.
17
20
  # Inclused methods to access YouTube Data API V3 resources (channels, videos,
18
- # ...), YouTube Analytics API V2 resources (metrics, earnings, ...), and
21
+ # ...), YouTube Analytics API V2 resources (metrics, estimated_revenue, ...), and
19
22
  # objects not available through the API (annotations).
20
23
  module Yt
21
- end
24
+ end
@@ -6,7 +6,7 @@ require 'yt/config'
6
6
  module Yt
7
7
  module Actions
8
8
  module List
9
- delegate :any?, :count, :each, :each_cons, :each_slice, :find, :first,
9
+ delegate :any?, :count, :each, :each_cons, :each_slice, :find, :first, :take,
10
10
  :flat_map, :map, :select, :size, to: :list
11
11
 
12
12
  def first!
@@ -47,7 +47,7 @@ module Yt
47
47
  def find_next
48
48
  @items ||= []
49
49
  if @items[@last_index].nil? && more_pages?
50
- more_items = next_page.map{|data| new_item data}
50
+ more_items = next_page.map{|data| new_item data}.compact
51
51
  @items.concat more_items
52
52
  end
53
53
  @items[(@last_index +=1) -1]
@@ -136,4 +136,4 @@ module Yt
136
136
  end
137
137
  end
138
138
  end
139
- end
139
+ end
@@ -9,6 +9,7 @@ module Yt
9
9
  def has_authentication
10
10
  require 'yt/collections/authentications'
11
11
  require 'yt/collections/device_flows'
12
+ require 'yt/collections/revocations'
12
13
  require 'yt/errors/missing_auth'
13
14
  require 'yt/errors/no_items'
14
15
  require 'yt/errors/unauthorized'
@@ -57,7 +58,31 @@ module Yt
57
58
  def refreshed_access_token?
58
59
  old_access_token = authentication.access_token
59
60
  @authentication = @access_token = @refreshed_authentications = nil
60
- old_access_token != authentication.access_token
61
+
62
+ if old_access_token != authentication.access_token
63
+ access_token_was_refreshed
64
+ true
65
+ else
66
+ false
67
+ end
68
+ end
69
+
70
+ # Revoke access given to the application.
71
+ # Returns true if the access was correctly revoked.
72
+ # @see https://developers.google.com/identity/protocols/OAuth2WebServer#tokenrevoke
73
+ def revoke_access
74
+ revocations.first!
75
+ @authentication = @access_token = @refreshed_authentications = nil
76
+ true
77
+ rescue Errors::RequestError => e
78
+ raise unless e.reasons.include? 'invalid_token'
79
+ false
80
+ end
81
+
82
+ # Invoked when the access token is refreshed.
83
+ def access_token_was_refreshed
84
+ # Apps using Yt can override this method to handle this event, for
85
+ # instance to store the newly generated access token in the database.
61
86
  end
62
87
 
63
88
  private
@@ -103,6 +128,7 @@ module Yt
103
128
  error_message = case
104
129
  when @redirect_uri && @scopes then missing_authorization_code_message
105
130
  when @scopes then pending_device_code_message
131
+ else {}
106
132
  end
107
133
  raise Errors::MissingAuth, error_message
108
134
  end
@@ -149,6 +175,12 @@ module Yt
149
175
  end
150
176
  end
151
177
 
178
+ def revocations
179
+ @revocations ||= Collections::Revocations.of(self).tap do |auth|
180
+ auth.auth_params = {token: @refresh_token || @access_token}
181
+ end
182
+ end
183
+
152
184
  def authentication_url_params
153
185
  {}.tap do |params|
154
186
  params[:client_id] = client_id
@@ -206,18 +206,17 @@ module Yt
206
206
  # @macro report
207
207
  # @macro report_with_country_and_state
208
208
 
209
- # Defines two public instance methods to access the reports of a
209
+ # Defines a public instance methods to access the reports of a
210
210
  # resource for a specific metric.
211
211
  # @param [Symbol] metric the metric to access the reports of.
212
212
  # @param [Class] type The class to cast the returned values to.
213
- # @example Adds +comments+ and +comments_on+ on a Channel resource.
213
+ # @example Adds +comments+ on a Channel resource.
214
214
  # class Channel < Resource
215
215
  # has_report :comments, Integer
216
216
  # end
217
217
  def has_report(metric, type)
218
218
  require 'yt/collections/reports'
219
219
 
220
- define_metric_on_method metric
221
220
  define_metric_method metric
222
221
  define_reports_method metric, type
223
222
  define_range_metric_method metric
@@ -226,12 +225,6 @@ module Yt
226
225
 
227
226
  private
228
227
 
229
- def define_metric_on_method(metric)
230
- define_method "#{metric}_on" do |date|
231
- send(metric, from: date, to: date, by: :day).values.first
232
- end
233
- end
234
-
235
228
  def define_reports_method(metric, type)
236
229
  (@metrics ||= {})[metric] = type
237
230
  define_method :reports do |options = {}|
@@ -242,6 +235,7 @@ module Yt
242
235
  state = location[:state] if location.is_a?(Hash)
243
236
  dimension = options[:by] || (metric == :viewer_percentage ? :gender_age_group : :range)
244
237
  videos = options[:videos]
238
+ historical = options[:historical].to_s if [true, false].include?(options[:historical])
245
239
  if dimension == :month
246
240
  from = from.to_date.beginning_of_month
247
241
  to = to.to_date.beginning_of_month
@@ -250,9 +244,9 @@ module Yt
250
244
 
251
245
  only = options.fetch :only, []
252
246
  reports = Collections::Reports.of(self).tap do |reports|
253
- reports.metrics = self.class.instance_variable_get(:@metrics).select{|k, v| k.in? only}
247
+ reports.metrics = self.class.instance_variable_get(:@metrics).select{|k, v| k.in? only}
254
248
  end
255
- reports.within date_range, country, state, dimension, videos
249
+ reports.within date_range, country, state, dimension, videos, historical
256
250
  end unless defined?(reports)
257
251
  end
258
252
 
@@ -265,6 +259,7 @@ module Yt
265
259
  state = location[:state] if location.is_a?(Hash)
266
260
  dimension = options[:by] || (metric == :viewer_percentage ? :gender_age_group : :range)
267
261
  videos = options[:videos]
262
+ historical = options[:historical].to_s if [true, false].include?(options[:historical])
268
263
  if dimension == :month
269
264
  from = from.to_date.beginning_of_month
270
265
  to = to.to_date.beginning_of_month
@@ -276,10 +271,10 @@ module Yt
276
271
  results = case dimension
277
272
  when :day
278
273
  Hash[*range.flat_map do |date|
279
- [date, instance_variable_get("@#{metric}_#{dimension}_#{country}_#{state}")[date] ||= send("range_#{metric}", range, dimension, country, state, videos)[date]]
274
+ [date, instance_variable_get("@#{metric}_#{dimension}_#{country}_#{state}")[date] ||= send("range_#{metric}", range, dimension, country, state, videos, historical)[date]]
280
275
  end]
281
276
  else
282
- instance_variable_get("@#{metric}_#{dimension}_#{country}_#{state}")[range] ||= send("range_#{metric}", range, dimension, country, state, videos)
277
+ instance_variable_get("@#{metric}_#{dimension}_#{country}_#{state}")[range] ||= send("range_#{metric}", range, dimension, country, state, videos, historical)
283
278
  end
284
279
  lookup_class = case options[:by]
285
280
  when :video, :related_video then Yt::Collections::Videos
@@ -296,25 +291,25 @@ module Yt
296
291
  end
297
292
 
298
293
  def define_range_metric_method(metric)
299
- define_method "range_#{metric}" do |date_range, dimension, country, state, videos|
294
+ define_method "range_#{metric}" do |date_range, dimension, country, state, videos, historical|
300
295
  ivar = instance_variable_get "@range_#{metric}_#{dimension}_#{country}_#{state}"
301
296
  instance_variable_set "@range_#{metric}_#{dimension}_#{country}_#{state}", ivar || {}
302
- instance_variable_get("@range_#{metric}_#{dimension}_#{country}_#{state}")[date_range] ||= send("all_#{metric}").within date_range, country, state, dimension, videos
297
+ instance_variable_get("@range_#{metric}_#{dimension}_#{country}_#{state}")[date_range] ||= send("all_#{metric}").within date_range, country, state, dimension, videos, historical
303
298
  end
304
299
  private "range_#{metric}"
305
300
  end
306
301
 
307
302
  def define_all_metric_method(metric, type)
308
303
  define_method "all_#{metric}" do
309
- # @note Asking for the "earnings" metric of a day in which a channel
304
+ # @note Asking for the "estimated_revenue" metric of a day in which a channel
310
305
  # made 0 USD returns the wrong "nil". But adding to the request the
311
306
  # "estimatedMinutesWatched" metric returns the correct value 0.
312
307
  metrics = {metric => type}
313
- metrics[:estimated_minutes_watched] = Integer if metric == :earnings
308
+ metrics[:estimated_minutes_watched] = Integer if metric == :estimated_revenue
314
309
  Collections::Reports.of(self).tap{|reports| reports.metrics = metrics}
315
310
  end
316
311
  private "all_#{metric}"
317
312
  end
318
313
  end
319
314
  end
320
- end
315
+ end
@@ -39,7 +39,7 @@ module Yt
39
39
  # is accessed; it should be replaced with a filter on params instead.
40
40
  def assets_path
41
41
  @where_params ||= {}
42
- if @where_params.empty? || @where_params.key?(:id)
42
+ if @where_params.key?(:id)
43
43
  '/youtube/partner/v1/assets'
44
44
  else
45
45
  '/youtube/partner/v1/assetSearch'
@@ -55,4 +55,4 @@ module Yt
55
55
  end
56
56
  end
57
57
  end
58
- end
58
+ end
@@ -40,8 +40,15 @@ module Yt
40
40
  end
41
41
 
42
42
  def expected?(error)
43
- error.kind == 'invalid_grant'
43
+ error.kind == 'invalid_grant' &&
44
+ invalid_code_errors.exclude?(error.description)
45
+ end
46
+
47
+ private
48
+
49
+ def invalid_code_errors
50
+ ["Code was already redeemed.", "Invalid code."]
44
51
  end
45
52
  end
46
53
  end
47
- end
54
+ end
@@ -31,9 +31,9 @@ module Yt
31
31
  # are at https://developers.google.com/youtube/v3/docs/search/list
32
32
  #
33
33
  # @example Return the first video of a channel (no requirements):
34
- # video.channels.first
34
+ # channel.videos.first
35
35
  # @example Return the first long video of a channel by video count:
36
- # video.channels.where(order: 'viewCount', video_duration: 'long').first
36
+ # channel.videos.where(order: 'viewCount', video_duration: 'long').first
37
37
  def where(requirements = {})
38
38
  self.tap do
39
39
  @items = []
@@ -59,4 +59,4 @@ module Yt
59
59
  end
60
60
  end
61
61
  end
62
- end
62
+ end
@@ -0,0 +1,28 @@
1
+ require 'yt/collections/base'
2
+ require 'yt/models/bulk_report_job'
3
+
4
+ module Yt
5
+ module Collections
6
+ # @private
7
+ class BulkReportJobs < Base
8
+
9
+ private
10
+
11
+ def attributes_for_new_item(data)
12
+ {id: data['id'], auth: @auth, report_type_id: data['reportTypeId']}
13
+ end
14
+
15
+ def list_params
16
+ super.tap do |params|
17
+ params[:host] = 'youtubereporting.googleapis.com'
18
+ params[:path] = "/v1/jobs"
19
+ params[:params] = {on_behalf_of_content_owner: @parent.owner_name}
20
+ end
21
+ end
22
+
23
+ def items_key
24
+ 'jobs'
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ require 'yt/collections/base'
2
+ require 'yt/models/bulk_report'
3
+
4
+ module Yt
5
+ module Collections
6
+ # @private
7
+ class BulkReports < Base
8
+
9
+ private
10
+
11
+ def list_params
12
+ super.tap do |params|
13
+ params[:host] = 'youtubereporting.googleapis.com'
14
+ params[:path] = "/v1/jobs/#{@parent.id}/reports"
15
+ params[:params] = {on_behalf_of_content_owner: @parent.auth.owner_name}
16
+ end
17
+ end
18
+
19
+ def items_key
20
+ 'reports'
21
+ end
22
+ end
23
+ end
24
+ end
@@ -17,6 +17,27 @@ module Yt
17
17
 
18
18
  private
19
19
 
20
+ def attributes_for_new_item(data)
21
+ {}.tap do |attributes|
22
+ attributes[:id] = data['id']
23
+ attributes[:auth] = @auth
24
+ attributes[:data] = data
25
+ attributes[:asset] = data['asset']
26
+ end
27
+ end
28
+
29
+ def eager_load_items_from(items)
30
+ if included_relationships.include? :asset
31
+ asset_ids = items.map{|a| a.values_at 'videoId', 'assetId'}.to_h.values
32
+ conditions = { id: asset_ids.join(','), fetch_metadata: 'effective' }
33
+ assets = @parent.assets.where conditions
34
+ items.each do |item|
35
+ item['asset'] = assets.find { |a| a.id == item['assetId'] }
36
+ end
37
+ end
38
+ super
39
+ end
40
+
20
41
  # @return [Hash] the parameters to submit to YouTube to list claims
21
42
  # administered by the content owner.
22
43
  # @see https://developers.google.com/youtube/partner/docs/v1/claims/list
@@ -53,4 +74,4 @@ module Yt
53
74
  end
54
75
  end
55
76
  end
56
- end
77
+ end
@@ -0,0 +1,41 @@
1
+ require 'yt/collections/base'
2
+ require 'yt/models/video'
3
+ require 'yt/models/channel'
4
+
5
+ module Yt
6
+ module Collections
7
+ # @private
8
+ class CommentThreads < Base
9
+
10
+ private
11
+
12
+ def attributes_for_new_item(data)
13
+ {}.tap do |attributes|
14
+ attributes[:id] = data['id']
15
+ attributes[:snippet] = data['snippet']
16
+ attributes[:auth] = @auth
17
+ end
18
+ end
19
+
20
+ # @return [Hash] the parameters to submit to YouTube to get the resource.
21
+ # @see https://developers.google.com/youtube/v3/docs/commentThreads#resource
22
+ def list_params
23
+ super.tap do |params|
24
+ params[:path] = "/youtube/v3/commentThreads"
25
+ params[:params] = comments_params
26
+ end
27
+ end
28
+
29
+ def comments_params
30
+ apply_where_params!({max_results: 100, part: 'snippet'}).tap do |params|
31
+ case @parent
32
+ when Yt::Video
33
+ params[:videoId] = @parent.id
34
+ when Yt::Channel
35
+ params[:channelId] = @parent.id
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -11,7 +11,7 @@ module Yt
11
11
  private
12
12
 
13
13
  def attributes_for_new_item(data)
14
- {owner_name: data['id'], authentication: @auth.authentication}
14
+ {owner_name: data['id'], display_name: data['displayName'], authentication: @auth.authentication}
15
15
  end
16
16
 
17
17
  # @return [Hash] the parameters to submit to YouTube to list content
@@ -0,0 +1,27 @@
1
+ require 'yt/collections/base'
2
+ require 'yt/models/snippet'
3
+
4
+ module Yt
5
+ module Collections
6
+ # @private
7
+ class GroupInfos < Base
8
+
9
+ private
10
+
11
+ def attributes_for_new_item(data)
12
+ {data: data, auth: @auth}
13
+ end
14
+
15
+ def list_params
16
+ super.tap do |params|
17
+ params[:host] = 'youtubeanalytics.googleapis.com'
18
+ params[:path] = "/v2/groups"
19
+ params[:params] = {id: @parent.id}
20
+ if @auth.owner_name
21
+ params[:params][:on_behalf_of_content_owner] = @auth.owner_name
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end