yt 0.6.2 → 0.6.3

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