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