yt 0.6.2 → 0.6.3

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: c4e025834f30276ff685e3ff61e3bd1330fc42b4
4
- data.tar.gz: 725a5254897a04ff2ff0694125b58101dd6689aa
3
+ metadata.gz: 0608745d313bbcec172603a1854abfffdee3f074
4
+ data.tar.gz: 1f53091e0e0810f74c00a63f82584404fb640044
5
5
  SHA512:
6
- metadata.gz: 582c36cde9977d2d0b7585fea15950ee1f09292ed373e39a9b99f53ae8ce32a8bdb198106d8afe00d78c60f28890eab1d9ec81f2ba5d6e9495fbc9f60c35943d
7
- data.tar.gz: 3e9bb4cda3a7fec3d604c92d3c8d609b7463a20a8e833cb01ff8b8c09e40b0bf05e106d35bfc68d76a7655e6b3095e09e650244c1293555e45ae54de2175deba
6
+ metadata.gz: 3d79894c6d8bb6ce42e1e0ab7ee49c05f723a68eb41c2e1f32f5278979fb7e73d902af587648fb76b8cc5d18012d37220e04f84b1d463f714399a80cf46ce509
7
+ data.tar.gz: c393cf4018880ec464394d6c55087b8a65a99bcbf0466da48574c37927dc4232441851bf5b8f18033d361159e022dc6f584dbd68022b53849894ecd8849bcd09
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- yt (0.6.2)
4
+ yt (0.6.3)
5
5
  activesupport
6
6
 
7
7
  GEM
@@ -23,10 +23,10 @@ GEM
23
23
  docile (1.1.3)
24
24
  i18n (0.6.9)
25
25
  json (1.8.1)
26
- mime-types (2.2)
27
- minitest (5.3.3)
28
- multi_json (1.10.0)
29
- rake (10.3.1)
26
+ mime-types (2.3)
27
+ minitest (5.3.4)
28
+ multi_json (1.10.1)
29
+ rake (10.3.2)
30
30
  rest-client (1.6.7)
31
31
  mime-types (>= 1.16)
32
32
  rspec (3.0.0)
@@ -38,7 +38,7 @@ GEM
38
38
  rspec-expectations (3.0.0)
39
39
  diff-lcs (>= 1.2.0, < 2.0)
40
40
  rspec-support (~> 3.0.0)
41
- rspec-mocks (3.0.0)
41
+ rspec-mocks (3.0.1)
42
42
  rspec-support (~> 3.0.0)
43
43
  rspec-support (3.0.0)
44
44
  simplecov (0.8.2)
@@ -49,9 +49,9 @@ GEM
49
49
  term-ansicolor (1.3.0)
50
50
  tins (~> 1.0)
51
51
  thor (0.19.1)
52
- thread_safe (0.3.3)
53
- tins (1.2.0)
54
- tzinfo (1.1.0)
52
+ thread_safe (0.3.4)
53
+ tins (1.3.0)
54
+ tzinfo (1.2.1)
55
55
  thread_safe (~> 0.1)
56
56
  yard (0.8.7.4)
57
57
 
data/HISTORY.md CHANGED
@@ -5,6 +5,7 @@ v0.6 - 2014/06/05
5
5
  * [breaking change] Account#videos shows *all* videos owned by account (public and private)
6
6
  * Add the .status association to *every* type of resource (Channel, Video, Playlist)
7
7
  * Allow account.videos to be chained with .where, such as in account.videos.where(q: 'query')
8
+ * Retry request once when YouTube times out
8
9
 
9
10
  v0.5 - 2014/05/16
10
11
  -----------------
data/README.md CHANGED
@@ -1,14 +1,16 @@
1
- Yt
2
- ==
1
+ Yt - a Ruby client for the YouTube API
2
+ ======================================================
3
3
 
4
- Yt helps you write apps that need to interact with the YouTube API V3.
4
+ Yt helps you write apps that need to interact with YouTube.
5
5
 
6
- [![Gem Version](https://badge.fury.io/rb/yt.svg)](http://badge.fury.io/rb/yt)
7
- [![Dependency Status](https://gemnasium.com/Fullscreen/yt.png)](https://gemnasium.com/Fullscreen/yt)
8
- [![Build Status](https://travis-ci.org/Fullscreen/yt.png?branch=master)](https://travis-ci.org/Fullscreen/yt)
9
- [![Coverage Status](https://coveralls.io/repos/Fullscreen/yt/badge.png?)](https://coveralls.io/r/Fullscreen/yt)
10
- [![Code Climate](https://codeclimate.com/github/Fullscreen/yt.png)](https://codeclimate.com/github/Fullscreen/yt)
11
- [![Inline docs](http://inch-pages.github.io/github/Fullscreen/yt.png)](http://inch-pages.github.io/github/Fullscreen/yt)
6
+ The **full documentation** is available at [rubydoc.info](http://rubydoc.info/github/Fullscreen/yt/master/frames).
7
+
8
+ [![Build Status](http://img.shields.io/travis/Fullscreen/yt/master.svg)](https://travis-ci.org/Fullscreen/yt)
9
+ [![Coverage Status](http://img.shields.io/coveralls/Fullscreen/yt/master.svg)](https://coveralls.io/r/Fullscreen/yt)
10
+ [![Dependency Status](http://img.shields.io/gemnasium/Fullscreen/yt.svg)](https://gemnasium.com/Fullscreen/yt)
11
+ [![Code Climate](http://img.shields.io/codeclimate/github/Fullscreen/yt.svg)](https://codeclimate.com/github/Fullscreen/yt)
12
+ [![Online docs](http://img.shields.io/badge/docs-✓-green.svg)](http://rubydoc.info/github/Fullscreen/yt/master/frames)
13
+ [![Gem Version](http://img.shields.io/gem/v/yt.svg)](http://rubygems.org/gems/yt)
12
14
 
13
15
  After [registering your app](#configuring-your-app), you can run commands like:
14
16
 
@@ -330,7 +332,7 @@ To install on your system, run
330
332
 
331
333
  To use inside a bundled Ruby project, add this line to the Gemfile:
332
334
 
333
- gem 'yt', '~> 0.6.2'
335
+ gem 'yt', '~> 0.6.3'
334
336
 
335
337
  Since the gem follows [Semantic Versioning](http://semver.org),
336
338
  indicating the full version in your Gemfile (~> *major*.*minor*.*patch*)
@@ -3,14 +3,21 @@ require 'yt/models/annotation'
3
3
 
4
4
  module Yt
5
5
  module Collections
6
+ # Provides methods to interact with a collection of YouTube annotations.
7
+ # Resources with annotations are: {Yt::Models::Video videos}.
6
8
  class Annotations < Base
7
9
 
8
10
  private
9
11
 
12
+ # @return [Yt::Models::Annotation] a new annotation initialized with one
13
+ # of the items returned by asking YouTube for a list of annotations.
10
14
  def new_item(data)
11
15
  Yt::Annotation.new data: data
12
16
  end
13
17
 
18
+ # @return [Hash] the parameters to submit to YouTube to list annotations.
19
+ # @note YouTube does not provide an API endpoint to get annotations for
20
+ # a video, so we use an "old-style" URL that YouTube still maintains.
14
21
  def list_params
15
22
  super.tap do |params|
16
23
  params[:format] = :xml
@@ -21,6 +28,9 @@ module Yt
21
28
  end
22
29
  end
23
30
 
31
+ # @private
32
+ # @note Annotations overwrites +next_page+ since the list of annotations
33
+ # is not paginated API-style, but in its own custom way.
24
34
  def next_page
25
35
  request = Yt::Request.new list_params
26
36
  response = request.run
@@ -3,14 +3,21 @@ require 'yt/models/channel'
3
3
 
4
4
  module Yt
5
5
  module Collections
6
+ # Provides methods to interact with a collection of YouTube channels.
7
+ # Resources with channels are: {Yt::Models::Account accounts}.
6
8
  class Channels < Base
7
9
 
8
10
  private
9
11
 
12
+ # @return [Yt::Models::Channel] a new channel initialized with one of
13
+ # the items returned by asking YouTube for a list of channels.
14
+ # @see https://developers.google.com/youtube/v3/docs/channels#resource
10
15
  def new_item(data)
11
16
  Yt::Channel.new id: data['id'], snippet: data['snippet'], auth: @auth
12
17
  end
13
18
 
19
+ # @return [Hash] the parameters to submit to YouTube to list channels.
20
+ # @see https://developers.google.com/youtube/v3/docs/channels/list
14
21
  def list_params
15
22
  super.tap do |params|
16
23
  params[:params] = {maxResults: 50, part: 'snippet', mine: true}
@@ -1,37 +1,13 @@
1
- require 'yt/collections/base'
1
+ require 'yt/collections/reports'
2
2
 
3
3
  module Yt
4
4
  module Collections
5
- class Earnings < Base
6
-
7
- def within(days_range)
8
- @days_range = days_range
9
- Hash[*flat_map{|daily_earning| daily_earning}]
10
- end
5
+ class Earnings < Reports
11
6
 
12
7
  private
13
8
 
14
- def new_item(data)
15
- # NOTE: could use column headers to be more precise
16
- [Date.iso8601(data.first), data.last]
17
- end
18
-
19
- def list_params
20
- super.tap do |params|
21
- params[:path] = '/youtube/analytics/v1/reports'
22
- params[:params] = {}.tap do |params|
23
- params['ids'] = "contentOwner==#{@auth.owner_name}"
24
- params['filters'] = "channel==#{@parent.id}"
25
- params['start-date'] = @days_range.begin
26
- params['end-date'] = @days_range.end
27
- params['metrics'] = [:estimatedMinutesWatched, :earnings].join ','
28
- params['dimensions'] = :day
29
- end
30
- end
31
- end
32
-
33
- def items_key
34
- 'rows'
9
+ def metrics
10
+ [:estimatedMinutesWatched, :earnings].join ','
35
11
  end
36
12
  end
37
13
  end
@@ -6,6 +6,8 @@ module Yt
6
6
 
7
7
  private
8
8
 
9
+ # @return [Hash] the parameters to submit to YouTube to list partnered channels.
10
+ # @see https://developers.google.com/youtube/v3/docs/channels/list
9
11
  def list_params
10
12
  super.tap do |params|
11
13
  params[:params].delete :mine
@@ -14,6 +16,9 @@ module Yt
14
16
  end
15
17
  end
16
18
 
19
+ # @private
20
+ # @note Partnered Channels overwrites +list_resources+ since the endpoint
21
+ # to hit is 'channels', not 'partnered_channels'.
17
22
  def list_resources
18
23
  self.class.superclass
19
24
  end
@@ -0,0 +1,52 @@
1
+ require 'yt/collections/base'
2
+
3
+ module Yt
4
+ module Collections
5
+ class Reports < Base
6
+
7
+ def within(days_range)
8
+ @days_range = days_range
9
+ Hash[*flat_map{|daily_value| daily_value}]
10
+ end
11
+
12
+ private
13
+
14
+ def new_item(data)
15
+ [day_of(data), value_of(data)]
16
+ end
17
+
18
+ # @see https://developers.google.com/youtube/analytics/v1/content_owner_reports
19
+ def list_params
20
+ super.tap do |params|
21
+ params[:path] = '/youtube/analytics/v1/reports'
22
+ params[:params] = {}.tap do |params|
23
+ params['ids'] = "contentOwner==#{@auth.owner_name}"
24
+ params['filters'] = "channel==#{@parent.id}"
25
+ params['start-date'] = @days_range.begin
26
+ params['end-date'] = @days_range.end
27
+ params['metrics'] = metrics
28
+ params['dimensions'] = :day
29
+ end
30
+ end
31
+ end
32
+
33
+ def day_of(data)
34
+ # NOTE: could use column headers to be more precise
35
+ Date.iso8601 data.first
36
+ end
37
+
38
+ def value_of(data)
39
+ # NOTE: could use column headers to be more precise
40
+ data.last
41
+ end
42
+
43
+ def metrics
44
+ ''
45
+ end
46
+
47
+ def items_key
48
+ 'rows'
49
+ end
50
+ end
51
+ end
52
+ end
@@ -3,14 +3,22 @@ require 'yt/models/video'
3
3
 
4
4
  module Yt
5
5
  module Collections
6
+ # Provides methods to interact with a collection of YouTube videos.
7
+ # Resources with videos are: {Yt::Models::Channel channels} and
8
+ # {Yt::Models::Account accounts}.
6
9
  class Videos < Base
7
10
 
8
11
  private
9
12
 
13
+ # @return [Yt::Models::Video] a new video initialized with one of
14
+ # the items returned by asking YouTube for a list of videos.
15
+ # @see https://developers.google.com/youtube/v3/docs/videos#resource
10
16
  def new_item(data)
11
17
  Yt::Video.new id: data['id']['videoId'], snippet: data['snippet'], auth: @auth
12
18
  end
13
19
 
20
+ # @return [Hash] the parameters to submit to YouTube to list videos.
21
+ # @see https://developers.google.com/youtube/v3/docs/search/list
14
22
  def list_params
15
23
  super.tap do |params|
16
24
  params[:params] = @parent.videos_params.merge videos_params
@@ -1,37 +1,17 @@
1
- require 'yt/collections/base'
1
+ require 'yt/collections/reports'
2
2
 
3
3
  module Yt
4
4
  module Collections
5
- class Views < Base
6
-
7
- def within(days_range)
8
- @days_range = days_range
9
- Hash[*flat_map{|daily_view| daily_view}]
10
- end
5
+ class Views < Reports
11
6
 
12
7
  private
13
8
 
14
- def new_item(data)
15
- # NOTE: could use column headers to be more precise
16
- [Date.iso8601(data.first), (data.last.to_i if data.last)]
17
- end
18
-
19
- def list_params
20
- super.tap do |params|
21
- params[:path] = '/youtube/analytics/v1/reports'
22
- params[:params] = {}.tap do |params|
23
- params['ids'] = "contentOwner==#{@auth.owner_name}"
24
- params['filters'] = "channel==#{@parent.id}"
25
- params['start-date'] = @days_range.begin
26
- params['end-date'] = @days_range.end
27
- params['metrics'] = :views
28
- params['dimensions'] = :day
29
- end
30
- end
9
+ def metrics
10
+ :views
31
11
  end
32
12
 
33
- def items_key
34
- 'rows'
13
+ def value_of(data)
14
+ data.last.to_i if data.last
35
15
  end
36
16
  end
37
17
  end
data/lib/yt/config.rb CHANGED
@@ -1,53 +1,54 @@
1
1
  require 'yt/models/configuration'
2
2
 
3
3
  module Yt
4
- # Provides methods to read and write runtime configuration information.
4
+ # Provides methods to read and write global configuration settings.
5
5
  #
6
- # Configuration options are loaded from `~/.yt`, `.yt`, command line
7
- # switches, and the `YT_OPTS` environment variable (listed in lowest to
8
- # highest precedence).
9
- #
10
- # @note Config is the only module auto-loaded in the Yt module,
11
- # in order to have a syntax as easy as Yt.configure
12
- #
13
- # @example A server-to-server YouTube client app
6
+ # A typical usage is to set the API keys retrieved from the
7
+ # {http://console.developers.google.com Google Developers Console}.
14
8
  #
9
+ # @example Set the API key for a server-only YouTube app:
15
10
  # Yt.configure do |config|
16
11
  # config.api_key = 'ABCDEFGHIJ1234567890'
17
12
  # end
18
13
  #
19
- # @example A web YouTube client app
20
- #
14
+ # @example Set the API client id/secret for a web-client YouTube app:
21
15
  # Yt.configure do |config|
22
16
  # config.client_id = 'ABCDEFGHIJ1234567890'
23
17
  # config.client_secret = 'ABCDEFGHIJ1234567890'
24
18
  # end
25
19
  #
20
+ # Note that Yt.configure has precedence over values through with
21
+ # environment variables (see {Yt::Models::Configuration}).
22
+ #
26
23
  module Config
27
- # Yields the global configuration to a block.
28
- # @yield [Yt::Configuration] global configuration
24
+ # Yields the global configuration to the given block.
29
25
  #
30
26
  # @example
31
27
  # Yt.configure do |config|
32
28
  # config.api_key = 'ABCDEFGHIJ1234567890'
33
29
  # end
34
- # @see Yt::Configuration
30
+ #
31
+ # @yield [Yt::Models::Configuration] The global configuration.
35
32
  def configure
36
33
  yield configuration if block_given?
37
34
  end
38
35
 
39
- # Returns the global [Configuration](Yt/Configuration) object. While you
40
- # _can_ use this method to access the configuration, the more common
41
- # convention is to use [Yt.configure](Yt#configure-class_method).
36
+ # Returns the global {Yt::Models::Configuration} object.
37
+ #
38
+ # While this method _can_ be used to read and write configuration settings,
39
+ # it is easier to use {Yt::Config#configure} Yt.configure}.
42
40
  #
43
41
  # @example
44
42
  # Yt.configuration.api_key = 'ABCDEFGHIJ1234567890'
45
- # @see Yt.configure
46
- # @see Yt::Configuration
43
+ #
44
+ # @return [Yt::Models::Configuration] The global configuration.
47
45
  def configuration
48
46
  @configuration ||= Yt::Configuration.new
49
47
  end
50
48
  end
51
49
 
50
+ # @note Config is the only module auto-loaded in the Yt module,
51
+ # in order to have a syntax as easy as Yt.configure
52
+
52
53
  extend Config
53
54
  end
@@ -3,14 +3,30 @@ require 'yt/associations/authentications'
3
3
 
4
4
  module Yt
5
5
  module Models
6
- # Provides methods to access a YouTube account.
6
+ # Provides methods to interact with YouTube accounts.
7
+ # @see https://developers.google.com/youtube/v3/guides/authentication
7
8
  class Account < Base
8
9
  include Associations::Authentications
9
10
 
10
- has_one :channel, delegate: [:videos, :playlists, :create_playlist, :delete_playlists, :update_playlists]
11
- has_one :user_info, delegate: [:id, :email, :has_verified_email?, :gender, :name, :given_name, :family_name, :profile_url, :avatar_url, :locale, :hd]
11
+ # @!attribute channel
12
+ # @return [Yt::Models::Channel] the account’s channel.
13
+ has_one :channel
14
+ delegate :playlists, :create_playlist, :delete_playlists, to: :channel
15
+
16
+ # @!attribute user_info
17
+ # @return [Yt::Models::UserInfo] the account’s profile information.
18
+ has_one :user_info
19
+ delegate :id, :email, :has_verified_email?, :gender, :name,
20
+ :given_name, :family_name, :profile_url, :avatar_url,
21
+ :locale, :hd, to: :user_info
22
+
23
+ # @!attribute videos
24
+ # @return [Yt::Collections::Videos] the videos owned by the account.
12
25
  has_many :videos
13
26
 
27
+ # @private
28
+ # Tells `has_many :videos` that account.videos should return all the
29
+ # videos *owned by* the account (public, private, unlisted).
14
30
  def videos_params
15
31
  {forMine: true}
16
32
  end
@@ -1,82 +1,71 @@
1
1
  module Yt
2
2
  module Models
3
- # Provides methods to access and analyze a single YouTube annotation.
3
+ # Provides methods to interact with YouTube annotations.
4
+ # @see https://www.youtube.com/yt/playbook/annotations.html
4
5
  class Annotation
5
- # Instantiate an Annotation object from its YouTube XML representation.
6
- #
7
- # @note There is no documented way to access annotations through API.
8
- # There is an endpoint that returns an XML in an undocumented format,
9
- # which is here parsed into a comprehensible set of attributes.
10
- #
11
- # @param [String] xml_data The YouTube XML representation of an annotation
6
+ # @param [Hash] options the options to initialize an Annotation.
7
+ # @option options [String] :data The XML representation of an annotation
8
+ # @note YouTube API V3 does not provide access to video annotations,
9
+ # therefore the XML endpoint is used to retrieve them and its response
10
+ # is passed to the Annotation initializer.
12
11
  def initialize(options = {})
13
12
  @data = options[:data]
14
13
  end
15
14
 
16
- # Checks whether the entire annotation box remains above y
17
- #
15
+ # @return [Boolean] whether the text box surrounding the annotation is
16
+ # completely in the top y% of the video frame.
18
17
  # @param [Integer] y Vertical position in the Youtube video (0 to 100)
19
- #
20
- # @return [Boolean] Whether the box remains above y
21
18
  def above?(y)
22
19
  top && top < y
23
20
  end
24
21
 
25
- # Checks whether the entire annotation box remains below y
26
- #
22
+ # @return [Boolean] whether the text box surrounding the annotation is
23
+ # completely in the bottom y% of the video frame.
27
24
  # @param [Integer] y Vertical position in the Youtube video (0 to 100)
28
- #
29
- # @return [Boolean] Whether the box remains below y
30
25
  def below?(y)
31
26
  bottom && bottom > y
32
27
  end
33
28
 
34
- # Checks whether there is a link to subscribe.
35
- # Should a branding watermark also counts, because it links to the channel?
36
- #
37
- # @return [Boolean] Whether there is a link to subscribe in the annotation
29
+ # @return [Boolean] whether the annotation includes a link to subscribe.
38
30
  def has_link_to_subscribe?(options = {}) # TODO: options for which videos
39
31
  link_class == '5'
40
32
  end
41
33
 
42
- # Checks whether there is a link to a video.
43
- # An Invideo featured video also counts
44
- #
45
- # @return [Boolean] Whether there is a link to a video in the annotation
34
+ # @return [Boolean] whether the annotation includes a link to a video,
35
+ # either directly in the text, or as an "Invideo featured video".
46
36
  def has_link_to_video?(options = {}) # TODO: options for which videos
47
37
  link_class == '1' || type == 'promotion'
48
38
  end
49
39
 
50
- # Checks whether there is a link to a playlist.
51
- # A link to a video with the playlist in the URL also counts
52
- #
53
- # @return [Boolean] Whether there is a link to a playlist in the annotation
40
+ # @return [Boolean] whether the annotation includes a link to a playlist,
41
+ # or to a video embedded in a playlist.
54
42
  def has_link_to_playlist?
55
43
  link_class == '2' || text.include?('&list=')
56
44
  end
57
45
 
58
- # Checks whether the link opens in the same window.
59
- #
60
- # @return [Boolean] Whether the link opens in the same window
46
+ # @return [Boolean] whether the annotation includes a link that will
47
+ # open in the current browser window.
61
48
  def has_link_to_same_window?
62
49
  link_target == 'current'
63
50
  end
64
51
 
65
- # Checks whether the annotation comes from InVideo Programming
66
- #
67
- # @return [Boolean] Whether the annotation comes from InVideo Programming
52
+ # @return [Boolean] whether the annotation is an "InVideo Programming".
68
53
  def has_invideo_programming?
69
54
  type == 'promotion' || type == 'branding'
70
55
  end
71
56
 
72
- # @return [Boolean] Whether the annotation starts after the number of seconds
57
+ # @param [Numeric] seconds the number of seconds
58
+ # @return [Boolean] whether the annotation starts after the number of
59
+ # seconds indicated.
73
60
  # @note This is broken for invideo programming, because they do not
74
61
  # have the timestamp in the region, but in the "data" field
75
62
  def starts_after?(seconds)
76
63
  timestamps.first > seconds if timestamps.any?
77
64
  end
78
65
 
79
- # @return [Boolean] Whether the annotation starts before the number of seconds
66
+ # @param [Numeric] seconds the number of seconds
67
+ # @return [Boolean] whether the annotation starts before the number of
68
+ # seconds indicated.
80
69
  # @note This is broken for invideo programming, because they do not
81
70
  # have the timestamp in the region, but in the "data" field
82
71
  def starts_before?(seconds)
@@ -1,5 +1,7 @@
1
1
  module Yt
2
2
  module Models
3
+ # Provides methods to authenticate with YouTube.
4
+ # @see https://developers.google.com/youtube/v3/guides/authentication
3
5
  class Authentication
4
6
  attr_reader :access_token, :refresh_token, :expires_at
5
7
 
@@ -5,13 +5,13 @@ require 'yt/errors/request_error'
5
5
  require 'active_support/core_ext/module/delegation' # for delegate
6
6
  require 'active_support/core_ext/string/inflections' # for camelize
7
7
 
8
-
9
8
  module Yt
10
9
  module Models
11
10
  class Base
12
11
  include Actions::Delete
13
12
  include Actions::Update
14
13
 
14
+ # @private
15
15
  def self.has_many(attributes)
16
16
  attributes = attributes.to_s
17
17
  require "yt/collections/#{attributes}"
@@ -24,9 +24,8 @@ module Yt
24
24
  end
25
25
  end
26
26
 
27
- def self.has_one(attribute, options = {})
28
- delegate *options[:delegate], to: attribute if options[:delegate]
29
-
27
+ # @private
28
+ def self.has_one(attribute)
30
29
  attributes = attribute.to_s.pluralize
31
30
  has_many attributes
32
31
 
@@ -4,30 +4,79 @@ require 'yt/associations/views'
4
4
 
5
5
  module Yt
6
6
  module Models
7
+ # Provides methods to interact with YouTube channels.
8
+ # @see https://developers.google.com/youtube/v3/docs/channels
7
9
  class Channel < Resource
8
10
  include Associations::Earnings
9
11
  include Associations::Views
10
12
 
13
+ # @!attribute subscriptions
14
+ # @return [Yt::Collections::Subscriptions] the channel’s subscriptions.
11
15
  has_many :subscriptions
16
+
17
+ # @!attribute videos
18
+ # @return [Yt::Collections::Videos] the channel’s videos.
12
19
  has_many :videos
20
+
21
+ # @!attribute playlists
22
+ # @return [Yt::Collections::Playlists] the channel’s playlists.
13
23
  has_many :playlists
14
24
 
25
+ # Returns whether the authenticated account is subscribed to the channel.
26
+ #
27
+ # This method requires {Resource#auth auth} to return an
28
+ # authenticated instance of {Yt::Account}.
29
+ # @return [Boolean] whether the account is subscribed to the channel.
30
+ # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
31
+ # return an authenticated account.
15
32
  def subscribed?
16
33
  subscriptions.any?{|s| s.exists?}
17
34
  end
18
35
 
36
+ # Subscribes the authenticated account to the channel.
37
+ # Does not raise an error if the account was already subscribed.
38
+ #
39
+ # This method requires {Resource#auth auth} to return an
40
+ # authenticated instance of {Yt::Account}.
41
+ # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
42
+ # return an authenticated account.
19
43
  def subscribe
20
44
  subscriptions.insert ignore_errors: true
21
45
  end
22
46
 
47
+ # Subscribes the authenticated account to the channel.
48
+ # Raises an error if the account was already subscribed.
49
+ #
50
+ # This method requires {Resource#auth auth} to return an
51
+ # authenticated instance of {Yt::Account}.
52
+ # @raise [Yt::Errors::RequestError] if {Resource#auth auth} was already
53
+ # subscribed to the channel.
54
+ # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
55
+ # return an authenticated account.
23
56
  def subscribe!
24
57
  subscriptions.insert
25
58
  end
26
59
 
60
+ # Unsubscribes the authenticated account from the channel.
61
+ # Does not raise an error if the account was not subscribed.
62
+ #
63
+ # This method requires {Resource#auth auth} to return an
64
+ # authenticated instance of {Yt::Account}.
65
+ # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
66
+ # return an authenticated account.
27
67
  def unsubscribe
28
68
  subscriptions.delete_all({}, ignore_errors: true)
29
69
  end
30
70
 
71
+ # Unsubscribes the authenticated account from the channel.
72
+ # Raises an error if the account was not subscribed.
73
+ #
74
+ # This method requires {Resource#auth auth} to return an
75
+ # authenticated instance of {Yt::Account}.
76
+ # @raise [Yt::Errors::RequestError] if {Resource#auth auth} was not
77
+ # subscribed to the channel.
78
+ # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
79
+ # return an authenticated account.
31
80
  def unsubscribe!
32
81
  subscriptions.delete_all
33
82
  end
@@ -40,6 +89,9 @@ module Yt
40
89
  playlists.delete_all attrs
41
90
  end
42
91
 
92
+ # @private
93
+ # Tells `has_many :videos` that channel.videos should return all the
94
+ # videos publicly available on the channel.
43
95
  def videos_params
44
96
  {channelId: id}
45
97
  end
@@ -1,27 +1,48 @@
1
1
  module Yt
2
2
  module Models
3
- # Stores runtime configuration information.
3
+ # Provides an object to store global configuration settings.
4
4
  #
5
- # Configuration options are loaded from `~/.yt`, `.yt`, command line
6
- # switches, and the `YT_OPTS` environment variable (listed in lowest to
7
- # highest precedence).
8
- #
9
- # @example A server-to-server YouTube client app
10
- #
11
- # Yt.configure do |config|
12
- # config.api_key = 'ABCDEFGHIJ1234567890'
13
- # end
14
- #
15
- # @example A web YouTube client app
5
+ # This class is typically not used directly, but by calling
6
+ # {Yt::Config#configure Yt.configure}, which creates and updates a single
7
+ # instance of {Yt::Models::Configuration}.
16
8
  #
9
+ # @example Set the API client id/secret for a web-client YouTube app:
17
10
  # Yt.configure do |config|
18
11
  # config.client_id = 'ABCDEFGHIJ1234567890'
19
12
  # config.client_secret = 'ABCDEFGHIJ1234567890'
20
13
  # end
21
14
  #
15
+ # @see Yt::Config for more examples.
16
+ #
17
+ # An alternative way to set global configuration settings is by storing
18
+ # them in the following environment variables:
19
+ #
20
+ # * +YT_CLIENT_ID+ to store the Client ID for web/device apps
21
+ # * +YT_CLIENT_SECRET+ to store the Client Secret for web/device apps
22
+ # * +YT_API_KEY+ to store the API key for server/browser apps
23
+ #
24
+ # In case both methods are used together,
25
+ # {Yt::Config#configure Yt.configure} takes precedence.
26
+ #
27
+ # @example Set the API client id/secret for a web-client YouTube app:
28
+ # ENV['YT_CLIENT_ID'] = 'ABCDEFGHIJ1234567890'
29
+ # ENV['YT_CLIENT_SECRET'] = 'ABCDEFGHIJ1234567890'
30
+ #
22
31
  class Configuration
23
- attr_accessor :api_key, :client_id, :client_secret
32
+ # @return [String] the Client ID for web/device YouTube applications.
33
+ # @see https://console.developers.google.com Google Developers Console
34
+ attr_accessor :client_id
35
+
36
+ # @return [String] the Client Secret for web/device YouTube applications.
37
+ # @see https://console.developers.google.com Google Developers Console
38
+ attr_accessor :client_secret
39
+
40
+ # @return [String] the API key for server/browser YouTube applications.
41
+ # @see https://console.developers.google.com Google Developers Console
42
+ attr_accessor :api_key
24
43
 
44
+ # Initialize the global configuration settings, using the values of
45
+ # the specified following environment variables by default.
25
46
  def initialize
26
47
  @client_id = ENV['YT_CLIENT_ID']
27
48
  @client_secret = ENV['YT_CLIENT_SECRET']
@@ -2,9 +2,16 @@ require 'yt/models/account'
2
2
 
3
3
  module Yt
4
4
  module Models
5
- # Provides methods to access a YouTube content owner.
5
+ # Provides methods to interact with YouTube CMS accounts.
6
+ # @see https://cms.youtube.com
7
+ # @see https://developers.google.com/youtube/analytics/v1/content_owner_reports
6
8
  class ContentOwner < Account
9
+
10
+ # @!attribute partnered_channels
11
+ # @return [Yt::Collection::PartneredChannels] the channels managed by the CMS account.
7
12
  has_many :partnered_channels
13
+
14
+ # @return [String] the name of the CMS account.
8
15
  attr_reader :owner_name
9
16
 
10
17
  def initialize(options = {})
@@ -6,18 +6,16 @@ module Yt
6
6
  # Resources with descriptions are: videos and channels.
7
7
  #
8
8
  # @example
9
- #
10
- # description = Yt::Description.new 'Fullscreen provides a suite of end-to-end YouTube tools and services to many of the world’s leading brands and media companies.'
11
- # description.to_s.slice(0,19) # => 'Fullscreen provides'
12
- # description.length # => 127
9
+ # description = Yt::Description.new 'Fullscreen provides a suite of end-to-end YouTube tools and services to many of the world’s leading brands and media companies.'
10
+ # description.to_s.slice(0,19) # => 'Fullscreen provides'
11
+ # description.length # => 127
13
12
  #
14
13
  class Description < String
15
14
  # Returns whether the description includes a YouTube video URL
16
15
  #
17
16
  # @example
18
- #
19
- # description = Yt::Description.new 'Link to video: youtube.com/watch?v=MESycYJytkU'
20
- # description.has_link_to_video? #=> true
17
+ # description = Yt::Description.new 'Link to video: youtube.com/watch?v=MESycYJytkU'
18
+ # description.has_link_to_video? #=> true
21
19
  #
22
20
  # @return [Boolean] Whether the description includes a link to a video
23
21
  def has_link_to_video?
@@ -29,9 +27,8 @@ module Yt
29
27
  # Returns whether the description includes a YouTube channel URL
30
28
  #
31
29
  # @example
32
- #
33
- # description = Yt::Description.new 'Link to channel: youtube.com/fullscreen'
34
- # description.has_link_to_channel? #=> true
30
+ # description = Yt::Description.new 'Link to channel: youtube.com/fullscreen'
31
+ # description.has_link_to_channel? #=> true
35
32
  #
36
33
  # @return [Boolean] Whether the description includes a link to a channel
37
34
  def has_link_to_channel?(options = {}) # TODO: which channel
@@ -43,9 +40,8 @@ module Yt
43
40
  # Returns whether the description includes a YouTube subscription URL
44
41
  #
45
42
  # @example
46
- #
47
- # description = Yt::Description.new 'Link to subscribe: youtube.com/subscription_center?add_user=fullscreen'
48
- # description.has_link_to_subscribe? #=> true
43
+ # description = Yt::Description.new 'Link to subscribe: youtube.com/subscription_center?add_user=fullscreen'
44
+ # description.has_link_to_subscribe? #=> true
49
45
  #
50
46
  # @return [Boolean] Whether the description includes a link to subscribe
51
47
  def has_link_to_subscribe?(options = {}) # TODO: which channel
@@ -57,9 +53,8 @@ module Yt
57
53
  # Returns whether the description includes a YouTube playlist URL
58
54
  #
59
55
  # @example
60
- #
61
- # description = Yt::Description.new 'Link to playlist: youtube.com/playlist?list=LLxO1tY8h1AhOz0T4ENwmpow'
62
- # description.has_link_to_playlist? #=> true
56
+ # description = Yt::Description.new 'Link to playlist: youtube.com/playlist?list=LLxO1tY8h1AhOz0T4ENwmpow'
57
+ # description.has_link_to_playlist? #=> true
63
58
  #
64
59
  # @return [Boolean] Whether the description includes a link to a playlist
65
60
  def has_link_to_playlist?
@@ -2,7 +2,12 @@ require 'yt/models/resource'
2
2
 
3
3
  module Yt
4
4
  module Models
5
+ # Provides methods to interact with YouTube playlists.
6
+ # @see https://developers.google.com/youtube/v3/docs/playlists
5
7
  class Playlist < Resource
8
+
9
+ # @!attribute playlist_items
10
+ # @return [Yt::Collections::PlaylistItems] the playlist’s items.
6
11
  has_many :playlist_items
7
12
 
8
13
  def delete
@@ -2,8 +2,18 @@ require 'yt/models/base'
2
2
 
3
3
  module Yt
4
4
  module Models
5
+ # Provides methods to interact with YouTube playlist items.
6
+ # @see https://developers.google.com/youtube/v3/docs/playlistItems
5
7
  class PlaylistItem < Base
6
- attr_reader :id, :video, :position
8
+ # @return [String] the ID that uniquely identify a YouTube playlist item.
9
+ attr_reader :id
10
+
11
+ # @return [String] the order in which the item appears in the playlist.
12
+ # The value uses a zero-based index, so the first item has a position
13
+ # of 0, the second item has a position of 1, and so forth.
14
+ attr_reader :position
15
+
16
+ attr_reader :video
7
17
 
8
18
  def initialize(options = {})
9
19
  @id = options[:id]
@@ -2,7 +2,12 @@ require 'yt/models/base'
2
2
 
3
3
  module Yt
4
4
  module Models
5
+ # Provides methods to modify the rating of a video on YouTube.
6
+ # @see https://developers.google.com/youtube/v3/docs/videos/rate
7
+ # @see https://developers.google.com/youtube/v3/docs/videos/getRating
5
8
  class Rating < Base
9
+ # @return [Symbol or nil] the rating of a video (if present).
10
+ # Valid values are: :dislike, :like, :none, :unspecified
6
11
  attr_reader :rating
7
12
 
8
13
  def initialize(options = {})
@@ -29,7 +29,7 @@ module Yt
29
29
  if response.is_a? @expected_response
30
30
  response.tap{|response| response.body = parse_format response.body}
31
31
  else
32
- run_again? ? run : raise(error_for(response), request_error_message)
32
+ run_again? ? run : raise(response_error, request_error_message)
33
33
  end
34
34
  end
35
35
 
@@ -39,6 +39,8 @@ module Yt
39
39
  @response ||= Net::HTTP.start(*net_http_options) do |http|
40
40
  http.request http_request
41
41
  end
42
+ rescue OpenSSL::SSL::SSLError, Errno::ETIMEDOUT, Errno::ENETUNREACH => e
43
+ @response ||= e
42
44
  end
43
45
 
44
46
  def http_request
@@ -105,7 +107,7 @@ module Yt
105
107
  # random error that can be fixed by waiting for some seconds and running
106
108
  # the exact same query, or the access token needs to be refreshed.
107
109
  def run_again?
108
- run_again_with_refreshed_authentication? || run_again_after_a_while?
110
+ refresh_token_and_retry? || server_error? && sleep_and_retry?
109
111
  end
110
112
 
111
113
  # Once in a while, YouTube responds with 500, or 503, or 400 Error and
@@ -113,17 +115,20 @@ module Yt
113
115
  # In all these cases, running the same query after some seconds fixes
114
116
  # the issue. This it not documented by YouTube and hardly testable, but
115
117
  # trying again is a workaround that works and hardly causes any damage.
116
- def run_again_after_a_while?(max_retries = 1)
118
+ def sleep_and_retry?(max_retries = 1)
117
119
  @retries_so_far ||= -1
118
120
  @retries_so_far += 1
119
- if (@retries_so_far < max_retries) && worth_another_try?
121
+ if (@retries_so_far < max_retries)
120
122
  @response = @http_request = @uri = nil
121
123
  sleep 3
122
124
  end
123
125
  end
124
126
 
125
- def worth_another_try?
127
+ def server_error?
126
128
  case response
129
+ when OpenSSL::SSL::SSLError then true
130
+ when Errno::ETIMEDOUT then true
131
+ when Errno::ENETUNREACH then true
127
132
  when Net::HTTPServerError then true
128
133
  when Net::HTTPBadRequest then response.body =~ /did not conform/
129
134
  else false
@@ -133,26 +138,28 @@ module Yt
133
138
  # If a request authorized with an access token returns 401, then the
134
139
  # access token might have expired. If a refresh token is also present,
135
140
  # try to run the request one more time with a refreshed access token.
136
- def run_again_with_refreshed_authentication?
141
+ def refresh_token_and_retry?
137
142
  if response.is_a? Net::HTTPUnauthorized
138
143
  @response = @http_request = @uri = nil
139
144
  @auth.refresh
140
145
  end if @auth.respond_to? :refresh
141
146
  end
142
147
 
143
- def error_for(response)
144
- case response
145
- when Net::HTTPServerError then Errors::ServerError
148
+ def response_error
149
+ if server_error?
150
+ Errors::ServerError
151
+ else case response
146
152
  when Net::HTTPUnauthorized then Errors::Unauthorized
147
153
  when Net::HTTPForbidden then Errors::Forbidden
148
154
  else Errors::RequestError
155
+ end
149
156
  end
150
157
  end
151
158
 
152
159
  def request_error_message
153
160
  {}.tap do |message|
154
161
  message[:request_curl] = as_curl
155
- message[:response_body] = JSON(response.body) rescue response.body
162
+ message[:response_body] = JSON(response.body) rescue response.inspect
156
163
  end.to_json
157
164
  end
158
165
 
@@ -6,8 +6,13 @@ module Yt
6
6
  class Resource < Base
7
7
  attr_reader :auth
8
8
  has_one :id
9
- has_one :snippet, delegate: [:title, :description, :thumbnail_url, :published_at, :tags]
10
- has_one :status, delegate: [:privacy_status, :public?, :private?, :unlisted?]
9
+
10
+ has_one :snippet
11
+ delegate :title, :description, :thumbnail_url, :published_at,
12
+ :tags, to: :snippet
13
+
14
+ has_one :status
15
+ delegate :privacy_status, :public?, :private?, :unlisted?, to: :status
11
16
 
12
17
  def initialize(options = {})
13
18
  @url = URL.new(options[:url]) if options[:url]
@@ -2,8 +2,10 @@ require 'yt/models/base'
2
2
 
3
3
  module Yt
4
4
  module Models
5
+ # Provides methods to interact with YouTube subscriptions.
6
+ # @see https://developers.google.com/youtube/v3/docs/subscriptions
5
7
  class Subscription < Base
6
-
8
+ # @return [String] the ID that uniquely identify a YouTube subscription.
7
9
  attr_reader :id
8
10
 
9
11
  def initialize(options = {})
@@ -2,64 +2,66 @@ require 'yt/models/base'
2
2
 
3
3
  module Yt
4
4
  module Models
5
+ # Provides methods to retrieve an account’s user profile.
6
+ # @see https://developers.google.com/+/api/latest/people/getOpenIdConnect
5
7
  class UserInfo < Base
6
8
  def initialize(options = {})
7
9
  @data = options[:data]
8
10
  end
9
11
 
10
- # @return [String] User ID
12
+ # @return [String] the user’s ID.
11
13
  def id
12
14
  @id ||= @data.fetch 'id', ''
13
15
  end
14
16
 
15
- # Return the email of the YouTube account.
16
- #
17
- # @return [String] Email of the YouTube account
17
+ # @return [String] the user’s email address.
18
18
  def email
19
19
  @email ||= @data.fetch 'email', ''
20
20
  end
21
21
 
22
- # @return [Boolean] Email is verified?
22
+ # @return [Boolean] whether the email address is verified.
23
23
  def has_verified_email?
24
24
  @verified_email ||= @data.fetch 'verified_email', false
25
25
  end
26
26
 
27
- # @return [String] name
27
+ # @return [String] the user's full name.
28
28
  def name
29
29
  @name ||= @data.fetch 'name', ''
30
30
  end
31
31
 
32
- # @return [String] given_name
32
+ # @return [String] the user’s given (first) name.
33
33
  def given_name
34
34
  @given_name ||= @data.fetch 'given_name', ''
35
35
  end
36
36
 
37
- # @return [String] family_name
37
+ # @return [String] the user’s family (last) name.
38
38
  def family_name
39
39
  @family_name ||= @data.fetch 'family_name', ''
40
40
  end
41
41
 
42
- # @return [String] family_name
42
+ # @return [String] the URL of the user’s profile page.
43
43
  def profile_url
44
44
  @profile_url ||= @data.fetch 'link', ''
45
45
  end
46
46
 
47
- # @return [String] avatar_url
47
+ # @return [String] the URL of the user’s profile picture.
48
48
  def avatar_url
49
49
  @avatar_url ||= @data.fetch 'picture', ''
50
50
  end
51
51
 
52
- # @return [String] gender
52
+ # @return [String] the person’s gender. Possible values include, but
53
+ # are not limited to, "male", "female", "other".
53
54
  def gender
54
55
  @gender ||= @data.fetch 'gender', ''
55
56
  end
56
57
 
57
- # @return [String] locale
58
+ # @return [String] the user’s preferred locale.
58
59
  def locale
59
60
  @locale ||= @data.fetch 'locale', ''
60
61
  end
61
62
 
62
- # @return [String] hd
63
+ # @return [String] the hosted domain name for the user’s Google Apps
64
+ # account. For instance, example.com.
63
65
  def hd
64
66
  @hd ||= @data.fetch 'hd', ''
65
67
  end
@@ -2,25 +2,64 @@ require 'yt/models/resource'
2
2
 
3
3
  module Yt
4
4
  module Models
5
+ # Provides methods to interact with YouTube videos.
6
+ # @see https://developers.google.com/youtube/v3/docs/videos
5
7
  class Video < Resource
6
- has_one :details_set, delegate: [:duration]
8
+ # @!attribute details_set
9
+ # @return [Yt::Models::DetailsSet] the video’s content details.
10
+ has_one :details_set
11
+ delegate :duration, to: :details_set
12
+
13
+ # @!attribute rating
14
+ # @return [Yt::Models::Rating] the video’s rating.
7
15
  has_one :rating
16
+
17
+ # @!attribute annotations
18
+ # @return [Yt::Collections::Annotations] the video’s annotations.
8
19
  has_many :annotations
9
20
 
21
+ # Returns whether the authenticated account likes the video.
22
+ #
23
+ # This method requires {Resource#auth auth} to return an
24
+ # authenticated instance of {Yt::Account}.
25
+ # @return [Boolean] whether the account likes the video.
26
+ # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
27
+ # return an authenticated account.
10
28
  def liked?
11
29
  rating.rating == :like
12
30
  end
13
31
 
32
+ # Likes the video on behalf of the authenticated account.
33
+ #
34
+ # This method requires {Resource#auth auth} to return an
35
+ # authenticated instance of {Yt::Account}.
36
+ # @return [Boolean] whether the account likes the video.
37
+ # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
38
+ # return an authenticated account.
14
39
  def like
15
40
  rating.update :like
16
41
  liked?
17
42
  end
18
43
 
44
+ # Dislikes the video on behalf of the authenticated account.
45
+ #
46
+ # This method requires {Resource#auth auth} to return an
47
+ # authenticated instance of {Yt::Account}.
48
+ # @return [Boolean] whether the account does not like the video.
49
+ # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
50
+ # return an authenticated account.
19
51
  def dislike
20
52
  rating.update :dislike
21
53
  !liked?
22
54
  end
23
55
 
56
+ # Resets the rating of the video on behalf of the authenticated account.
57
+ #
58
+ # This method requires {Resource#auth auth} to return an
59
+ # authenticated instance of {Yt::Account}.
60
+ # @return [Boolean] whether the account does not like the video.
61
+ # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
62
+ # return an authenticated account.
24
63
  def unlike
25
64
  rating.update :none
26
65
  !liked?
data/lib/yt/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Yt
2
- VERSION = '0.6.2'
2
+ VERSION = '0.6.3'
3
3
  end
@@ -6,11 +6,12 @@ describe Yt::Request do
6
6
  subject(:request) { Yt::Request.new host: 'example.com' }
7
7
  let(:response) { response_class.new nil, nil, nil }
8
8
  let(:response_body) { }
9
- before { allow(response).to receive(:body).and_return response_body }
10
- before { expect(Net::HTTP).to receive(:start).once.and_return response }
11
9
 
12
10
  describe '#run' do
13
11
  context 'given a request that returns' do
12
+ before { allow(response).to receive(:body).and_return response_body }
13
+ before { expect(Net::HTTP).to receive(:start).once.and_return response }
14
+
14
15
  context 'a success code 2XX' do
15
16
  let(:response_class) { Net::HTTPOK }
16
17
 
@@ -70,5 +71,73 @@ describe Yt::Request do
70
71
  it { expect{request.run}.to fail }
71
72
  end
72
73
  end
74
+
75
+ # TODO: clean up the following tests, removing repetitions
76
+ context 'given a request that raises' do
77
+ before { expect(Net::HTTP).to receive(:start).once.and_raise http_error }
78
+
79
+ # NOTE: This test is just a reflection of YouTube irrational behavior of
80
+ # being unavailable once in a while, and therefore causing Net::HTTP to
81
+ # fail, although retrying after some seconds works.
82
+ context 'an OpenSSL::SSL::SSLError' do
83
+ let(:http_error) { OpenSSL::SSL::SSLError.new }
84
+
85
+ context 'every time' do
86
+ before { expect(Net::HTTP).to receive(:start).at_least(:once).and_raise http_error }
87
+
88
+ it { expect{request.run}.to fail }
89
+ end
90
+
91
+ context 'but works the second time' do
92
+ before { expect(Net::HTTP).to receive(:start).at_least(:once).and_return retry_response }
93
+ before { allow(retry_response).to receive(:body) }
94
+ let(:retry_response) { Net::HTTPOK.new nil, nil, nil }
95
+
96
+ it { expect{request.run}.not_to fail }
97
+ end
98
+ end
99
+
100
+ # NOTE: This test is just a reflection of YouTube irrational behavior of
101
+ # being unavailable once in a while, and therefore causing Net::HTTP to
102
+ # fail, although retrying after some seconds works.
103
+ context 'an Errno::ETIMEDOUT' do
104
+ let(:http_error) { Errno::ETIMEDOUT.new }
105
+
106
+ context 'every time' do
107
+ before { expect(Net::HTTP).to receive(:start).at_least(:once).and_raise http_error }
108
+
109
+ it { expect{request.run}.to fail }
110
+ end
111
+
112
+ context 'but works the second time' do
113
+ before { expect(Net::HTTP).to receive(:start).at_least(:once).and_return retry_response }
114
+ before { allow(retry_response).to receive(:body) }
115
+ let(:retry_response) { Net::HTTPOK.new nil, nil, nil }
116
+
117
+ it { expect{request.run}.not_to fail }
118
+ end
119
+ end
120
+
121
+ # NOTE: This test is just a reflection of YouTube irrational behavior of
122
+ # being unavailable once in a while, and therefore causing Net::HTTP to
123
+ # fail, although retrying after some seconds works.
124
+ context 'an Errno::ENETUNREACH' do
125
+ let(:http_error) { Errno::ENETUNREACH.new }
126
+
127
+ context 'every time' do
128
+ before { expect(Net::HTTP).to receive(:start).at_least(:once).and_raise http_error }
129
+
130
+ it { expect{request.run}.to fail }
131
+ end
132
+
133
+ context 'but works the second time' do
134
+ before { expect(Net::HTTP).to receive(:start).at_least(:once).and_return retry_response }
135
+ before { allow(retry_response).to receive(:body) }
136
+ let(:retry_response) { Net::HTTPOK.new nil, nil, nil }
137
+
138
+ it { expect{request.run}.not_to fail }
139
+ end
140
+ end
141
+ end
73
142
  end
74
143
  end
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.6.2
4
+ version: 0.6.3
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 00:00:00.000000000 Z
11
+ date: 2014-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -136,6 +136,7 @@ files:
136
136
  - lib/yt/collections/playlist_items.rb
137
137
  - lib/yt/collections/playlists.rb
138
138
  - lib/yt/collections/ratings.rb
139
+ - lib/yt/collections/reports.rb
139
140
  - lib/yt/collections/snippets.rb
140
141
  - lib/yt/collections/statuses.rb
141
142
  - lib/yt/collections/subscriptions.rb