yt 0.4.10 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/HISTORY.md +10 -0
  4. data/README.md +7 -3
  5. data/TODO.md +6 -2
  6. data/lib/yt/actions/delete.rb +1 -1
  7. data/lib/yt/actions/delete_all.rb +1 -1
  8. data/lib/yt/actions/insert.rb +1 -1
  9. data/lib/yt/actions/list.rb +8 -2
  10. data/lib/yt/actions/update.rb +1 -1
  11. data/lib/yt/associations.rb +1 -0
  12. data/lib/yt/associations/details_sets.rb +1 -1
  13. data/lib/yt/associations/ids.rb +20 -0
  14. data/lib/yt/associations/snippets.rb +1 -1
  15. data/lib/yt/associations/subscriptions.rb +2 -2
  16. data/lib/yt/associations/user_infos.rb +1 -1
  17. data/lib/yt/collections/base.rb +1 -0
  18. data/lib/yt/collections/details_sets.rb +0 -1
  19. data/lib/yt/collections/ids.rb +22 -0
  20. data/lib/yt/collections/playlist_items.rb +3 -3
  21. data/lib/yt/collections/playlists.rb +0 -1
  22. data/lib/yt/collections/snippets.rb +0 -1
  23. data/lib/yt/collections/subscriptions.rb +4 -3
  24. data/lib/yt/collections/user_infos.rb +0 -2
  25. data/lib/yt/collections/videos.rb +0 -1
  26. data/lib/yt/errors/base.rb +43 -0
  27. data/lib/yt/errors/error.rb +8 -0
  28. data/lib/yt/errors/failed.rb +17 -0
  29. data/lib/yt/errors/no_items.rb +17 -0
  30. data/lib/yt/errors/unauthenticated.rb +34 -0
  31. data/lib/yt/models/account.rb +1 -17
  32. data/lib/yt/models/base.rb +1 -0
  33. data/lib/yt/models/description.rb +11 -35
  34. data/lib/yt/models/id.rb +4 -0
  35. data/lib/yt/models/request.rb +102 -0
  36. data/lib/yt/models/resource.rb +13 -2
  37. data/lib/yt/models/subscription.rb +3 -2
  38. data/lib/yt/models/url.rb +88 -0
  39. data/lib/yt/version.rb +1 -1
  40. data/spec/associations/device_auth/details_sets_spec.rb +1 -1
  41. data/spec/associations/device_auth/ids_spec.rb +19 -0
  42. data/spec/associations/device_auth/playlist_items_spec.rb +3 -3
  43. data/spec/associations/device_auth/snippets_spec.rb +2 -2
  44. data/spec/associations/device_auth/user_infos_spec.rb +7 -2
  45. data/spec/associations/server_auth/details_sets_spec.rb +4 -4
  46. data/spec/associations/server_auth/ids_spec.rb +18 -0
  47. data/spec/associations/server_auth/snippets_spec.rb +2 -2
  48. data/spec/collections/playlist_items_spec.rb +25 -5
  49. data/spec/collections/subscriptions_spec.rb +6 -4
  50. data/spec/errors/failed_spec.rb +9 -0
  51. data/spec/errors/no_items_spec.rb +9 -0
  52. data/spec/errors/unauthenticated_spec.rb +9 -0
  53. data/spec/models/account_spec.rb +0 -0
  54. data/spec/models/description_spec.rb +2 -2
  55. data/spec/models/request_spec.rb +29 -0
  56. data/spec/models/resource_spec.rb +21 -0
  57. data/spec/models/subscription_spec.rb +6 -4
  58. data/spec/models/url_spec.rb +72 -0
  59. data/spec/support/fail_matcher.rb +14 -0
  60. metadata +32 -4
  61. data/lib/yt/actions/request.rb +0 -112
  62. data/lib/yt/actions/request_error.rb +0 -11
@@ -10,28 +10,12 @@ module Yt
10
10
  :name, :given_name, :family_name, :profile_url, :avatar_url, :locale, :hd]
11
11
 
12
12
  def initialize(options = {})
13
- # By default is someone passes a refresh_token but not a scope, we can assume it's a youtube one
14
- @scope = options.fetch :scope, 'https://www.googleapis.com/auth/youtube'
15
13
  @access_token = options[:access_token]
16
14
  @refresh_token = options[:refresh_token]
17
15
  @redirect_url = options[:redirect_url]
18
16
  end
19
17
 
20
- def access_token_for(scope)
21
- # TODO incremental scope
22
-
23
- # HERE manage the fact that we must change some scope on device,
24
- # like 'https://www.googleapis.com/auth/youtube.readonly' is not accepted
25
- if Yt.configuration.scenario == :device_app && scope == 'https://www.googleapis.com/auth/youtube.readonly'
26
- scope = 'https://www.googleapis.com/auth/youtube'
27
- end
28
-
29
- # TODO !! include? is not enough, because (for instance) 'youtube' also includes 'youtube.readonly'
30
-
31
- # unless (@scope == scope) || (scope == 'https://www.googleapis.com/auth/youtube.readonly' && @scope =='https://www.googleapis.com/auth/youtube')
32
- # @scope = scope
33
- # @access_token = @refresh_token = nil
34
- # end
18
+ def access_token
35
19
  @access_token ||= refresh_access_token || get_access_token
36
20
  end
37
21
 
@@ -1,6 +1,7 @@
1
1
  require 'yt/associations'
2
2
  require 'yt/actions/delete'
3
3
  require 'yt/actions/update'
4
+ require 'yt/errors/error'
4
5
 
5
6
  module Yt
6
7
  class Base
@@ -1,3 +1,5 @@
1
+ require 'yt/models/url'
2
+
1
3
  module Yt
2
4
  # Provides read-only access to the description of a YouTube resource.
3
5
  # Resources with descriptions are: videos and channels.
@@ -20,79 +22,53 @@ module Yt
20
22
  def has_link_to_video?
21
23
  # TODO: might take as an option WHICH video to link to
22
24
  # in order to check if it's my own video
23
- regex? :video_long_url, :video_short_url
25
+ links.any?{|link| link.kind == :video}
24
26
  end
25
27
 
26
28
  # Returns whether the description includes a YouTube channel URL
27
29
  #
28
30
  # @example
29
31
  #
30
- # description = Yt::Description.new Link to channel: youtube.com/fullscreen
32
+ # description = Yt::Description.new 'Link to channel: youtube.com/fullscreen'
31
33
  # description.has_link_to_channel? #=> true
32
34
  #
33
35
  # @return [Boolean] Whether the description includes a link to a channel
34
36
  def has_link_to_channel?(options = {}) # TODO: which channel
35
37
  # TODO: might take as an option WHICH channel to link to
36
38
  # in order to check if it's my own channel
37
- regex? :channel_long_url, :channel_short_url, :channel_user_url
39
+ links.any?{|link| link.kind == :channel}
38
40
  end
39
41
 
40
42
  # Returns whether the description includes a YouTube subscription URL
41
43
  #
42
44
  # @example
43
45
  #
44
- # description = Yt::Description.new Link to subscribe: youtube.com/subscription_center?add_user=fullscreen
46
+ # description = Yt::Description.new 'Link to subscribe: youtube.com/subscription_center?add_user=fullscreen'
45
47
  # description.has_link_to_subscribe? #=> true
46
48
  #
47
49
  # @return [Boolean] Whether the description includes a link to subscribe
48
50
  def has_link_to_subscribe?(options = {}) # TODO: which channel
49
51
  # TODO: might take as an option WHICH channel to subscribe to
50
52
  # in order to check if it's my own channel
51
- regex? :subscribe_center_url, :subscribe_widget_url, :subscribe_confirm_url
53
+ links.any?{|link| link.kind == :subscription}
52
54
  end
53
55
 
54
56
  # Returns whether the description includes a YouTube playlist URL
55
57
  #
56
58
  # @example
57
59
  #
58
- # description = Yt::Description.new Link to playlist: youtube.com/playlist?list=LLxO1tY8h1AhOz0T4ENwmpow
60
+ # description = Yt::Description.new 'Link to playlist: youtube.com/playlist?list=LLxO1tY8h1AhOz0T4ENwmpow'
59
61
  # description.has_link_to_playlist? #=> true
60
62
  #
61
63
  # @return [Boolean] Whether the description includes a link to a playlist
62
64
  def has_link_to_playlist?
63
- regex? :playlist_long_url, :playlist_embed_url
65
+ links.any?{|link| link.kind == :playlist}
64
66
  end
65
67
 
66
68
  private
67
69
 
68
- def regex?(*keys)
69
- keys.find{|key| self =~ regex_for(key)}
70
- end
71
-
72
- def regex_for(key)
73
- host, name = '(?:https?://)?(?:www\.)?', '([a-zA-Z0-9_-]+)'
74
- case key
75
- when :video_long_url
76
- %r{#{host}youtube\.com/watch\?v=#{name}}
77
- when :video_short_url
78
- %r{#{host}youtu\.be/#{name}}
79
- when :channel_long_url
80
- %r{#{host}youtube\.com/channel/#{name}}
81
- when :channel_short_url
82
- %r{#{host}youtube\.com/#{name}}
83
- when :channel_user_url
84
- %r{#{host}youtube\.com/user/#{name}}
85
- when :subscribe_center_url
86
- %r{#{host}youtube\.com/subscription_center\?add_user=#{name}}
87
- when :subscribe_widget_url
88
- %r{#{host}youtube\.com/subscribe_widget\?p=#{name}}
89
- when :subscribe_confirm_url
90
- %r{#{host}youtube\.com/channel/(?:[a-zA-Z0-9&_=-]*)\?sub_confirmation=1}
91
- when :playlist_long_url
92
- %r{#{host}youtube\.com/playlist\?list=#{name}}
93
- when :playlist_embed_url
94
- %r{#{host}youtube\.com/watch\?v=(?:[a-zA-Z0-9&_=-]*)&list=#{name}}
95
- end
70
+ def links
71
+ @links ||= self.split(' ').map{|word| URL.new word}
96
72
  end
97
73
  end
98
74
  end
@@ -0,0 +1,4 @@
1
+ module Yt
2
+ class Id < String
3
+ end
4
+ end
@@ -0,0 +1,102 @@
1
+ require 'net/http' # for Net::HTTP.start
2
+ require 'uri' # for URI.json
3
+ require 'json' # for JSON.parse
4
+ require 'active_support/core_ext' # for Hash.from_xml, Hash.to_param
5
+
6
+ require 'yt/config'
7
+ require 'yt/errors/failed'
8
+ require 'yt/errors/unauthenticated'
9
+
10
+ module Yt
11
+ class Request
12
+ def initialize(options = {})
13
+ options[:query] ||= options[:params].to_param
14
+ @uri = URI::HTTPS.build options.slice(:host, :path, :query)
15
+ @method = options.fetch :method, :get
16
+ @format = options[:format]
17
+ @body = options[:body]
18
+ @body_type = options[:body_type]
19
+ @auth = options[:auth]
20
+ @headers = {}
21
+ end
22
+
23
+ def run
24
+ add_authorization_to_request! if requires_authorization?
25
+ fetch_response.tap do |response|
26
+ if response.is_a? Net::HTTPSuccess
27
+ response.body = parse_format response.body
28
+ else
29
+ raise Errors::Failed, to_error(response)
30
+ end
31
+ end
32
+ end
33
+
34
+ def self.default_params
35
+ {}.tap do |params|
36
+ params[:format] = :json
37
+ params[:host] = 'www.googleapis.com'
38
+ params[:body_type] = :json
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def add_authorization_to_request!
45
+ if @auth.respond_to? :access_token
46
+ @headers['Authorization'] = "Bearer #{@auth.access_token}"
47
+ elsif Yt.configuration.api_key
48
+ params = URI.decode_www_form @uri.query || ''
49
+ params << [:key, Yt.configuration.api_key]
50
+ @uri.query = URI.encode_www_form params
51
+ else
52
+ raise Errors::Unauthenticated, to_error
53
+ end
54
+ end
55
+
56
+ def requires_authorization?
57
+ @uri.host == Request.default_params[:host]
58
+ end
59
+
60
+ def fetch_response
61
+ klass = "Net::HTTP::#{@method.capitalize}".constantize
62
+ request = klass.new @uri.request_uri
63
+ case @body_type
64
+ when :json
65
+ request.initialize_http_header 'Content-Type' => 'application/json'
66
+ request.initialize_http_header 'Content-length' => '0' unless @body
67
+ request.body = @body.to_json if @body
68
+ when :form
69
+ request.set_form_data @body if @body
70
+ end
71
+ @headers.each{|k,v| request.add_field k, v}
72
+
73
+ Net::HTTP.start(@uri.host, @uri.port, use_ssl: true) do |http|
74
+ http.request request
75
+ end
76
+ end
77
+
78
+ def parse_format(body)
79
+ case @format
80
+ when :xml then Hash.from_xml body
81
+ when :json then JSON body
82
+ end if body
83
+ end
84
+
85
+ def to_error(response = nil)
86
+ request_msg = {}.tap do |msg|
87
+ msg[:method] = @method
88
+ msg[:headers] = @headers
89
+ msg[:url] = @uri.to_s
90
+ msg[:body] = @body
91
+ end
92
+
93
+ response_msg = {}.tap do |msg|
94
+ msg[:code] = response.code
95
+ msg[:headers] = {}.tap{|h| response.each_header{|k,v| h[k] = v }}
96
+ msg[:body] = response.body
97
+ end if response
98
+
99
+ {request: request_msg, response: response_msg}.to_json
100
+ end
101
+ end
102
+ end
@@ -1,16 +1,27 @@
1
1
  require 'yt/models/base'
2
+ require 'yt/models/url'
2
3
 
3
4
  module Yt
4
5
  class Resource < Base
5
- attr_reader :id, :auth
6
+ attr_reader :auth
7
+ has_one :id
6
8
  has_one :snippet, delegate: [:title, :description, :thumbnail_url, :published_at, :tags]
7
9
  has_one :status, delegate: [:privacy_status, :public?, :private?, :unlisted?]
8
10
 
9
11
  def initialize(options = {})
10
- @id = options[:id]
12
+ @url = URL.new(options[:url]) if options[:url]
13
+ @id = options[:id] || (@url.id if @url)
11
14
  @auth = options[:auth]
12
15
  @snippet = Snippet.new(data: options[:snippet]) if options[:snippet]
13
16
  @status = Status.new(data: options[:status]) if options[:status]
14
17
  end
18
+
19
+ def kind
20
+ @url ? @url.kind.to_s : self.class.to_s.demodulize.underscore
21
+ end
22
+
23
+ def username
24
+ @url.username if @url
25
+ end
15
26
  end
16
27
  end
@@ -13,8 +13,9 @@ module Yt
13
13
  def delete(options = {})
14
14
  begin
15
15
  do_delete {@id = nil}
16
- rescue Yt::RequestError => error
17
- raise error unless options[:ignore_not_found] && error.reasons.include?('subscriptionNotFound')
16
+ rescue Errors::Base => error
17
+ ignorable_errors = error.reasons & ['subscriptionNotFound']
18
+ raise error unless options[:ignore_errors] && ignorable_errors.any?
18
19
  end
19
20
  !exists?
20
21
  end
@@ -0,0 +1,88 @@
1
+ module Yt
2
+ class URL
3
+ attr_reader :kind
4
+
5
+ def initialize(url)
6
+ @url = url
7
+ @kind ||= parse url
8
+ @match_data ||= {}
9
+ end
10
+
11
+ def id
12
+ @match_data[:id]
13
+ rescue IndexError
14
+ end
15
+
16
+ def username
17
+ @match_data[:username]
18
+ rescue IndexError
19
+ end
20
+
21
+ private
22
+
23
+ def parse(url)
24
+ matching_pattern = patterns.find do |pattern|
25
+ @match_data = url.match pattern[:regex]
26
+ end
27
+ matching_pattern[:kind] if matching_pattern
28
+ end
29
+
30
+ def patterns
31
+ # @note: :video *must* be the last since one of its regex eats the
32
+ # remaining patterns. In short, don't change the following order
33
+
34
+ @patterns ||= patterns_for :playlist, :subscription, :channel, :video
35
+ end
36
+
37
+ def patterns_for(*kinds)
38
+ prefix = '^(?:https?://)?(?:www\.)?'
39
+ suffix = '(?:|/)$'
40
+ kinds.map do |kind|
41
+ patterns = send "#{kind}_patterns" # meta programming :/
42
+ patterns.map do |pattern|
43
+ {kind: kind, regex: %r{#{prefix}#{pattern}#{suffix}}}
44
+ end
45
+ end.flatten
46
+ end
47
+
48
+ def subscription_patterns
49
+ name = '(?:[a-zA-Z0-9&_=-]*)'
50
+
51
+ %W{
52
+ subscription_center\\?add_user=#{name}
53
+ subscribe_widget\\?p=#{name}
54
+ channel/#{name}\\?sub_confirmation=1
55
+ }.map{|path| "youtube\\.com/#{path}"}
56
+ end
57
+
58
+ def playlist_patterns
59
+ playlist_id = '(?<id>[a-zA-Z0-9_-]+)'
60
+ video_id = '(?:[a-zA-Z0-9&_=-]*)'
61
+
62
+ %W{
63
+ playlist\\?list=#{playlist_id}
64
+ watch\\?v=#{video_id}&list=#{playlist_id}
65
+ }.map{|path| "youtube\\.com/#{path}"}
66
+ end
67
+
68
+ def video_patterns
69
+ video_id = '(?<id>[a-zA-Z0-9_-]+)'
70
+
71
+ %W{
72
+ youtube\\.com/watch\\?v=#{video_id}
73
+ youtu\\.be/#{video_id}
74
+ }
75
+ end
76
+
77
+ def channel_patterns
78
+ channel_id = '(?<id>[a-zA-Z0-9_-]+)'
79
+ username = '(?<username>[a-zA-Z0-9_-]+)'
80
+
81
+ %W{
82
+ channel/#{channel_id}
83
+ user/#{username}
84
+ #{username}
85
+ }.map{|path| "youtube\\.com/#{path}"}
86
+ end
87
+ end
88
+ end
@@ -1,3 +1,3 @@
1
1
  module Yt
2
- VERSION = '0.4.10'
2
+ VERSION = '0.5.3'
3
3
  end
@@ -13,7 +13,7 @@ describe Yt::Associations::DetailsSets, scenario: :device_app do
13
13
 
14
14
  context 'given an unknown video' do
15
15
  let(:video) { Yt::Video.new id: 'not-a-video-id', auth: account }
16
- it { expect(video.details_set).to be_nil }
16
+ it { expect{video.details_set}.to raise_error Yt::Errors::NoItems }
17
17
  end
18
18
  end
19
19
  end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+ require 'yt/associations/ids'
3
+
4
+ describe Yt::Associations::Ids, scenario: :device_app do
5
+ let(:account) { Yt.configuration.account }
6
+ subject(:resource) { Yt::Resource.new url: url, auth: account.auth }
7
+
8
+ describe '#id' do
9
+ context 'given a URL containing an existing username' do
10
+ let(:url) { 'youtube.com/fullscreen' }
11
+ it { expect(resource.id).to eq 'UCxO1tY8h1AhOz0T4ENwmpow' }
12
+ end
13
+
14
+ context 'given a URL containing an unknown username' do
15
+ let(:url) { 'youtube.com/--not--a--valid--username' }
16
+ it { expect{resource.id}.to raise_error Yt::Errors::NoItems }
17
+ end
18
+ end
19
+ end
@@ -46,12 +46,12 @@ describe Yt::Associations::PlaylistItems, scenario: :device_app do
46
46
 
47
47
  context 'given an unknown video' do
48
48
  let(:video_id) { 'not-a-video' }
49
- it { expect{@playlist.add_video! video_id}.to raise_error Yt::RequestError }
49
+ it { expect{@playlist.add_video! video_id}.to fail.with 'videoNotFound' }
50
50
  end
51
51
 
52
52
  context 'given a video of a terminated account' do
53
53
  let(:video_id) { 'kDCpdKeTe5g' }
54
- it { expect{@playlist.add_video! video_id}.to raise_error Yt::RequestError }
54
+ it { expect{@playlist.add_video! video_id}.to fail.with 'forbidden' }
55
55
  end
56
56
  end
57
57
 
@@ -66,7 +66,7 @@ describe Yt::Associations::PlaylistItems, scenario: :device_app do
66
66
  describe '#add_videos!' do
67
67
  context 'given one existing and one unknown video' do
68
68
  let(:video_ids) { ['MESycYJytkU', 'not-a-video'] }
69
- it { expect{@playlist.add_videos! video_ids}.to raise_error Yt::RequestError }
69
+ it { expect{@playlist.add_videos! video_ids}.to fail.with 'videoNotFound'}
70
70
  end
71
71
  end
72
72
 
@@ -14,7 +14,7 @@ describe Yt::Associations::Snippets, scenario: :device_app do
14
14
 
15
15
  context 'given an unknown video resource' do
16
16
  let(:video) { Yt::Video.new id: 'not-a-video-id', auth: account }
17
- it { expect(video.snippet).to be_nil }
17
+ it { expect{video.snippet}.to raise_error Yt::Errors::NoItems }
18
18
  end
19
19
 
20
20
  context 'given an existing channel resource' do
@@ -24,7 +24,7 @@ describe Yt::Associations::Snippets, scenario: :device_app do
24
24
 
25
25
  context 'given an unknown channel resource' do
26
26
  let(:channel) { Yt::Channel.new id: 'not-a-channel-id', auth: account }
27
- it { expect(channel.snippet).to be_nil }
27
+ it { expect{channel.snippet}.to raise_error Yt::Errors::NoItems }
28
28
  end
29
29
  end
30
30
  end