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 +4 -4
- data/.yardopts +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/bin/yt +82 -19
- data/lib/yt/{modules/authentication.rb → associations/has_authentication.rb} +88 -17
- data/lib/yt/{modules/associations.rb → associations/has_many.rb} +5 -16
- data/lib/yt/associations/has_one.rb +21 -0
- data/lib/yt/{modules/reports.rb → associations/has_reports.rb} +6 -5
- data/lib/yt/collections/device_flows.rb +32 -0
- data/lib/yt/errors/missing_auth.rb +81 -0
- data/lib/yt/errors/request_error.rb +1 -1
- data/lib/yt/models/account.rb +2 -4
- data/lib/yt/models/authentication.rb +6 -0
- data/lib/yt/models/base.rb +11 -3
- data/lib/yt/models/channel.rb +0 -4
- data/lib/yt/models/device_flow.rb +23 -0
- data/lib/yt/models/request.rb +5 -2
- data/lib/yt/models/video.rb +0 -4
- data/lib/yt/version.rb +1 -1
- data/spec/errors/missing_auth_spec.rb +17 -3
- data/spec/errors/unauthorized_spec.rb +10 -0
- data/spec/requests/as_account/authentications_spec.rb +27 -3
- metadata +11 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f3bc792631d1995834e498558253d038a781341
|
4
|
+
data.tar.gz: c01d949dd3e7b77368c37f62bf3dcd9d90224be8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23993ad7e807201c61d8b129f52e4a8a73ddb9546435b3b78dd06926b96a31646b167a936919f8f5d8354c698dc62ee8c3447ea83ef4e1ecd47b0584156e154a
|
7
|
+
data.tar.gz: ce5c7b8c9608b3d263311abcb176055c6ff9015d4869d487f3505d6e6a55eaa8e2b9459f81d777b80014c2cfabae5c83f019a84d0431490513dc170424a0b218
|
data/.yardopts
CHANGED
data/Gemfile.lock
CHANGED
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.
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
puts
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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 "
|
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
|
8
|
-
# Provides authentication methods to YouTube resources, which
|
9
|
-
#
|
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
|
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 ||=
|
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,
|
65
|
-
|
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
|
-
|
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
|
74
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
data/lib/yt/models/account.rb
CHANGED
@@ -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 = {})
|
data/lib/yt/models/base.rb
CHANGED
@@ -1,15 +1,23 @@
|
|
1
1
|
require 'yt/actions/delete'
|
2
2
|
require 'yt/actions/update'
|
3
|
-
|
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
|
|
data/lib/yt/models/channel.rb
CHANGED
@@ -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
|
data/lib/yt/models/request.rb
CHANGED
@@ -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
|
data/lib/yt/models/video.rb
CHANGED
@@ -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,10 +1,24 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'yt/errors/
|
2
|
+
require 'yt/errors/missing_auth'
|
3
3
|
|
4
|
-
describe Yt::Errors::
|
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{
|
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
|
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
|
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::
|
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.
|
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-
|
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
|