yt 0.32.6 → 0.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -4
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +22 -32
  5. data/YOUTUBE_IT.md +4 -4
  6. data/lib/yt.rb +0 -1
  7. data/lib/yt/associations/has_reports.rb +9 -14
  8. data/lib/yt/collections/reports.rb +5 -7
  9. data/lib/yt/models/resource.rb +69 -3
  10. data/lib/yt/models/url.rb +2 -60
  11. data/lib/yt/request.rb +6 -2
  12. data/lib/yt/version.rb +1 -1
  13. data/yt.gemspec +5 -2
  14. metadata +31 -169
  15. data/spec/collections/claims_spec.rb +0 -62
  16. data/spec/collections/comment_threads_spec.rb +0 -46
  17. data/spec/collections/playlist_items_spec.rb +0 -44
  18. data/spec/collections/playlists_spec.rb +0 -27
  19. data/spec/collections/policies_spec.rb +0 -30
  20. data/spec/collections/references_spec.rb +0 -30
  21. data/spec/collections/reports_spec.rb +0 -30
  22. data/spec/collections/subscriptions_spec.rb +0 -25
  23. data/spec/collections/videos_spec.rb +0 -43
  24. data/spec/constants/geography_spec.rb +0 -16
  25. data/spec/errors/forbidden_spec.rb +0 -10
  26. data/spec/errors/missing_auth_spec.rb +0 -24
  27. data/spec/errors/no_items_spec.rb +0 -10
  28. data/spec/errors/request_error_spec.rb +0 -44
  29. data/spec/errors/server_error_spec.rb +0 -10
  30. data/spec/errors/unauthorized_spec.rb +0 -10
  31. data/spec/models/account_spec.rb +0 -138
  32. data/spec/models/annotation_spec.rb +0 -180
  33. data/spec/models/asset_spec.rb +0 -32
  34. data/spec/models/channel_spec.rb +0 -127
  35. data/spec/models/claim_event_spec.rb +0 -62
  36. data/spec/models/claim_history_spec.rb +0 -27
  37. data/spec/models/claim_spec.rb +0 -223
  38. data/spec/models/comment_spec.rb +0 -40
  39. data/spec/models/comment_thread_spec.rb +0 -93
  40. data/spec/models/configuration_spec.rb +0 -44
  41. data/spec/models/content_detail_spec.rb +0 -52
  42. data/spec/models/content_owner_detail_spec.rb +0 -6
  43. data/spec/models/file_detail_spec.rb +0 -13
  44. data/spec/models/live_streaming_detail_spec.rb +0 -6
  45. data/spec/models/ownership_spec.rb +0 -59
  46. data/spec/models/player_spec.rb +0 -13
  47. data/spec/models/playlist_item_spec.rb +0 -120
  48. data/spec/models/playlist_spec.rb +0 -138
  49. data/spec/models/policy_rule_spec.rb +0 -63
  50. data/spec/models/policy_spec.rb +0 -41
  51. data/spec/models/rating_spec.rb +0 -12
  52. data/spec/models/reference_spec.rb +0 -249
  53. data/spec/models/request_spec.rb +0 -204
  54. data/spec/models/resource_spec.rb +0 -42
  55. data/spec/models/right_owner_spec.rb +0 -71
  56. data/spec/models/snippet_spec.rb +0 -13
  57. data/spec/models/statistics_set_spec.rb +0 -13
  58. data/spec/models/status_spec.rb +0 -13
  59. data/spec/models/subscription_spec.rb +0 -30
  60. data/spec/models/url_spec.rb +0 -78
  61. data/spec/models/video_category_spec.rb +0 -21
  62. data/spec/models/video_spec.rb +0 -669
  63. data/spec/requests/as_account/account_spec.rb +0 -143
  64. data/spec/requests/as_account/authentications_spec.rb +0 -127
  65. data/spec/requests/as_account/channel_spec.rb +0 -246
  66. data/spec/requests/as_account/channels_spec.rb +0 -18
  67. data/spec/requests/as_account/playlist_item_spec.rb +0 -55
  68. data/spec/requests/as_account/playlist_spec.rb +0 -218
  69. data/spec/requests/as_account/thumbnail.jpg +0 -0
  70. data/spec/requests/as_account/video.mp4 +0 -0
  71. data/spec/requests/as_account/video_spec.rb +0 -408
  72. data/spec/requests/as_content_owner/account_spec.rb +0 -29
  73. data/spec/requests/as_content_owner/advertising_options_set_spec.rb +0 -15
  74. data/spec/requests/as_content_owner/asset_spec.rb +0 -31
  75. data/spec/requests/as_content_owner/bulk_report_job_spec.rb +0 -19
  76. data/spec/requests/as_content_owner/channel_spec.rb +0 -1836
  77. data/spec/requests/as_content_owner/claim_history_spec.rb +0 -20
  78. data/spec/requests/as_content_owner/claim_spec.rb +0 -17
  79. data/spec/requests/as_content_owner/content_owner_spec.rb +0 -370
  80. data/spec/requests/as_content_owner/match_policy_spec.rb +0 -17
  81. data/spec/requests/as_content_owner/ownership_spec.rb +0 -25
  82. data/spec/requests/as_content_owner/playlist_spec.rb +0 -767
  83. data/spec/requests/as_content_owner/video_group_spec.rb +0 -112
  84. data/spec/requests/as_content_owner/video_spec.rb +0 -1223
  85. data/spec/requests/as_server_app/channel_spec.rb +0 -54
  86. data/spec/requests/as_server_app/comment_spec.rb +0 -22
  87. data/spec/requests/as_server_app/comment_thread_spec.rb +0 -27
  88. data/spec/requests/as_server_app/comment_threads_spec.rb +0 -41
  89. data/spec/requests/as_server_app/playlist_item_spec.rb +0 -30
  90. data/spec/requests/as_server_app/playlist_spec.rb +0 -33
  91. data/spec/requests/as_server_app/url_spec.rb +0 -94
  92. data/spec/requests/as_server_app/video_spec.rb +0 -60
  93. data/spec/requests/as_server_app/videos_spec.rb +0 -40
  94. data/spec/requests/unauthenticated/video_spec.rb +0 -14
  95. data/spec/spec_helper.rb +0 -20
  96. data/spec/support/fail_matcher.rb +0 -15
  97. 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: 7f3d794d74e724d49a7a96cf3d27dcfbeabcc0c42a7a9766deaae7e01be367c0
4
+ data.tar.gz: bf836aa2984cb6c6903881d56a6f60aad68504a5cb7013c276e6466ee075cf71
5
5
  SHA512:
6
- metadata.gz: 3d19c4c4c947855487e23d07fd89f6b11719af3cb11b8bc3d73594f4703be7f7990652050ac03449359d6861be02cd43eea2599839e2f9b8c0f304449bf084cf
7
- data.tar.gz: d794dfbbe1eddcaa98bf0ec0daa36983b7e69b97c8aafa060418999ed5a5806dbf3bbec18fe772a2383f28033f87b8f1364c4742f74abdc6b1a201a1209935a4
6
+ metadata.gz: 74662b9a898592295377a2e3bd51295682a67d07c773abc27945706c858860e74ce4ce4c77ca20455a674a746090603ef49261bce21b40418984430f93f5d18c
7
+ data.tar.gz: 74f82bc61b9bde908766aa6c1e19afe7da9fbeb42faeead0a37d2cc31def646b71f333c0e3dde43589027b28cf3e781d04d1824b58be7db99af3b31914debc0d
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,25 @@ 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.0 - 2020-04-10
10
+
11
+ If your code calls reports methods such as `views`, `likes`, or `reports`,
12
+ do not include `by: :week` option since `7DayTotals` dimension will no longer be
13
+ supported by YouTube API as of [April 15, 2020](https://developers.google.com/youtube/analytics/revision_history#october-15,-2019).
14
+
15
+ Use `by: :day` option instead and add up the numbers from each day.
16
+
17
+ If you keep using `by: :week` option after this change it will raise an error
18
+ (before the gem upgrade) or it will run with `day` dimension instead (after
19
+ the gem upgrade, like any other random input).
20
+
21
+ * [REMOVAL] Remove `by: :week` option for reports.
22
+ * [FEATURE] Add back the option of initializing a resource by its URL.
23
+ * [BUGFIX] Limit retries on refreshing tokens
24
+
25
+ **Breaking change**
26
+
27
+ 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
28
 
10
29
  ## 0.32.6 - 2020-02-07
11
30
 
data/README.md CHANGED
@@ -526,44 +526,34 @@ If a variable is set in both places, then `Yt.configure` takes precedence.
526
526
  How to test
527
527
  ===========
528
528
 
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:
529
+ To run tests:
535
530
 
536
531
  ```bash
537
- rspec spec/models spec/collections spec/errors
532
+ rspec
538
533
  ```
539
534
 
540
- To also run live-tests against the YouTube API, type:
535
+ We recommend RSpec >= 3.8.
541
536
 
542
- ```bash
543
- rspec
544
- ```
537
+ Yt comes with two different sets of tests:
538
+
539
+ 1. Unit tests in `spec/models`, `spec/collections` and `spec/errors`
540
+ 2. Legacy integration tests in `spec/requests`
541
+
542
+ Coming soon will be a new set of high-level integration tests.
543
+
544
+ Integration tests are recorded with VCR. Some of the tests refer to
545
+ fixture data that an arbitrary account may not have access to. If you
546
+ need to modify one of these tests or re-record the cassette, we'd
547
+ suggest working against your own version of the testing setup. Then in
548
+ your pull request, we can help canonize your test/fixtures.
549
+
550
+ Some of the integration tests require authentication. These can be set
551
+ with the following environment variables:
545
552
 
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.
553
+ * `YT_TEST_CLIENT_ID`
554
+ * `YT_TEST_CLIENT_SECRET`
555
+ * `YT_TEST_API_KEY`
556
+ * `YT_TEST_REFRESH_TOKEN`
567
557
 
568
558
  How to release new versions
569
559
  ===========================
@@ -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
@@ -13,7 +13,6 @@ require 'yt/models/video_group'
13
13
  require 'yt/models/comment_thread'
14
14
  require 'yt/models/ownership'
15
15
  require 'yt/models/advertising_options_set'
16
- require 'yt/models/url'
17
16
 
18
17
  # An object-oriented Ruby client for YouTube.
19
18
  # Helps creating applications that need to interact with YouTube objects.
@@ -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
@@ -12,7 +12,9 @@ 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
+ @id ||= @match['id'] || fetch_channel_id
17
+ end
16
18
 
17
19
  ### STATUS ###
18
20
 
@@ -42,7 +44,12 @@ module Yt
42
44
 
43
45
  # @private
44
46
  def initialize(options = {})
45
- @id = options[:id]
47
+ if options[:url]
48
+ @url = options[:url]
49
+ @match = find_pattern_match
50
+ else
51
+ @id = options[:id]
52
+ end
46
53
  @auth = options[:auth]
47
54
  @snippet = Snippet.new(data: options[:snippet]) if options[:snippet]
48
55
  @status = Status.new(data: options[:status]) if options[:status]
@@ -50,7 +57,11 @@ module Yt
50
57
 
51
58
  # @private
52
59
  def kind
53
- self.class.to_s.demodulize.underscore
60
+ if @url
61
+ @match[:kind].to_s
62
+ else
63
+ self.class.to_s.demodulize.underscore
64
+ end
54
65
  end
55
66
 
56
67
  # @private
@@ -66,8 +77,63 @@ module Yt
66
77
  end
67
78
  end
68
79
 
80
+ # @return [Array<Regexp>] patterns matching URLs of YouTube playlists.
81
+ PLAYLIST_PATTERNS = [
82
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/playlist/?\?list=(?<id>[a-zA-Z0-9_-]+)},
83
+ ]
84
+
85
+ # @return [Array<Regexp>] patterns matching URLs of YouTube videos.
86
+ VIDEO_PATTERNS = [
87
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/watch\?v=(?<id>[a-zA-Z0-9_-]{11})},
88
+ %r{^(?:https?://)?(?:www\.)?youtu\.be/(?<id>[a-zA-Z0-9_-]{11})},
89
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/embed/(?<id>[a-zA-Z0-9_-]{11})},
90
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/v/(?<id>[a-zA-Z0-9_-]{11})},
91
+ ]
92
+
93
+ # @return [Array<Regexp>] patterns matching URLs of YouTube channels.
94
+ CHANNEL_PATTERNS = [
95
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/channel/(?<id>UC[a-zA-Z0-9_-]{22})},
96
+ %r{^(?:https?://)?(?:www\.)?youtube\.com/(?<format>c/|user/)?(?<name>[a-zA-Z0-9_-]+)}
97
+ ]
98
+
69
99
  private
70
100
 
101
+ def find_pattern_match
102
+ patterns.find do |kind, regex|
103
+ if data = @url.match(regex)
104
+ # Note: With Ruby 2.4, the following is data.named_captures
105
+ break data.names.zip(data.captures).to_h.merge kind: kind
106
+ end
107
+ end || {kind: :unknown}
108
+ end
109
+
110
+ def patterns
111
+ # @note: :channel *must* be the last since one of its regex eats the
112
+ # remaining patterns. In short, don't change the following order.
113
+ Enumerator.new do |patterns|
114
+ VIDEO_PATTERNS.each {|regex| patterns << [:video, regex]}
115
+ PLAYLIST_PATTERNS.each {|regex| patterns << [:playlist, regex]}
116
+ CHANNEL_PATTERNS.each {|regex| patterns << [:channel, regex]}
117
+ end
118
+ end
119
+
120
+ def fetch_channel_id
121
+ response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
122
+ http.request Net::HTTP::Get.new("/#{@match['format']}#{@match['name']}")
123
+ end
124
+ if response.is_a?(Net::HTTPRedirection)
125
+ response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
126
+ http.request Net::HTTP::Get.new(response['location'])
127
+ end
128
+ end
129
+ regex = %r{<meta itemprop="channelId" content="(?<id>UC[a-zA-Z0-9_-]{22})">}
130
+ if data = response.body.match(regex)
131
+ data[:id]
132
+ else
133
+ raise Yt::Errors::NoItems
134
+ end
135
+ end
136
+
71
137
  # Since YouTube API only returns tags on Videos#list, the memoized
72
138
  # `@snippet` is erased if the video was instantiated through Video#search
73
139
  # (e.g., by calling account.videos or channel.videos), so that the full
@@ -14,18 +14,17 @@ module Yt
14
14
  # @param [String] text the name or URL of a YouTube resource (in any form).
15
15
  def initialize(text)
16
16
  @text = text.to_s.strip
17
- @match = find_pattern_match
18
17
  end
19
18
 
20
19
  # @return [Symbol] the kind of YouTube resource matching the URL.
21
20
  # Possible values are: +:playlist+, +:video+, +:channel+, and +:unknown:.
22
21
  def kind
23
- @match[:kind]
22
+ Resource.new(url: @text).kind.to_sym
24
23
  end
25
24
 
26
25
  # @return [<String, nil>] the ID of the YouTube resource matching the URL.
27
26
  def id
28
- @match['id'] ||= fetch_id
27
+ Resource.new(url: @text).id
29
28
  end
30
29
 
31
30
  # @return [<Yt::Channel>] the resource associated with the URL
@@ -37,63 +36,6 @@ module Yt
37
36
  else raise Yt::Errors::NoItems
38
37
  end.new options.merge(id: id)
39
38
  end
40
-
41
- # @return [Array<Regexp>] patterns matching URLs of YouTube playlists.
42
- PLAYLIST_PATTERNS = [
43
- %r{^(?:https?://)?(?:www\.)?youtube\.com/playlist/?\?list=(?<id>[a-zA-Z0-9_-]+)},
44
- ]
45
-
46
- # @return [Array<Regexp>] patterns matching URLs of YouTube videos.
47
- VIDEO_PATTERNS = [
48
- %r{^(?:https?://)?(?:www\.)?youtube\.com/watch\?v=(?<id>[a-zA-Z0-9_-]{11})},
49
- %r{^(?:https?://)?(?:www\.)?youtu\.be/(?<id>[a-zA-Z0-9_-]{11})},
50
- %r{^(?:https?://)?(?:www\.)?youtube\.com/embed/(?<id>[a-zA-Z0-9_-]{11})},
51
- %r{^(?:https?://)?(?:www\.)?youtube\.com/v/(?<id>[a-zA-Z0-9_-]{11})},
52
- ]
53
-
54
- # @return [Array<Regexp>] patterns matching URLs of YouTube channels.
55
- CHANNEL_PATTERNS = [
56
- %r{^(?:https?://)?(?:www\.)?youtube\.com/channel/(?<id>UC[a-zA-Z0-9_-]{22})},
57
- %r{^(?:https?://)?(?:www\.)?youtube\.com/(?<format>c/|user/)?(?<name>[a-zA-Z0-9_-]+)}
58
- ]
59
-
60
- private
61
-
62
- def find_pattern_match
63
- patterns.find(-> {{kind: :unknown}}) do |kind, regex|
64
- if data = @text.match(regex)
65
- # Note: With Ruby 2.4, the following is data.named_captures
66
- break data.names.zip(data.captures).to_h.merge kind: kind
67
- end
68
- end
69
- end
70
-
71
- def patterns
72
- # @note: :channel *must* be the last since one of its regex eats the
73
- # remaining patterns. In short, don't change the following order.
74
- Enumerator.new do |patterns|
75
- VIDEO_PATTERNS.each {|regex| patterns << [:video, regex]}
76
- PLAYLIST_PATTERNS.each {|regex| patterns << [:playlist, regex]}
77
- CHANNEL_PATTERNS.each {|regex| patterns << [:channel, regex]}
78
- end
79
- end
80
-
81
- def fetch_id
82
- response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
83
- http.request Net::HTTP::Get.new("/#{@match['format']}#{@match['name']}")
84
- end
85
- if response.is_a?(Net::HTTPRedirection)
86
- response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
87
- http.request Net::HTTP::Get.new(response['location'])
88
- end
89
- end
90
- regex = %r{<meta itemprop="channelId" content="(?<id>UC[a-zA-Z0-9_-]{22})">}
91
- if data = response.body.match(regex)
92
- data[:id]
93
- else
94
- raise Yt::Errors::NoItems
95
- end
96
- end
97
39
  end
98
40
  end
99
41
  end