yt 0.25.13 → 0.32.2

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