yt 0.7.6 → 0.7.7

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