yt 0.32.6 → 0.33.4

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -4
  3. data/CHANGELOG.md +36 -0
  4. data/README.md +22 -33
  5. data/YOUTUBE_IT.md +4 -4
  6. data/lib/yt.rb +1 -1
  7. data/lib/yt/actions/list.rb +1 -1
  8. data/lib/yt/associations/has_reports.rb +9 -14
  9. data/lib/yt/collections/reports.rb +5 -7
  10. data/lib/yt/models/account.rb +13 -3
  11. data/lib/yt/models/channel.rb +19 -4
  12. data/lib/yt/models/resource.rb +74 -3
  13. data/lib/yt/models/status.rb +3 -1
  14. data/lib/yt/models/url.rb +2 -60
  15. data/lib/yt/models/video.rb +16 -1
  16. data/lib/yt/request.rb +7 -4
  17. data/lib/yt/version.rb +1 -1
  18. data/yt.gemspec +5 -2
  19. metadata +32 -170
  20. data/spec/collections/claims_spec.rb +0 -62
  21. data/spec/collections/comment_threads_spec.rb +0 -46
  22. data/spec/collections/playlist_items_spec.rb +0 -44
  23. data/spec/collections/playlists_spec.rb +0 -27
  24. data/spec/collections/policies_spec.rb +0 -30
  25. data/spec/collections/references_spec.rb +0 -30
  26. data/spec/collections/reports_spec.rb +0 -30
  27. data/spec/collections/subscriptions_spec.rb +0 -25
  28. data/spec/collections/videos_spec.rb +0 -43
  29. data/spec/constants/geography_spec.rb +0 -16
  30. data/spec/errors/forbidden_spec.rb +0 -10
  31. data/spec/errors/missing_auth_spec.rb +0 -24
  32. data/spec/errors/no_items_spec.rb +0 -10
  33. data/spec/errors/request_error_spec.rb +0 -44
  34. data/spec/errors/server_error_spec.rb +0 -10
  35. data/spec/errors/unauthorized_spec.rb +0 -10
  36. data/spec/models/account_spec.rb +0 -138
  37. data/spec/models/annotation_spec.rb +0 -180
  38. data/spec/models/asset_spec.rb +0 -32
  39. data/spec/models/channel_spec.rb +0 -127
  40. data/spec/models/claim_event_spec.rb +0 -62
  41. data/spec/models/claim_history_spec.rb +0 -27
  42. data/spec/models/claim_spec.rb +0 -223
  43. data/spec/models/comment_spec.rb +0 -40
  44. data/spec/models/comment_thread_spec.rb +0 -93
  45. data/spec/models/configuration_spec.rb +0 -44
  46. data/spec/models/content_detail_spec.rb +0 -52
  47. data/spec/models/content_owner_detail_spec.rb +0 -6
  48. data/spec/models/file_detail_spec.rb +0 -13
  49. data/spec/models/live_streaming_detail_spec.rb +0 -6
  50. data/spec/models/ownership_spec.rb +0 -59
  51. data/spec/models/player_spec.rb +0 -13
  52. data/spec/models/playlist_item_spec.rb +0 -120
  53. data/spec/models/playlist_spec.rb +0 -138
  54. data/spec/models/policy_rule_spec.rb +0 -63
  55. data/spec/models/policy_spec.rb +0 -41
  56. data/spec/models/rating_spec.rb +0 -12
  57. data/spec/models/reference_spec.rb +0 -249
  58. data/spec/models/request_spec.rb +0 -204
  59. data/spec/models/resource_spec.rb +0 -42
  60. data/spec/models/right_owner_spec.rb +0 -71
  61. data/spec/models/snippet_spec.rb +0 -13
  62. data/spec/models/statistics_set_spec.rb +0 -13
  63. data/spec/models/status_spec.rb +0 -13
  64. data/spec/models/subscription_spec.rb +0 -30
  65. data/spec/models/url_spec.rb +0 -78
  66. data/spec/models/video_category_spec.rb +0 -21
  67. data/spec/models/video_spec.rb +0 -669
  68. data/spec/requests/as_account/account_spec.rb +0 -143
  69. data/spec/requests/as_account/authentications_spec.rb +0 -127
  70. data/spec/requests/as_account/channel_spec.rb +0 -246
  71. data/spec/requests/as_account/channels_spec.rb +0 -18
  72. data/spec/requests/as_account/playlist_item_spec.rb +0 -55
  73. data/spec/requests/as_account/playlist_spec.rb +0 -218
  74. data/spec/requests/as_account/thumbnail.jpg +0 -0
  75. data/spec/requests/as_account/video.mp4 +0 -0
  76. data/spec/requests/as_account/video_spec.rb +0 -408
  77. data/spec/requests/as_content_owner/account_spec.rb +0 -29
  78. data/spec/requests/as_content_owner/advertising_options_set_spec.rb +0 -15
  79. data/spec/requests/as_content_owner/asset_spec.rb +0 -31
  80. data/spec/requests/as_content_owner/bulk_report_job_spec.rb +0 -19
  81. data/spec/requests/as_content_owner/channel_spec.rb +0 -1836
  82. data/spec/requests/as_content_owner/claim_history_spec.rb +0 -20
  83. data/spec/requests/as_content_owner/claim_spec.rb +0 -17
  84. data/spec/requests/as_content_owner/content_owner_spec.rb +0 -370
  85. data/spec/requests/as_content_owner/match_policy_spec.rb +0 -17
  86. data/spec/requests/as_content_owner/ownership_spec.rb +0 -25
  87. data/spec/requests/as_content_owner/playlist_spec.rb +0 -767
  88. data/spec/requests/as_content_owner/video_group_spec.rb +0 -112
  89. data/spec/requests/as_content_owner/video_spec.rb +0 -1223
  90. data/spec/requests/as_server_app/channel_spec.rb +0 -54
  91. data/spec/requests/as_server_app/comment_spec.rb +0 -22
  92. data/spec/requests/as_server_app/comment_thread_spec.rb +0 -27
  93. data/spec/requests/as_server_app/comment_threads_spec.rb +0 -41
  94. data/spec/requests/as_server_app/playlist_item_spec.rb +0 -30
  95. data/spec/requests/as_server_app/playlist_spec.rb +0 -33
  96. data/spec/requests/as_server_app/url_spec.rb +0 -94
  97. data/spec/requests/as_server_app/video_spec.rb +0 -60
  98. data/spec/requests/as_server_app/videos_spec.rb +0 -40
  99. data/spec/requests/unauthenticated/video_spec.rb +0 -14
  100. data/spec/spec_helper.rb +0 -20
  101. data/spec/support/fail_matcher.rb +0 -15
  102. data/spec/support/global_hooks.rb +0 -48
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e19662ddb23d57f544dd42ab0ceb974e0cf7b7a0aadb2137a298c9a5f0afe2cf
4
- data.tar.gz: c02517119190c7eda5f532305330b148979ca2bd04b38485915717fb41c5125c
3
+ metadata.gz: 8c89bfab3bb32975237aa1b79a1f0897a240ea494f3d3928a03b423c734b71f6
4
+ data.tar.gz: 6c58ab20408d9fdf79265fc499d1ee0f88623295de50592a25948e8029aaf91a
5
5
  SHA512:
6
- metadata.gz: 3d19c4c4c947855487e23d07fd89f6b11719af3cb11b8bc3d73594f4703be7f7990652050ac03449359d6861be02cd43eea2599839e2f9b8c0f304449bf084cf
7
- data.tar.gz: d794dfbbe1eddcaa98bf0ec0daa36983b7e69b97c8aafa060418999ed5a5806dbf3bbec18fe772a2383f28033f87b8f1364c4742f74abdc6b1a201a1209935a4
6
+ metadata.gz: 2bcd96aa69c14456e067f0cad222148743334e8d8f89f3a052923f687e2d06e3a53db772898ec43399d1cdff20352fc9e56f63745bdbbebe69641511255cf5a9
7
+ data.tar.gz: 52245373b815ad681f7516bea9157597c7b5b118db206a50c7ca2746cf61b8c591f2fb4765c63787427cac26fb803ce11053c1d12855e4d213fdbfb54b372e0c
data/.rspec CHANGED
@@ -1,6 +1,3 @@
1
- --format documentation
2
1
  --color
3
- --tag ~rate_limited
4
- --tag ~flaky
5
- --tag ~extended_permissions
6
2
  --exclude_pattern spec/requests/as_content_owner/*_spec.rb
3
+ --tag=~slow
@@ -6,6 +6,42 @@ For more information about changelogs, check
6
6
  [Keep a Changelog](http://keepachangelog.com) and
7
7
  [Vandamme](http://tech-angels.github.io/vandamme).
8
8
 
9
+ ## 0.33.4 - 2021-01-15
10
+
11
+ * [REMOVAL] remove retry for quota errors
12
+
13
+ ## 0.33.3 - 2020-11-17
14
+
15
+ * [BUGFIX] require the `URL` model when requiring `yt`
16
+ * [BUGFIX] handle passing in a `nil` id
17
+
18
+ ## 0.33.2 - 2020-11-11
19
+
20
+ * [BUGFIX] No more pages when page token is an empty string, per YouTube change.
21
+
22
+ ## 0.33.1 - 2020-10-19
23
+
24
+ * [BUGFIX] Only retry once when exchanging a refresh token
25
+
26
+ ## 0.33.0 - 2020-04-10
27
+
28
+ If your code calls reports methods such as `views`, `likes`, or `reports`,
29
+ do not include `by: :week` option since `7DayTotals` dimension will no longer be
30
+ supported by YouTube API as of [April 15, 2020](https://developers.google.com/youtube/analytics/revision_history#october-15,-2019).
31
+
32
+ Use `by: :day` option instead and add up the numbers from each day.
33
+
34
+ If you keep using `by: :week` option after this change it will raise an error
35
+ (before the gem upgrade) or it will run with `day` dimension instead (after
36
+ the gem upgrade, like any other random input).
37
+
38
+ * [REMOVAL] Remove `by: :week` option for reports.
39
+ * [FEATURE] Add back the option of initializing a resource by its URL.
40
+ * [BUGFIX] Limit retries on refreshing tokens
41
+
42
+ **Breaking change**
43
+
44
+ If your code is using constant `Yt::URL::CHANNEL_PATTERNS` etc then it's moved to `Yt::Resource::CHANNEL_PATTERNS`, `Yt::Resource::VIDEO_PATTERNS`, and `Yt::Resource::PLAYLIST_PATTERNS`.
9
45
 
10
46
  ## 0.32.6 - 2020-02-07
11
47
 
data/README.md CHANGED
@@ -18,7 +18,6 @@ After [registering your app](#configuring-your-app), you can run commands like:
18
18
  channel = Yt::Channel.new id: 'UCxO1tY8h1AhOz0T4ENwmpow'
19
19
  channel.title #=> "Fullscreen"
20
20
  channel.public? #=> true
21
- channel.comment_count #=> 773
22
21
  channel.videos.count #=> 12
23
22
  ```
24
23
 
@@ -526,44 +525,34 @@ If a variable is set in both places, then `Yt.configure` takes precedence.
526
525
  How to test
527
526
  ===========
528
527
 
529
- Yt comes with two different sets of tests:
530
-
531
- 1. tests in `spec/models`, `spec/collections` and `spec/errors` **do not hit** the YouTube API
532
- 1. tests in `spec/requests` **hit** the YouTube API and require authentication
533
-
534
- To only run tests against models, collections and errors (which do not hit the API), type:
528
+ To run tests:
535
529
 
536
530
  ```bash
537
- rspec spec/models spec/collections spec/errors
531
+ rspec
538
532
  ```
539
533
 
540
- To also run live-tests against the YouTube API, type:
534
+ We recommend RSpec >= 3.8.
541
535
 
542
- ```bash
543
- rspec
544
- ```
536
+ Yt comes with two different sets of tests:
537
+
538
+ 1. Unit tests in `spec/models`, `spec/collections` and `spec/errors`
539
+ 2. Legacy integration tests in `spec/requests`
540
+
541
+ Coming soon will be a new set of high-level integration tests.
542
+
543
+ Integration tests are recorded with VCR. Some of the tests refer to
544
+ fixture data that an arbitrary account may not have access to. If you
545
+ need to modify one of these tests or re-record the cassette, we'd
546
+ suggest working against your own version of the testing setup. Then in
547
+ your pull request, we can help canonize your test/fixtures.
548
+
549
+ Some of the integration tests require authentication. These can be set
550
+ with the following environment variables:
545
551
 
546
- This will fail unless you have set up a test YouTube application and some
547
- tests YouTube accounts (with appropriate fixture data) to hit the API.
548
- Furthermore, tests that require authentication are divided into three
549
- roles, which correspond to each directory in `spec/requests`:
550
-
551
- * Account-based tests, which require a valid refresh token along with
552
- the application-level credentials the refresh token was created with
553
- (`YT_TEST_DEVICE_REFRESH_TOKEN`, `YT_TEST_DEVICE_CLIENT_ID`, and
554
- `YT_TEST_DEVICE_CLIENT_SECRET` respectively).
555
- * Server application tests, which use a server API key
556
- (`YT_TEST_SERVER_API_KEY`).
557
- * Tests that excercise YouTube's partner functionality. This requires an
558
- a partner channel id (`YT_TEST_CONTENT_OWNER_NAME`), a refresh token
559
- that's authenticated with that channel
560
- (`YT_TEST_CONTENT_OWNER_REFRESH_TOKEN`), and the corresponding
561
- application (`YT_TEST_PARTNER_CLIENT_ID` and
562
- (`YT_TEST_PARTNER_CLIENT_SECRET`).
563
-
564
- The refresh tokens need to be generated with the `youtube`,
565
- `yt-analytics` and `userinfo.profile` permissions in order for tests to
566
- pass.
552
+ * `YT_TEST_CLIENT_ID`
553
+ * `YT_TEST_CLIENT_SECRET`
554
+ * `YT_TEST_API_KEY`
555
+ * `YT_TEST_REFRESH_TOKEN`
567
556
 
568
557
  How to release new versions
569
558
  ===========================
@@ -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 id: 'UCxxxxxxxxx'
144
+ channel = Yt::Channel.new url: 'youtube.com/liz'
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 id: 'UCxxxxxxxxx'
179
+ channel = Yt::Channel.new url: 'youtube.com/liz'
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 id: 'UCxxxxxxxxx'
191
+ channel = Yt::Channel.new url: 'youtube.com/liz'
192
192
  channel.related_playlists.find{|p| p.title == 'Favorites'}
193
193
  ```
194
194
 
@@ -832,4 +832,4 @@ TODO
832
832
 
833
833
  $ client.activity(user) #default current user
834
834
 
835
- -->
835
+ -->
data/lib/yt.rb CHANGED
@@ -8,12 +8,12 @@ require 'yt/models/content_owner'
8
8
  require 'yt/models/match_policy'
9
9
  require 'yt/models/playlist'
10
10
  require 'yt/models/playlist_item'
11
+ require 'yt/models/url'
11
12
  require 'yt/models/video'
12
13
  require 'yt/models/video_group'
13
14
  require 'yt/models/comment_thread'
14
15
  require 'yt/models/ownership'
15
16
  require 'yt/models/advertising_options_set'
16
- require 'yt/models/url'
17
17
 
18
18
  # An object-oriented Ruby client for YouTube.
19
19
  # Helps creating applications that need to interact with YouTube objects.
@@ -75,7 +75,7 @@ module Yt
75
75
  end
76
76
 
77
77
  def more_pages?
78
- @last_index.zero? || !@page_token.nil?
78
+ @last_index.zero? || @page_token.present?
79
79
  end
80
80
 
81
81
  def next_page
@@ -10,7 +10,7 @@ module Yt
10
10
  # @option options [Array<Symbol>] :only The metrics to generate reports
11
11
  # for.
12
12
  # @option options [Symbol] :by (:day) The dimension to collect metrics
13
- # by. Accepted values are: +:day+, +:week+, +:month+.
13
+ # by. Accepted values are: +:day+, +:month+.
14
14
  # @option options [#to_date] :since The first day of the time-range.
15
15
  # Also aliased as +:from+.
16
16
  # @option options [#to_date] :until The last day of the time-range.
@@ -40,11 +40,6 @@ module Yt
40
40
  # @example Get the $1 for this and last month:
41
41
  # resource.$1 since: 1.month.ago, by: :month
42
42
  # # => {Wed, 01 Apr 2014..Thu, 30 Apr 2014 => 12.0, Fri, 01 May 2014..Sun, 31 May 2014 => 34.0, …}
43
- # @return [Hash<Range<Date, Date>, $2>] if grouped by week, the $1
44
- # for each week in the time-range.
45
- # @example Get the $1 for this and last week:
46
- # resource.$1 since: 1.week.ago, by: :week
47
- # # => {Wed, 01 Apr 2014..Tue, 07 Apr 2014 => 20.0, Wed, 08 Apr 2014..Tue, 14 Apr 2014 => 13.0, …}
48
43
  # @macro report
49
44
 
50
45
  # @!macro [new] report_with_range
@@ -75,19 +70,19 @@ module Yt
75
70
 
76
71
  # @!macro [new] report_by_day
77
72
  # @option options [Symbol] :by (:day) The dimension to collect $1 by.
78
- # Accepted values are: +:day+, +:week+, +:month+.
73
+ # Accepted values are: +:day+, +:month+.
79
74
  # @macro report_with_day
80
75
 
81
76
  # @!macro [new] report_by_day_and_country
82
77
  # @option options [Symbol] :by (:day) The dimension to collect $1 by.
83
- # Accepted values are: +:day+, +:week+, +:month+, :+range+.
78
+ # Accepted values are: +:day+, +:month+, :+range+.
84
79
  # @macro report_with_day
85
80
  # @macro report_with_range
86
81
  # @macro report_with_country
87
82
 
88
83
  # @!macro [new] report_by_day_and_state
89
84
  # @option options [Symbol] :by (:day) The dimension to collect $1 by.
90
- # Accepted values are: +:day+, +:week+, +:month+, :+range+.
85
+ # Accepted values are: +:day+, +:month+, :+range+.
91
86
  # @macro report_with_day
92
87
  # @macro report_with_range
93
88
  # @macro report_with_country_and_state
@@ -128,7 +123,7 @@ module Yt
128
123
 
129
124
  # @!macro [new] report_by_video_dimensions
130
125
  # @option options [Symbol] :by (:day) The dimension to collect $1 by.
131
- # Accepted values are: +:day+, +:week+, +:month+, +:range+,
126
+ # Accepted values are: +:day+, +:month+, +:range+,
132
127
  # +:traffic_source+,+:search_term+, +:playback_location+,
133
128
  # +:related_video+, +:embedded_player_location+.
134
129
  # @option options [Array<Symbol>] :includes ([:id]) if grouped by
@@ -162,7 +157,7 @@ module Yt
162
157
 
163
158
  # @!macro [new] report_by_channel_dimensions
164
159
  # @option options [Symbol] :by (:day) The dimension to collect $1 by.
165
- # Accepted values are: +:day+, +:week+, +:month+, +:range+,
160
+ # Accepted values are: +:day+, +:month+, +:range+,
166
161
  # +:traffic_source+, +:search_term+, +:playback_location+, +:video+,
167
162
  # +:related_video+, +:playlist+, +:embedded_player_location+.
168
163
  # @return [Hash<Symbol, $2>] if grouped by embedded player location,
@@ -175,7 +170,7 @@ module Yt
175
170
 
176
171
  # @!macro [new] report_by_playlist_dimensions
177
172
  # @option options [Symbol] :by (:day) The dimension to collect $1 by.
178
- # Accepted values are: +:day+, +:week+, +:month+, +:range+,
173
+ # Accepted values are: +:day+, +:month+, +:range+,
179
174
  # +:traffic_source+, +:playback_location+, +:related_video+, +:video+,
180
175
  # +:playlist+.
181
176
  # @macro report_with_channel_dimensions
@@ -228,7 +223,7 @@ module Yt
228
223
  def define_reports_method(metric, type)
229
224
  (@metrics ||= {})[metric] = type
230
225
  define_method :reports do |options = {}|
231
- from = options[:since] || options[:from] || (options[:by].in?([:day, :week, :month]) ? 5.days.ago : '2005-02-01')
226
+ from = options[:since] || options[:from] || (options[:by].in?([:day, :month]) ? 5.days.ago : '2005-02-01')
232
227
  to = options[:until] || options[:to] || Date.today
233
228
  location = options[:in]
234
229
  country = location.is_a?(Hash) ? location[:country] : location
@@ -252,7 +247,7 @@ module Yt
252
247
 
253
248
  def define_metric_method(metric)
254
249
  define_method metric do |options = {}|
255
- from = options[:since] || options[:from] || (options[:by].in?([:day, :week, :month]) ? 5.days.ago : '2005-02-01')
250
+ from = options[:since] || options[:from] || (options[:by].in?([:day, :month]) ? 5.days.ago : '2005-02-01')
256
251
  to = options[:until] || options[:to] || Date.today
257
252
  location = options[:in]
258
253
  country = location.is_a?(Hash) ? location[:country] : location
@@ -6,7 +6,6 @@ module Yt
6
6
  class Reports < Base
7
7
  DIMENSIONS = Hash.new({name: 'day', parse: ->(day, *values) { @metrics.keys.zip(values.map{|v| {Date.iso8601(day) => v}}).to_h} }).tap do |hash|
8
8
  hash[:month] = {name: 'month', parse: ->(month, *values) { @metrics.keys.zip(values.map{|v| {Range.new(Date.strptime(month, '%Y-%m').beginning_of_month, Date.strptime(month, '%Y-%m').end_of_month) => v} }).to_h} }
9
- hash[:week] = {name: '7DayTotals', parse: ->(last_day_of_week, *values) { @metrics.keys.zip(values.map{|v| {Range.new(Date.strptime(last_day_of_week) - 6, Date.strptime(last_day_of_week)) => v} }).to_h} }
10
9
  hash[:range] = {parse: ->(*values) { @metrics.keys.zip(values.map{|v| {total: v}}).to_h } }
11
10
  hash[:traffic_source] = {name: 'insightTrafficSourceType', parse: ->(source, *values) { @metrics.keys.zip(values.map{|v| {TRAFFIC_SOURCES.key(source) => v}}).to_h} }
12
11
  hash[:playback_location] = {name: 'insightPlaybackLocationType', parse: ->(location, *values) { @metrics.keys.zip(values.map{|v| {PLAYBACK_LOCATIONS.key(location) => v}}).to_h} }
@@ -140,11 +139,6 @@ module Yt
140
139
  end
141
140
  if dimension == :month
142
141
  hash = hash.transform_values{|h| h.sort_by{|range, v| range.first}.to_h}
143
- elsif dimension == :week
144
- hash = hash.transform_values do |h|
145
- h.select{|range, v| range.last.wday == days_range.last.wday}.
146
- sort_by{|range, v| range.first}.to_h
147
- end
148
142
  elsif dimension == :day
149
143
  hash = hash.transform_values{|h| h.sort_by{|day, v| day}.to_h}
150
144
  elsif dimension.in? [:traffic_source, :country, :state, :playback_location, :device_type, :operating_system, :subscribed_status]
@@ -159,11 +153,15 @@ module Yt
159
153
  # same query is a workaround that works and can hardly cause any damage.
160
154
  # Similarly, once in while YouTube responds with a random 503 error.
161
155
  rescue Yt::Error => e
162
- (max_retries > 0) && rescue?(e) ? sleep(3) && within(days_range, country, state, dimension, videos, historical, max_retries - 1) : raise
156
+ (max_retries > 0) && rescue?(e) ? sleep(retry_time) && within(days_range, country, state, dimension, videos, historical, max_retries - 1) : raise
163
157
  end
164
158
 
165
159
  private
166
160
 
161
+ def retry_time
162
+ 3
163
+ end
164
+
167
165
  def type_cast(value, type)
168
166
  case [type]
169
167
  when [Integer] then value.to_i if value
@@ -67,13 +67,19 @@ module Yt
67
67
  # @option params [String] :description The video’s description.
68
68
  # @option params [Array<String>] :tags The video’s tags.
69
69
  # @option params [String] :privacy_status The video’s privacy status.
70
+ # @option params [Boolean] :self_declared_made_for_kids The video’s made for kids self-declaration.
70
71
  # @return [Yt::Models::Video] the newly uploaded video.
71
72
  def upload_video(path_or_url, params = {})
72
73
  file = open path_or_url, 'rb'
73
74
  session = resumable_sessions.insert file.size, upload_body(params)
74
75
 
75
76
  session.update(body: file) do |data|
76
- Yt::Video.new id: data['id'], snippet: data['snippet'], status: data['privacyStatus'], auth: self
77
+ Yt::Video.new(
78
+ id: data['id'],
79
+ snippet: data['snippet'],
80
+ status: data['status'],
81
+ auth: self
82
+ )
77
83
  end
78
84
  end
79
85
 
@@ -217,8 +223,12 @@ module Yt
217
223
  snippet[:categoryId] = snippet.delete(:category_id) if snippet[:category_id]
218
224
  body[:snippet] = snippet if snippet.any?
219
225
 
220
- status = params[:privacy_status]
221
- body[:status] = {privacyStatus: status} if status
226
+ privacy_status = params[:privacy_status]
227
+ self_declared_made_for_kids = params[:self_declared_made_for_kids]
228
+
229
+ body[:status] = {}
230
+ body[:status][:privacyStatus] = privacy_status if privacy_status
231
+ body[:status][:selfDeclaredMadeForKids] = self_declared_made_for_kids unless self_declared_made_for_kids.nil?
222
232
  end
223
233
  end
224
234
 
@@ -29,6 +29,25 @@ module Yt
29
29
  # @return [Time] the date and time that the channel was created.
30
30
  delegate :published_at, to: :snippet
31
31
 
32
+ ### STATUS ###
33
+
34
+ # @!attribute [r] made_for_kids?
35
+ # @return [Boolean, nil] This value indicates whether the channel is
36
+ # designated as child-directed, and it contains the current "made for
37
+ # kids" status of the channel.
38
+ def made_for_kids?
39
+ status.made_for_kids
40
+ end
41
+
42
+ # @!attribute [r] self_declared_made_for_kids?
43
+ # @return [Boolean, nil] In a channels.update request, this property
44
+ # allows the channel owner to designate the channel as
45
+ # child-directed. The property value is only returned if the channel
46
+ # owner authorized the API request.
47
+ def self_declared_made_for_kids?
48
+ status.self_declared_made_for_kids
49
+ end
50
+
32
51
  ### SUBSCRIPTION ###
33
52
 
34
53
  has_one :subscription
@@ -197,10 +216,6 @@ module Yt
197
216
  # @return [Integer] the number of times the channel has been viewed.
198
217
  delegate :view_count, to: :statistics_set
199
218
 
200
- # @!attribute [r] comment_count
201
- # @return [Integer] the number of comments for the channel.
202
- delegate :comment_count, to: :statistics_set
203
-
204
219
  # @!attribute [r] video_count
205
220
  # @return [Integer] the number of videos uploaded to the channel.
206
221
  delegate :video_count, to: :statistics_set
@@ -12,7 +12,13 @@ module Yt
12
12
 
13
13
  # @!attribute [r] id
14
14
  # @return [String] the ID that YouTube uses to identify each resource.
15
- attr_reader :id
15
+ def id
16
+ if @id.nil? && @match && @match[:kind] == :channel
17
+ @id ||= fetch_channel_id
18
+ else
19
+ @id
20
+ end
21
+ end
16
22
 
17
23
  ### STATUS ###
18
24
 
@@ -42,7 +48,13 @@ module Yt
42
48
 
43
49
  # @private
44
50
  def initialize(options = {})
45
- @id = options[:id]
51
+ if options[:url]
52
+ @url = options[:url]
53
+ @match = find_pattern_match
54
+ @id = @match['id']
55
+ else
56
+ @id = options[:id]
57
+ end
46
58
  @auth = options[:auth]
47
59
  @snippet = Snippet.new(data: options[:snippet]) if options[:snippet]
48
60
  @status = Status.new(data: options[:status]) if options[:status]
@@ -50,7 +62,11 @@ module Yt
50
62
 
51
63
  # @private
52
64
  def kind
53
- self.class.to_s.demodulize.underscore
65
+ if @url
66
+ @match[:kind].to_s
67
+ else
68
+ self.class.to_s.demodulize.underscore
69
+ end
54
70
  end
55
71
 
56
72
  # @private
@@ -66,8 +82,63 @@ module Yt
66
82
  end
67
83
  end
68
84
 
85
+ # @return [Array<Regexp>] patterns matching URLs of YouTube playlists.
86
+ PLAYLIST_PATTERNS = [
87
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/playlist/?\?list=(?<id>[a-zA-Z0-9_-]+)},
88
+ ]
89
+
90
+ # @return [Array<Regexp>] patterns matching URLs of YouTube videos.
91
+ VIDEO_PATTERNS = [
92
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/watch\?v=(?<id>[a-zA-Z0-9_-]{11})},
93
+ %r{^(?:https?://)?(?:www\.)?youtu\.be/(?<id>[a-zA-Z0-9_-]{11})},
94
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/embed/(?<id>[a-zA-Z0-9_-]{11})},
95
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/v/(?<id>[a-zA-Z0-9_-]{11})},
96
+ ]
97
+
98
+ # @return [Array<Regexp>] patterns matching URLs of YouTube channels.
99
+ CHANNEL_PATTERNS = [
100
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/channel/(?<id>UC[a-zA-Z0-9_-]{22})},
101
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/(?<format>c/|user/)?(?<name>[a-zA-Z0-9_-]+)}
102
+ ]
103
+
69
104
  private
70
105
 
106
+ def find_pattern_match
107
+ patterns.find do |kind, regex|
108
+ if data = @url.match(regex)
109
+ # Note: With Ruby 2.4, the following is data.named_captures
110
+ break data.names.zip(data.captures).to_h.merge kind: kind
111
+ end
112
+ end || {kind: :unknown}
113
+ end
114
+
115
+ def patterns
116
+ # @note: :channel *must* be the last since one of its regex eats the
117
+ # remaining patterns. In short, don't change the following order.
118
+ Enumerator.new do |patterns|
119
+ VIDEO_PATTERNS.each {|regex| patterns << [:video, regex]}
120
+ PLAYLIST_PATTERNS.each {|regex| patterns << [:playlist, regex]}
121
+ CHANNEL_PATTERNS.each {|regex| patterns << [:channel, regex]}
122
+ end
123
+ end
124
+
125
+ def fetch_channel_id
126
+ response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
127
+ http.request Net::HTTP::Get.new("/#{@match['format']}#{@match['name']}")
128
+ end
129
+ if response.is_a?(Net::HTTPRedirection)
130
+ response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
131
+ http.request Net::HTTP::Get.new(response['location'])
132
+ end
133
+ end
134
+ regex = %r{<meta itemprop="channelId" content="(?<id>UC[a-zA-Z0-9_-]{22})">}
135
+ if data = response.body.match(regex)
136
+ data[:id]
137
+ else
138
+ raise Yt::Errors::NoItems
139
+ end
140
+ end
141
+
71
142
  # Since YouTube API only returns tags on Videos#list, the memoized
72
143
  # `@snippet` is erased if the video was instantiated through Video#search
73
144
  # (e.g., by calling account.videos or channel.videos), so that the full