yt 0.32.6 → 0.33.0

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 (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