yt 0.7.6 → 0.7.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 21e026d255684b3c18d8291bd4eeda02ff233f0c
4
- data.tar.gz: 9af923985d7022b16b6d31b8483af39f617015bf
3
+ metadata.gz: 7f3bc792631d1995834e498558253d038a781341
4
+ data.tar.gz: c01d949dd3e7b77368c37f62bf3dcd9d90224be8
5
5
  SHA512:
6
- metadata.gz: 0e8b1696e652a865442b0b604882293a9057fe193092167793da12e7a273e615605072b3eaffd3e661f2c65471c0ad6a958bbc21c61653e4aee3f05c4ebe904d
7
- data.tar.gz: 90b38724c6349dfd14100bb3f4bf12bf2187d704ebd3c069acd5f6bfa6dc1e2772acaf8c1306483cde36e3a2fa1beaecde069e6f0917b44af503b7cc8c2dd2e4
6
+ metadata.gz: 23993ad7e807201c61d8b129f52e4a8a73ddb9546435b3b78dd06926b96a31646b167a936919f8f5d8354c698dc62ee8c3447ea83ef4e1ecd47b0584156e154a
7
+ data.tar.gz: ce5c7b8c9608b3d263311abcb176055c6ff9015d4869d487f3505d6e6a55eaa8e2b9459f81d777b80014c2cfabae5c83f019a84d0431490513dc170424a0b218
data/.yardopts CHANGED
@@ -1,3 +1,3 @@
1
1
  --no-private
2
- lib/yt/modules/reports.rb
2
+ lib/yt/associations/has_reports.rb
3
3
  lib/**/*.rb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- yt (0.7.6)
4
+ yt (0.7.7)
5
5
  activesupport
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -471,7 +471,7 @@ To install on your system, run
471
471
 
472
472
  To use inside a bundled Ruby project, add this line to the Gemfile:
473
473
 
474
- gem 'yt', '~> 0.7.6'
474
+ gem 'yt', '~> 0.7.7'
475
475
 
476
476
  Since the gem follows [Semantic Versioning](http://semver.org),
477
477
  indicating the full version in your Gemfile (~> *major*.*minor*.*patch*)
data/bin/yt CHANGED
@@ -9,33 +9,96 @@ end
9
9
 
10
10
  ############################
11
11
 
12
- channel = Yt::Channel.new id: ARGV[0] || 'UCxO1tY8h1AhOz0T4ENwmpow'
13
-
14
- puts "Title: #{channel.title}"
15
- puts "Description: #{channel.description}"
16
- puts "Thumbnail: #{channel.thumbnail_url}"
17
- puts "Public? #{channel.public?}"
18
- puts "Views: #{channel.view_count}"
19
- puts "Comments: #{channel.comment_count}"
20
- puts "Videos: #{channel.video_count}"
21
- puts "Subscribers: #{channel.subscriber_count}"
22
- puts "Subscribers are visible? #{channel.subscriber_count_visible?}"
23
-
24
- puts "Videos: "
25
- channel.videos.each do |video|
26
- puts " Annotations: #{video.annotations.count}"
27
- puts " Duration: #{video.duration}s"
12
+ # account = Yt::Account.new refresh_token: ENV['YT_TEST_DEVICE_REFRESH_TOKEN']
13
+
14
+ # youtube.readonly and yt-analytics.readonly are not available with device :(
15
+ account = Yt::Account.new scopes: %w(userinfo.email userinfo.profile youtube)
16
+ 0.upto(60) do |i|
17
+ begin
18
+ break if account.authentication
19
+ rescue Yt::Errors::MissingAuth => e
20
+ puts e.more_details if i.zero?
21
+ 5.times {print '.'; sleep 1}
22
+ end
23
+ end
24
+
25
+ puts "\nACCOUNT:\n"
26
+ puts " ID: #{account.id}"
27
+ puts " Email: #{account.email}"
28
+ puts " Email verified? #{account.has_verified_email?}"
29
+ puts " Gender: #{account.gender}"
30
+ puts " Name: #{account.name}"
31
+ puts " Given Name: #{account.given_name}"
32
+ puts " Family Name: #{account.family_name}"
33
+ puts " Profile URL: #{account.profile_url}"
34
+ puts " Avatar URL: #{account.avatar_url}"
35
+ puts " Locale: #{account.locale}"
36
+ puts " Hd? #{account.hd}"
37
+
38
+ puts "\nCHANNEL:\n"
39
+ channel = account.channel
40
+ puts " Title: #{channel.title}"
41
+ puts " Description: #{channel.description.truncate(30)}"
42
+ puts " Thumbnail URL: #{channel.thumbnail_url}"
43
+ puts " Published at: #{channel.published_at}"
44
+ puts " Public? #{channel.public?}"
45
+ puts " Views: #{channel.view_count}"
46
+ puts " Comments: #{channel.comment_count}"
47
+ puts " Videos: #{channel.video_count}"
48
+ puts " Subscribers: #{channel.subscriber_count}"
49
+ puts " Subscribers are visible? #{channel.subscriber_count_visible?}"
50
+ # These are not available with a device auth :(
51
+ # puts " Views: #{channel.views}"
52
+ # puts " Comments: #{channel.comments}"
53
+ # puts " Likes: #{channel.likes}"
54
+ # puts " Dislikes: #{channel.dislikes}"
55
+ # puts " Shares: #{channel.shares}"
56
+
57
+ account.videos.first(5).each.with_index do |video, i|
58
+ puts "\nVIDEO #{i+1}:\n"
28
59
  puts " Title: #{video.title}"
29
- puts " Description: #{video.description}"
30
- puts " Thumbnail: #{video.thumbnail_url}"
60
+ puts " Description: #{video.description.truncate(30)}"
61
+ puts " Thumbnail URL: #{video.thumbnail_url}"
62
+ puts " Published at: #{video.published_at}"
63
+ puts " Tags: #{video.tags}"
64
+ puts " Channel ID: #{video.channel_id}"
65
+ puts " Channel Title: #{video.channel_title}"
66
+ puts " Category ID: #{video.category_id}"
67
+ puts " Live content? #{video.live_broadcast_content}"
31
68
  puts " Public? #{video.public?}"
32
69
  puts " Views: #{video.view_count}"
33
70
  puts " Comments: #{video.comment_count}"
34
71
  puts " Likes: #{video.like_count}"
35
72
  puts " Dislikes: #{video.dislike_count}"
36
73
  puts " Favorites: #{video.favorite_count}"
37
- puts " hd? #{video.hd?}"
74
+ puts " Duration: #{video.duration}s"
75
+ puts " HD: #{video.hd?}"
38
76
  puts " stereoscopic? #{video.stereoscopic?}"
39
77
  puts " captioned? #{video.captioned?}"
40
78
  puts " licensed? #{video.licensed?}"
79
+ # These are not available with a device auth :(
80
+ # puts " Views: #{video.views}"
81
+ # puts " Comments: #{video.comments}"
82
+ # puts " Likes: #{video.likes}"
83
+ # puts " Dislikes: #{video.dislikes}"
84
+ # puts " Shares: #{video.shares}"
85
+ puts " Annotations: #{video.annotations.count}"
86
+ end
87
+
88
+ account.playlists.first(5).each.with_index do |playlist, i|
89
+ puts "\nPLAYLIST #{i+1}:\n"
90
+ puts " Title: #{playlist.title}"
91
+ puts " Description: #{playlist.description.truncate(30)}"
92
+ puts " Thumbnail URL: #{playlist.thumbnail_url}"
93
+ puts " Published at: #{playlist.published_at}"
94
+ puts " Tags: #{playlist.tags}"
95
+ puts " Channel ID: #{playlist.channel_id}"
96
+ puts " Channel Title: #{playlist.channel_title}"
97
+ puts " Public? #{playlist.public?}"
98
+ puts " Playlist items: #{playlist.playlist_items.count}"
99
+ playlist.playlist_items.first(5).each.with_index do |playlist_item, j|
100
+ puts " \nPLAYLIST ITEM #{j+1}:\n"
101
+ puts " Position: #{playlist_item.position + 1}"
102
+ puts " Video ID: #{playlist_item.video_id}"
103
+ end
41
104
  end
@@ -1,20 +1,28 @@
1
- require 'yt/collections/authentications'
2
- require 'yt/config'
3
- require 'yt/errors/no_items'
4
- require 'yt/errors/unauthorized'
5
-
6
1
  module Yt
7
- module Modules
8
- # Provides authentication methods to YouTube resources, which
9
- # allows to access to content detail set-specific methods like `access_token`.
2
+ module Associations
3
+ # Provides authentication methods to YouTube resources, which allows to
4
+ # access to content detail set-specific methods like `access_token`.
10
5
  #
11
6
  # YouTube resources with authentication are: {Yt::Models::Account accounts}.
12
- module Authentication
7
+ module HasAuthentication
8
+ def has_authentication
9
+ require 'yt/collections/authentications'
10
+ require 'yt/collections/device_flows'
11
+ require 'yt/errors/missing_auth'
12
+ require 'yt/errors/no_items'
13
+ require 'yt/errors/unauthorized'
14
+
15
+ include Associations::Authenticable
16
+ end
17
+ end
18
+
19
+ module Authenticable
13
20
  delegate :access_token, :refresh_token, :expires_at, to: :authentication
14
21
 
15
22
  def initialize(options = {})
16
23
  @access_token = options[:access_token]
17
24
  @refresh_token = options[:refresh_token]
25
+ @device_code = options[:device_code]
18
26
  @expires_at = options[:expires_at]
19
27
  @authorization_code = options[:authorization_code]
20
28
  @redirect_uri = options[:redirect_uri]
@@ -27,7 +35,10 @@ module Yt
27
35
 
28
36
  def authentication
29
37
  @authentication = current_authentication
30
- @authentication ||= new_authentication || refreshed_authentication!
38
+ @authentication ||= use_refresh_token! if @refresh_token
39
+ @authentication ||= use_authorization_code! if @authorization_code
40
+ @authentication ||= use_device_code! if @device_code
41
+ @authentication ||= raise_missing_authentication!
31
42
  end
32
43
 
33
44
  def authentication_url
@@ -61,23 +72,55 @@ module Yt
61
72
  end
62
73
 
63
74
  # Tries to obtain an access token using the authorization code (which
64
- # can only be used once). On failure, does not raise an error because
65
- # the access token might still be retrieved with a refresh token.
66
- def new_authentication
75
+ # can only be used once). On failure, raise an error.
76
+ def use_authorization_code!
67
77
  new_authentications.first!
68
78
  rescue Errors::NoItems => error
69
- nil
79
+ raise Errors::Unauthorized, error.to_param
70
80
  end
71
81
 
72
82
  # Tries to obtain an access token using the refresh token (which can
73
- # be used multiple times). On failure, raise an error because there are
74
- # no more options to obtain an access token.
75
- def refreshed_authentication!
83
+ # be used multiple times). On failure, raise an error.
84
+ def use_refresh_token!
76
85
  refreshed_authentications.first!
77
86
  rescue Errors::NoItems => error
78
87
  raise Errors::Unauthorized, error.to_param
79
88
  end
80
89
 
90
+ # Tries to obtain an access token using the device code (which must be
91
+ # confirmed by the user with the user_code). On failure, raise an error.
92
+ def use_device_code!
93
+ device_code_authentications.first!.tap do |auth|
94
+ raise Errors::MissingAuth, pending_device_code_message if auth.pending?
95
+ end
96
+ end
97
+
98
+ def raise_missing_authentication!
99
+ error_message = case
100
+ when @redirect_uri && @scopes then missing_authorization_code_message
101
+ when @scopes then pending_device_code_message
102
+ end
103
+ raise Errors::MissingAuth, error_message
104
+ end
105
+
106
+ def pending_device_code_message
107
+ @device_flow ||= device_flows.first!
108
+ @device_code ||= @device_flow.device_code
109
+ {}.tap do |params|
110
+ params[:scopes] = @scopes
111
+ params[:user_code] = @device_flow.user_code
112
+ params[:verification_url] = @device_flow.verification_url
113
+ end
114
+ end
115
+
116
+ def missing_authorization_code_message
117
+ {}.tap do |params|
118
+ params[:scopes] = @scopes
119
+ params[:authentication_url] = authentication_url
120
+ params[:redirect_uri] = @redirect_uri
121
+ end
122
+ end
123
+
81
124
  def new_authentications
82
125
  @new_authentications ||= Collections::Authentications.of(self).tap do |auth|
83
126
  auth.auth_params = new_authentication_params
@@ -90,6 +133,18 @@ module Yt
90
133
  end
91
134
  end
92
135
 
136
+ def device_code_authentications
137
+ Collections::Authentications.of(self).tap do |auth|
138
+ auth.auth_params = device_code_authentication_params
139
+ end
140
+ end
141
+
142
+ def device_flows
143
+ @device_flows ||= Collections::DeviceFlows.of(self).tap do |auth|
144
+ auth.auth_params = device_flow_params
145
+ end
146
+ end
147
+
93
148
  def authentication_url_params
94
149
  {}.tap do |params|
95
150
  params[:client_id] = client_id
@@ -126,6 +181,22 @@ module Yt
126
181
  end
127
182
  end
128
183
 
184
+ def device_code_authentication_params
185
+ {}.tap do |params|
186
+ params[:client_id] = client_id
187
+ params[:client_secret] = client_secret
188
+ params[:code] = @device_code
189
+ params[:grant_type] = 'http://oauth.net/grant_type/device/1.0'
190
+ end
191
+ end
192
+
193
+ def device_flow_params
194
+ {}.tap do |params|
195
+ params[:client_id] = client_id
196
+ params[:scope] = authentication_scope
197
+ end
198
+ end
199
+
129
200
  def client_id
130
201
  Yt.configuration.client_id
131
202
  end
@@ -1,34 +1,23 @@
1
- require 'active_support' # does not load anything by default but is required
2
- require 'active_support/core_ext/module/delegation' # for delegate
3
- require 'active_support/core_ext/string/inflections' # for camelize/constantize
4
-
5
1
  module Yt
6
- module Modules
2
+ module Associations
7
3
  # Associations are a set of macro-like class methods to express
8
4
  # relationship between YouTube resources like "Channel has many Videos" or
9
5
  # "Account has one Id". They are inspired by ActiveRecord::Associations.
10
- module Associations
6
+ module HasMany
11
7
  # @example Adds the +videos+ method to the Channel resource.
12
8
  # class Channel < Resource
13
9
  # has_many :videos
14
10
  # end
15
11
  def has_many(attributes)
12
+ require 'active_support' # does not load anything by default
13
+ require 'active_support/core_ext/string/inflections' # for camelize ...
16
14
  require "yt/collections/#{attributes}"
15
+
17
16
  collection_name = attributes.to_s.sub(/.*\./, '').camelize.pluralize
18
17
  collection = "Yt::Collections::#{collection_name}".constantize
19
18
  define_memoized_method(attributes) { collection.of self }
20
19
  end
21
20
 
22
- # @example Adds the +status+ method to the Video resource.
23
- # class Video < Resource
24
- # has_one :status
25
- # end
26
- def has_one(attribute)
27
- attributes = attribute.to_s.pluralize
28
- has_many attributes
29
- define_memoized_method(attribute) { send(attributes).first! }
30
- end
31
-
32
21
  private
33
22
 
34
23
  # A wrapper around Ruby’s +define_method+ that, in addition to adding an
@@ -0,0 +1,21 @@
1
+ module Yt
2
+ module Associations
3
+ # Associations are a set of macro-like class methods to express
4
+ # relationship between YouTube resources like "Channel has many Videos" or
5
+ # "Account has one Id". They are inspired by ActiveRecord::Associations.
6
+ module HasOne
7
+ # @example Adds the +status+ method to the Video resource.
8
+ # class Video < Resource
9
+ # has_one :status
10
+ # end
11
+ def has_one(attribute)
12
+ require 'yt/associations/has_many'
13
+ extend Associations::HasMany
14
+
15
+ attributes = attribute.to_s.pluralize
16
+ has_many attributes
17
+ define_memoized_method(attribute) { send(attributes).first! }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,11 +1,10 @@
1
- require 'yt/collections/reports'
2
-
3
1
  module Yt
4
- module Modules
2
+ module Associations
5
3
  # Provides methods to to access the analytics reports of a resource.
6
4
  #
7
- # YouTube resources with reports are: {Yt::Models::Channel channels}.
8
- module Reports
5
+ # YouTube resources with reports are: {Yt::Models::Channel channels} and
6
+ # {Yt::Models::Channel videos}.
7
+ module HasReports
9
8
  # @!macro has_report
10
9
  # @!method $1_on(date)
11
10
  # @return [Float] the $1 for a single day.
@@ -26,6 +25,8 @@ module Yt
26
25
  # has_report :earnings
27
26
  # end
28
27
  def has_report(metric)
28
+ require 'yt/collections/reports'
29
+
29
30
  define_method "#{metric}_on" do |date|
30
31
  send(metric, from: date, to: date).values.first
31
32
  end
@@ -0,0 +1,32 @@
1
+ require 'yt/collections/base'
2
+ require 'yt/models/device_flow'
3
+
4
+ module Yt
5
+ module Collections
6
+ class DeviceFlows < Base
7
+ attr_accessor :auth_params
8
+
9
+ private
10
+
11
+ def new_item(data)
12
+ Yt::DeviceFlow.new data: data
13
+ end
14
+
15
+ def list_params
16
+ super.tap do |params|
17
+ params[:host] = 'accounts.google.com'
18
+ params[:path] = '/o/oauth2/device/code'
19
+ params[:body_type] = :form
20
+ params[:method] = :post
21
+ params[:auth] = nil
22
+ params[:body] = auth_params
23
+ end
24
+ end
25
+
26
+ def next_page
27
+ request = Yt::Request.new list_params
28
+ Array.wrap request.run.body
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,81 @@
1
+ require 'yt/errors/request_error'
2
+
3
+ module Yt
4
+ module Errors
5
+ class MissingAuth < RequestError
6
+ def message
7
+ <<-MSG.gsub(/^ {8}/, '')
8
+ A request to YouTube API was sent without a valid authentication.
9
+
10
+ #{more_details}
11
+ MSG
12
+ end
13
+
14
+ def more_details
15
+ if scopes && authentication_url && redirect_uri
16
+ more_details_with_authentication_url
17
+ elsif scopes && user_code && verification_url
18
+ more_details_with_verification_url
19
+ else
20
+ more_details_without_url
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def more_details_with_authentication_url
27
+ <<-MSG.gsub(/^ {8}/, '')
28
+ You can ask YouTube accounts to authenticate your app for the scopes
29
+ #{scopes} by directing them to #{authentication_url}.
30
+
31
+ After they provide access to their account, they will be redirected to
32
+ #{redirect_uri} with a 'code' query parameter that you can read and use
33
+ to build an authorized account object by running:
34
+
35
+ Yt::Account.new authorization_code: code, redirect_uri: "#{redirect_uri}"
36
+ MSG
37
+ end
38
+
39
+ def more_details_with_verification_url
40
+ <<-MSG.gsub(/^ {8}/, '')
41
+ Please authenticate your app by visiting the page #{verification_url}
42
+ and entering the code #{user_code} before continuing.
43
+ MSG
44
+ end
45
+
46
+ def more_details_without_url
47
+ <<-MSG.gsub(/^ {8}/, '')
48
+ If you know the access token of the YouTube you want to authenticate
49
+ with, build an authorized account object by running:
50
+
51
+ Yt::Account.new access_token: access_token
52
+
53
+ If you know the refresh token of the YouTube you want to authenticate
54
+ with, build an authorized account object by running:
55
+
56
+ Yt::Account.new refresh_token: refresh_token
57
+ MSG
58
+ end
59
+
60
+ def scopes
61
+ @msg[:scopes]
62
+ end
63
+
64
+ def authentication_url
65
+ @msg[:authentication_url]
66
+ end
67
+
68
+ def redirect_uri
69
+ @msg[:redirect_uri]
70
+ end
71
+
72
+ def user_code
73
+ @msg[:user_code]
74
+ end
75
+
76
+ def verification_url
77
+ @msg[:verification_url]
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,7 +1,7 @@
1
1
  module Yt
2
2
  module Errors
3
3
  class RequestError < StandardError
4
- def initialize(msg = nil)
4
+ def initialize(msg = {})
5
5
  @msg = msg
6
6
  super msg
7
7
  end
@@ -1,14 +1,10 @@
1
1
  require 'yt/models/base'
2
- require 'yt/modules/authentication'
3
2
 
4
3
  module Yt
5
4
  module Models
6
5
  # Provides methods to interact with YouTube accounts.
7
6
  # @see https://developers.google.com/youtube/v3/guides/authentication
8
7
  class Account < Base
9
- # Includes methods to authenticate with YouTube API.
10
- include Modules::Authentication
11
-
12
8
  # @!attribute [r] channel
13
9
  # @return [Yt::Models::Channel] the account’s channel.
14
10
  has_one :channel
@@ -28,6 +24,8 @@ module Yt
28
24
  # @return [nil] if the account is not a partnered content owner.
29
25
  attr_reader :owner_name
30
26
 
27
+ has_authentication
28
+
31
29
  # @private
32
30
  # Tells `has_many :videos` that account.videos should return all the
33
31
  # videos *owned by* the account (public, private, unlisted).
@@ -55,6 +55,7 @@ module Yt
55
55
  def initialize(data = {})
56
56
  @access_token = data['access_token']
57
57
  @refresh_token = data['refresh_token']
58
+ @error = data['error']
58
59
  @expires_at = expiration_date data.slice('expires_at', 'expires_in')
59
60
  end
60
61
 
@@ -63,6 +64,11 @@ module Yt
63
64
  @expires_at && @expires_at.past?
64
65
  end
65
66
 
67
+ # @return [Boolean] whether the device auth is pending
68
+ def pending?
69
+ @error == 'authorization_pending'
70
+ end
71
+
66
72
  private
67
73
 
68
74
  def expiration_date(options = {})
@@ -1,15 +1,23 @@
1
1
  require 'yt/actions/delete'
2
2
  require 'yt/actions/update'
3
- require 'yt/modules/associations'
3
+
4
+ require 'yt/associations/has_authentication'
5
+ require 'yt/associations/has_many'
6
+ require 'yt/associations/has_one'
7
+ require 'yt/associations/has_reports'
8
+
4
9
  require 'yt/errors/request_error'
5
10
 
6
11
  module Yt
7
12
  module Models
8
13
  class Base
9
- extend Modules::Associations
10
-
11
14
  include Actions::Delete
12
15
  include Actions::Update
16
+
17
+ extend Associations::HasReports
18
+ extend Associations::HasOne
19
+ extend Associations::HasMany
20
+ extend Associations::HasAuthentication
13
21
  end
14
22
  end
15
23
 
@@ -1,14 +1,10 @@
1
1
  require 'yt/models/resource'
2
- require 'yt/modules/reports'
3
2
 
4
3
  module Yt
5
4
  module Models
6
5
  # A channel resource contains information about a YouTube channel.
7
6
  # @see https://developers.google.com/youtube/v3/docs/channels
8
7
  class Channel < Resource
9
- # Includes the +:has_report+ method to access YouTube Analytics reports.
10
- extend Modules::Reports
11
-
12
8
  # @!attribute [r] subscriptions
13
9
  # @return [Yt::Collections::Subscriptions] the channel’s subscriptions.
14
10
  has_many :subscriptions
@@ -0,0 +1,23 @@
1
+ require 'yt/models/base'
2
+
3
+ module Yt
4
+ module Models
5
+ class DeviceFlow < Base
6
+ def initialize(options = {})
7
+ @data = options[:data]
8
+ end
9
+
10
+ def device_code
11
+ @device_code ||= @data['device_code']
12
+ end
13
+
14
+ def user_code
15
+ @user_code ||= @data['user_code']
16
+ end
17
+
18
+ def verification_url
19
+ @verification_url ||= @data['verification_url']
20
+ end
21
+ end
22
+ end
23
+ end
@@ -159,11 +159,14 @@ module Yt
159
159
  # If a request authorized with an access token returns 401, then the
160
160
  # access token might have expired. If a refresh token is also present,
161
161
  # try to run the request one more time with a refreshed access token.
162
+ # If it's not present, then don't raise the returned MissingAuth, just
163
+ # let the original error bubble up.
162
164
  def refresh_token_and_retry?
163
165
  if response.is_a? Net::HTTPUnauthorized
164
- @response = @http_request = @uri = nil
165
- @auth.refresh
166
+ @auth.refresh.tap { @response = @http_request = @uri = nil }
166
167
  end if @auth.respond_to? :refresh
168
+ rescue Errors::MissingAuth
169
+ false
167
170
  end
168
171
 
169
172
  def response_error
@@ -1,14 +1,10 @@
1
1
  require 'yt/models/resource'
2
- require 'yt/modules/reports'
3
2
 
4
3
  module Yt
5
4
  module Models
6
5
  # Provides methods to interact with YouTube videos.
7
6
  # @see https://developers.google.com/youtube/v3/docs/videos
8
7
  class Video < Resource
9
- # Includes the +:has_report+ method to access YouTube Analytics reports.
10
- extend Modules::Reports
11
-
12
8
  delegate :tags, :channel_id, :channel_title, :category_id,
13
9
  :live_broadcast_content, to: :snippet
14
10
 
data/lib/yt/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Yt
2
- VERSION = '0.7.6'
2
+ VERSION = '0.7.7'
3
3
  end
@@ -1,10 +1,24 @@
1
1
  require 'spec_helper'
2
- require 'yt/errors/unauthorized'
2
+ require 'yt/errors/missing_auth'
3
3
 
4
- describe Yt::Errors::Unauthorized do
4
+ describe Yt::Errors::MissingAuth do
5
+ subject(:error) { raise Yt::Errors::MissingAuth, params }
6
+ let(:params) { {} }
5
7
  let(:msg) { %r{^A request to YouTube API was sent without a valid authentication} }
6
8
 
7
9
  describe '#exception' do
8
- it { expect{raise Yt::Errors::Unauthorized}.to raise_error msg }
10
+ it { expect{error}.to raise_error msg }
11
+
12
+ context 'given the user can authenticate via web' do
13
+ let(:params) { {scopes: 'youtube', authentication_url: 'http://google.example.com/auth', redirect_uri: 'http://localhost/'} }
14
+ let(:msg) { %r{^You can ask YouTube accounts to authenticate your app} }
15
+ it { expect{error}.to raise_error msg }
16
+ end
17
+
18
+ context 'given the user can authenticate via device code' do
19
+ let(:params) { {scopes: 'youtube', user_code: 'abcdefgh', verification_url: 'http://google.com/device'} }
20
+ let(:msg) { %r{^Please authenticate your app by visiting the page} }
21
+ it { expect{error}.to raise_error msg }
22
+ end
9
23
  end
10
24
  end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+ require 'yt/errors/unauthorized'
3
+
4
+ describe Yt::Errors::Unauthorized do
5
+ let(:msg) { %r{^A request to YouTube API was sent without a valid authentication} }
6
+
7
+ describe '#exception' do
8
+ it { expect{raise Yt::Errors::Unauthorized}.to raise_error msg }
9
+ end
10
+ end
@@ -65,7 +65,12 @@ describe Yt::Account, :device_app do
65
65
  context 'that has expired' do
66
66
  let(:expires_at) { 1.day.ago.to_s }
67
67
 
68
- context 'and no valid refresh token' do
68
+ context 'and no refresh token' do
69
+ it { expect{account.authentication}.to raise_error Yt::Errors::MissingAuth }
70
+ end
71
+
72
+ context 'and an invalid refresh token' do
73
+ before { attrs[:refresh_token] = '--not-a-valid-refresh-token--' }
69
74
  it { expect{account.authentication}.to raise_error Yt::Errors::Unauthorized }
70
75
  end
71
76
 
@@ -80,7 +85,7 @@ describe Yt::Account, :device_app do
80
85
  let(:access_token) { '--not-a-valid-access-token--' }
81
86
  let(:expires_at) { 1.day.from_now }
82
87
 
83
- context 'and no valid refresh token' do
88
+ context 'and no refresh token' do
84
89
  it { expect{account.channel}.to raise_error Yt::Errors::Unauthorized }
85
90
  end
86
91
 
@@ -91,9 +96,28 @@ describe Yt::Account, :device_app do
91
96
  end
92
97
  end
93
98
 
99
+ context 'given scopes' do
100
+ let(:attrs) { {scopes: ['userinfo.email', 'youtube']} }
101
+
102
+ context 'and a redirect_uri' do
103
+ before { attrs[:redirect_uri] = 'http://localhost/' }
104
+
105
+ it { expect{account.authentication}.to raise_error Yt::Errors::MissingAuth }
106
+ end
107
+
108
+ context 'and no device token' do
109
+ it { expect{account.authentication}.to raise_error Yt::Errors::MissingAuth }
110
+ end
111
+
112
+ context 'and an invalid device code' do
113
+ before { attrs[:device_code] = '--not-a-valid-device-code--' }
114
+ it { expect{account.authentication}.to raise_error Yt::Errors::MissingAuth }
115
+ end
116
+ end
117
+
94
118
  context 'given no token or code' do
95
119
  let(:attrs) { {} }
96
- it { expect{account.authentication}.to raise_error Yt::Errors::Unauthorized }
120
+ it { expect{account.authentication}.to raise_error Yt::Errors::MissingAuth }
97
121
  end
98
122
  end
99
123
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.6
4
+ version: 0.7.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-27 00:00:00.000000000 Z
11
+ date: 2014-06-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -122,11 +122,16 @@ files:
122
122
  - lib/yt/actions/insert.rb
123
123
  - lib/yt/actions/list.rb
124
124
  - lib/yt/actions/update.rb
125
+ - lib/yt/associations/has_authentication.rb
126
+ - lib/yt/associations/has_many.rb
127
+ - lib/yt/associations/has_one.rb
128
+ - lib/yt/associations/has_reports.rb
125
129
  - lib/yt/collections/annotations.rb
126
130
  - lib/yt/collections/authentications.rb
127
131
  - lib/yt/collections/base.rb
128
132
  - lib/yt/collections/channels.rb
129
133
  - lib/yt/collections/content_details.rb
134
+ - lib/yt/collections/device_flows.rb
130
135
  - lib/yt/collections/ids.rb
131
136
  - lib/yt/collections/partnered_channels.rb
132
137
  - lib/yt/collections/playlist_items.rb
@@ -141,6 +146,7 @@ files:
141
146
  - lib/yt/collections/videos.rb
142
147
  - lib/yt/config.rb
143
148
  - lib/yt/errors/forbidden.rb
149
+ - lib/yt/errors/missing_auth.rb
144
150
  - lib/yt/errors/no_items.rb
145
151
  - lib/yt/errors/request_error.rb
146
152
  - lib/yt/errors/server_error.rb
@@ -154,6 +160,7 @@ files:
154
160
  - lib/yt/models/content_detail.rb
155
161
  - lib/yt/models/content_owner.rb
156
162
  - lib/yt/models/description.rb
163
+ - lib/yt/models/device_flow.rb
157
164
  - lib/yt/models/id.rb
158
165
  - lib/yt/models/playlist.rb
159
166
  - lib/yt/models/playlist_item.rb
@@ -167,9 +174,6 @@ files:
167
174
  - lib/yt/models/url.rb
168
175
  - lib/yt/models/user_info.rb
169
176
  - lib/yt/models/video.rb
170
- - lib/yt/modules/associations.rb
171
- - lib/yt/modules/authentication.rb
172
- - lib/yt/modules/reports.rb
173
177
  - lib/yt/version.rb
174
178
  - spec/collections/annotations_spec.rb
175
179
  - spec/collections/channels_spec.rb
@@ -184,6 +188,7 @@ files:
184
188
  - spec/errors/missing_auth_spec.rb
185
189
  - spec/errors/no_items_spec.rb
186
190
  - spec/errors/request_error_spec.rb
191
+ - spec/errors/unauthorized_spec.rb
187
192
  - spec/models/account_spec.rb
188
193
  - spec/models/annotation_spec.rb
189
194
  - spec/models/channel_spec.rb
@@ -261,6 +266,7 @@ test_files:
261
266
  - spec/errors/missing_auth_spec.rb
262
267
  - spec/errors/no_items_spec.rb
263
268
  - spec/errors/request_error_spec.rb
269
+ - spec/errors/unauthorized_spec.rb
264
270
  - spec/models/account_spec.rb
265
271
  - spec/models/annotation_spec.rb
266
272
  - spec/models/channel_spec.rb