yt 0.32.6 → 0.33.4

Sign up to get free protection for your applications and to get access to all the features.
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