yt-core 0.1.4 → 0.1.5

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: bcf250be4dd09edba92d177ab388d0e6ad9c1c7c
4
- data.tar.gz: 73302bb09f7fa918fb68d040c0c157e4ea171cd4
3
+ metadata.gz: 38fe8ee547f6c51750be3b935de33654304a92bc
4
+ data.tar.gz: ca5b035d73c726d4f9f4a2428b3a4cc6cb573515
5
5
  SHA512:
6
- metadata.gz: 11ad4095e3cba36dac088c13fe60084e3fb3b3a172e83b4a2e0ad893032a86d77afb7e9ffca71145be8a8140b891063eef3b0dbc52f0448a6224fcee5c2e4d58
7
- data.tar.gz: f86ceb9904d7480b89b41a0c4d33d93d9056265381085cd9566a4908b7200de69f869b4317a29eea23163093e0a887537c4b404c663f2135aeef12017b474760
6
+ metadata.gz: 3201de66a86b9df6caf10792426616f01ea177153983090cb32d1bd19f83f99b947b79f9012d4e33f34411930e5f3c978664d6d9c3f7b2c536ccbb4412523a52
7
+ data.tar.gz: 0f1daea896d3ea422837992e8b3f1aeefb04d0576af63e51f783ae00ac5234b45ccf7ce3d77b2fc84578968fc1e69a37befb9ac9d32b3c3cfb7ef2669c0653b1
@@ -1,6 +1,11 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2.2
3
+ - 2.2.2
4
4
  script:
5
- - bundle exec rspec
6
- - bundle exec yard stats | grep "100.00% documented"
5
+ - bundle exec rspec
6
+ - bundle exec yard stats | grep "100.00% documented"
7
+ notifications:
8
+ slack:
9
+ secure: q+C+6nmfAg0W3ZqmqCyV7cRJXoJX22WPHodEYI5WVE7PY2Y42phHOIipXeUFta3CF3VO5sk4MLXh08y0WtShi/ZGpUui0ZH8aimplUlPbbmwKukAGAwYJp7q/trU3hPZd2CkQoBBWNmJsx2aU4X39aYpUOExYrSwW4Cu17hgW1y2m3kxNJyOocuaAuMZyWYY8ktH0C5sVfL0LR7U8Ct8B9D72yyXzLrPOlTWkrln7e82tirh318XqWRM8YLLLGkwaOBqptxkNbmbNu/y+SKiqixcSUPDhLAjRXsByhDlR2f9kZsQZJ/1YnQFl/SygiL39JNMWY8aiHkN15N5iWiocalw/r+oSdZApWf7Ts69cgtoPGxNDt8L73FXs5tnhNOEkm/3kc+Vkq7mnrvX1l0Z0wqdcmZekiw31u30j/j6GU2WHC3C4taJ9TJIc7AfNUntg0l1GXh1aKi62pEdVVgJWLPU8MKXn0Z+2Me2ALrFjfGkfbGXD1lbIlQ/UfyiDyzmqN+DpT7UNcUuXKH4N/BoJtqr9upnXZsVIr0+mVD0/rpo7lZtNHsYaP1BtywKcKdXs4TNHSV51cE3ZUtAweGRsgnQsLWtszQPPG98OLFpqJP5wKRaAuIEI0134uf6V+DqWyICPY6oGX1BTbm8LuXCby7I8ErHI4V16tWuiNm6ojQ=
10
+ on_success: never
11
+ on_failure: change
@@ -6,6 +6,12 @@ For more information about changelogs, check
6
6
  [Keep a Changelog](http://keepachangelog.com) and
7
7
  [Vandamme](http://tech-angels.github.io/vandamme).
8
8
 
9
+ ## 0.1.5 - 2017-08-24
10
+
11
+ * [FEATURE] Add `Yt::PlaylistItem.insert` and `Yt::PlaylistItem#delete`
12
+ * [FEATURE] Add `Channel#related_playlists` and `Channel#like_playlists`
13
+ * [FEATURE] Add Channel.mine
14
+
9
15
  ## 0.1.4 - 2017-06-02
10
16
 
11
17
  * [FEATURE] Add CommentThread#comments
data/README.md CHANGED
@@ -57,11 +57,21 @@ To run live-tests against the YouTube API, type:
57
57
  rspec
58
58
  ```
59
59
 
60
- This will fail unless you have set up a test YouTube application with access to
61
- the YouTube Data API v3 and an environment variable:
60
+ Note that some tests actually hit the YouTube API, and therefore require
61
+ either an API key or authentication credentials.
62
62
 
63
- - `YT_SERVER_API_KEY`: API Key of a Google app with access to the YouTube Data API v3 and the YouTube Analytics API
63
+ In order to run tests marked as :server you need to set up a test YouTube
64
+ application with access to the YouTube Data API v3 and an environment variable:
64
65
 
66
+ - `YT_SERVER_API_KEY`: API Key of a Google app with access to the YouTube Data API v3
67
+
68
+ In order to run tests marked as :account you als need to create a client ID
69
+ and secret and then generate a refresh token for the account you want to use
70
+ as test. Make sure this account has a channel with at least one playlist:
71
+
72
+ - `YT_ACCOUNT_CLIENT_ID`: Client ID of a Google app with access to the YouTube Data API v3
73
+ - `YT_ACCOUNT_CLIENT_SECRET`: Client Secret of a Google app with access to the YouTube Data API v3
74
+ - `YT_ACCOUNT_REFRESH_TOKEN`: Refresh token of a YouTube account for the app above
65
75
 
66
76
  How to release new versions
67
77
  ===========================
@@ -32,6 +32,21 @@ channel = Yt::Channel.new id: 'UCwCnUcLcb9-eSrHa_RQGkQQ' ## use any chan
32
32
  channel.title # => "Yt Test"
33
33
  {% endhighlight %}
34
34
 
35
+
36
+ <p>
37
+ Other methods <strong>acts on behalf of YouTube accounts</strong> (e.g.: subscribe to a channel, delete playlists).<br />
38
+ To use these methods (marked with <span class="label label-warning">&nbsp;</span> below), you need to <a href="{{ site.baseurl }}/#api_client">get an API Client ID/Secret from Google</a>, then <a href="{{ site.baseurl }}/#tokens">obtain a refresh token</a> from the account you want to act as, and finally configure the values:
39
+ </p>
40
+
41
+ {% highlight ruby %}
42
+ Yt.configuration.client_id = "<your ID>" ## replace with your client ID
43
+ Yt.configuration.client_secret = "<your secret>" ## replace with your client secret
44
+ Yt.configuration.refresh_token = "<token>" ## use the account’s refresh token
45
+
46
+ channel = Yt::Channel.mine
47
+ # => #<Yt::Channel @id=UCwCnUcLcb9-eSrHa_RQGkQQ>
48
+ {% endhighlight %}
49
+
35
50
  <hr />
36
51
  <h4>List of <code>Yt::Channel</code> data methods</h4>
37
52
  <dl>
@@ -70,6 +85,12 @@ channel.title # => "Yt Test"
70
85
  {% include doc.html instance="Channel#featured_channels_title" %}{% include example.html object='channel' method='featured_channels_title' result='"Featured channels"' %}
71
86
  {% include doc.html instance="Channel#featured_channels_urls" %}{% include example.html object='channel' method='featured_channels_urls' result='["UCxO1tY8h1AhOz0T4ENwmpow"]' %}</pre>
72
87
  </div></dd>
88
+
89
+ {% include dt.html title="Channel’s content details" label="warning" auth="must authenticate as the channel’s account" %}
90
+ <dd><a class="anchor" id="content_details"></a><div class="highlight"><pre>
91
+ {% include doc.html instance="Channel#related_playlists" %}{% include example.html object='channel' method='related_playlists' result='{"likes"=>"LLwCncb9-e", "watchHistory"=>"HL"}' %}
92
+ {% include doc.html instance="Channel#like_playlists" %}{% include example.html object='channel' method='like_playlists' result='#&lt;Yt::Relation [#&lt;Yt::Playlist @id=LLWCncb...&gt;]&gt;' %}</pre>
93
+ </div></dd>
73
94
  </dl>
74
95
  <p>
75
96
  To limit the number of HTTP requests, use <code>select</code> to specify which <a href="https://developers.google.com/youtube/v3/docs/channels/list#part">parts</a> of the channel’s data to load:
@@ -142,3 +163,11 @@ channel.title # => "Yt Test"
142
163
  The previous method returns existing channels that match the provided IDs, skipping any unrecognized ID.<br />
143
164
  As usual, use <code>select</code> to specify which <a href="https://developers.google.com/youtube/v3/docs/channels/list#part">parts</a> of each channels’s data to load before iterating through the list.
144
165
  </p>
166
+
167
+ <dl>
168
+ {% include dt.html title="Authenticated account’s channel" label="warning" auth="must authenticate as the channel’s account" %}
169
+ <dd><a class="anchor" id="mine"></a><div class="highlight"><pre>
170
+ {% include doc.html class="Channel#mine" %}{% include example.html object='<span class="no">Yt</span><span class="o">::</span><span class="no">Channel</span>' method='mine' %}
171
+ {% include example.html result='#&lt;Yt::Channel @id=UCwCnUcLcb9-eSrHa_RQGkQQ&gt;' %}</pre>
172
+ </div></dd>
173
+ </dl>
@@ -27,6 +27,20 @@ item = Yt::PlaylistItem.new id: 'UEwtTGVUdXRjOUdSS0Qze' ## use any playlist item
27
27
  item.position # => 0
28
28
  {% endhighlight %}
29
29
 
30
+ <p>
31
+ Other methods <strong>acts on behalf of YouTube accounts</strong> (e.g.: add an item to a playlist).<br />
32
+ To use these methods (marked with <span class="label label-warning">&nbsp;</span> below), you need to <a href="{{ site.baseurl }}/#api_client">get an API Client ID/Secret from Google</a>, then <a href="{{ site.baseurl }}/#tokens">obtain a refresh token</a> from the account you want to act as, and finally configure the values:
33
+ </p>
34
+
35
+ {% highlight ruby %}
36
+ Yt.configuration.client_id = "<your ID>" ## replace with your client ID
37
+ Yt.configuration.client_secret = "<your secret>" ## replace with your client secret
38
+ Yt.configuration.refresh_token = "<token>" ## use the account’s refresh token
39
+
40
+ Yt::PlaylistItem.insert playlist_id: 'PL-LeTutc9GRKD3DhnRF_y', video_id: 'gknzFj_0vvY'
41
+ # => #<Yt::PlaylistItem:0x... @id=UEwtTGVUdXRjOUdSS0Qze>
42
+ {% endhighlight %}
43
+
30
44
  <hr />
31
45
  <h4>List of <code>Yt::PlaylistItem</code> data methods</h4>
32
46
  <dl>
@@ -63,3 +77,10 @@ item.position # => 0
63
77
  {% include example.html object='fast' method='privacy_status' result='=> no extra HTTP requests' %}</pre>
64
78
  </div></dd>
65
79
  </dl>
80
+ <dl>
81
+ {% include dt.html title="Adding and removing a playlist item" label="warning" auth="must authenticate as the channel’s account" %}
82
+ <dd><a class="anchor" id="insert_remove"></a><div class="highlight"><pre>
83
+ {% include doc.html class="PlaylistItem#insert" %}{% include example.html object='item = <span class="no">Yt</span><span class="o">::</span><span class="no">PlaylistItem</span>' method='insert' params=' <span class="ss">playlist_id:</span> <span class="s1">"PL-..."</span>, <span class="ss">video_id:</span> <span class="s1">"gknzFj_0vvY"</span>' %}
84
+ {% include doc.html instance="PlaylistItem#delete" %}{% include example.html object='item' method='delete' result='true' %}</pre>
85
+ </div></dd>
86
+ </dl>
@@ -91,6 +91,11 @@ module Yt
91
91
  # channels module.
92
92
  has_attribute :featured_channels_urls, in: %i(branding_settings channel), default: []
93
93
 
94
+ # @!attribute [r] related_playlists
95
+ # @return [Hash] the playlists associated with the channel, such as the
96
+ # channel's uploaded videos, liked videos, and watch history.
97
+ has_attribute :related_playlists, in: :content_details
98
+
94
99
  # @return [String] the canonical form of the channel’s URL.
95
100
  def canonical_url
96
101
  "https://www.youtube.com/channel/#{id}"
@@ -122,7 +127,7 @@ module Yt
122
127
  # @see https://developers.google.com/youtube/v3/docs/search/list#channelId
123
128
  def videos
124
129
  @videos ||= Relation.new(Video, channel_id: id, limit: 500) do |options|
125
- items = fetch '/youtube/v3/search', channel_videos_params(options)
130
+ items = get '/youtube/v3/search', channel_videos_params(options)
126
131
  videos_for items, 'id', options
127
132
  end
128
133
  end
@@ -130,8 +135,31 @@ module Yt
130
135
  # @return [Yt::Relation<Yt::Playlist>] the public playlists of the channel.
131
136
  def playlists
132
137
  @playlists ||= Relation.new(Playlist, channel_id: id) do |options|
133
- fetch '/youtube/v3/playlists', channel_playlists_params(options)
138
+ get '/youtube/v3/playlists', channel_playlists_params(options)
134
139
  end
135
140
  end
141
+
142
+ # @return [Yt::Relation<Yt::Playlist>] the playlists associated with
143
+ # liked videos. Includes the deprecated favorites if still present.
144
+ def like_playlists
145
+ @like_lists ||= Relation.new(Playlist, ids: like_list_ids) do |options|
146
+ get '/youtube/v3/playlists', resource_params(options)
147
+ end
148
+ end
149
+
150
+ # @return [Yt::Channel] the channel associated with the YouTube account
151
+ # that provided the authentication token.
152
+ def self.mine
153
+ Relation.new(self) do |options|
154
+ get '/youtube/v3/channels', mine: true, part: 'id'
155
+ end.first
156
+ end
157
+
158
+ private
159
+
160
+ def like_list_ids
161
+ names = %w(likes favorites)
162
+ related_playlists.select{|name,_| names.include? name}.values
163
+ end
136
164
  end
137
165
  end
@@ -20,7 +20,7 @@ module Yt
20
20
  def comments
21
21
  @comments ||= Relation.new(Comment, parent_id: id,
22
22
  initial_items: -> {[top_level_comment]}) do |options|
23
- fetch '/youtube/v3/comments', thread_comments_params(options)
23
+ get '/youtube/v3/comments', thread_comments_params(options)
24
24
  end
25
25
  end
26
26
  end
@@ -1,6 +1,7 @@
1
1
  require 'json' # for JSON.parse
2
2
 
3
3
  require 'yt/config'
4
+ require 'yt/auth'
4
5
  require 'yt/no_items_error'
5
6
  require 'yt/http_request'
6
7
  require 'yt/relation'
@@ -3,6 +3,6 @@ module Yt
3
3
  module Core
4
4
  # @return [String] the SemVer-compatible gem version.
5
5
  # @see http://semver.org
6
- VERSION = '0.1.4'
6
+ VERSION = '0.1.5'
7
7
  end
8
8
  end
@@ -59,7 +59,7 @@ module Yt
59
59
  # @return [Yt::Relation<Yt::PlaylistItem>] the items of the playlist.
60
60
  def items
61
61
  @items ||= Relation.new(PlaylistItem, playlist_id: id) do |options|
62
- fetch '/youtube/v3/playlistItems', playlist_items_params(options)
62
+ get '/youtube/v3/playlistItems', playlist_items_params(options)
63
63
  end
64
64
  end
65
65
 
@@ -67,7 +67,7 @@ module Yt
67
67
  def videos
68
68
  @videos ||= Relation.new(Video, playlist_id: id) do |options|
69
69
  params = playlist_items_params(options.merge parts: [:content_details])
70
- items = fetch '/youtube/v3/playlistItems', params
70
+ items = get '/youtube/v3/playlistItems', params
71
71
  videos_for items, 'contentDetails', options
72
72
  end
73
73
  end
@@ -55,5 +55,27 @@ module Yt
55
55
  def thumbnail_url(size = :default)
56
56
  thumbnails.fetch(size.to_s, {})['url']
57
57
  end
58
+
59
+ # @return [Yt::PlaylistItem] the item created by appending the given
60
+ # video to the given playlist.
61
+ def self.insert(playlist_id:, video_id:)
62
+ parts = %i(id snippet)
63
+ items = -> (body) { [body] } # the response body only includes one item
64
+ resource_id = {kind: 'youtube#video', videoId: video_id}
65
+ snippet = {playlistId: playlist_id, resourceId: resource_id}
66
+
67
+ Relation.new(self, parts: parts, extract_items: items) do |options|
68
+ post '/youtube/v3/playlistItems', {part: 'snippet'}, {snippet: snippet}
69
+ end.first
70
+ end
71
+
72
+ # @return [Boolean] whether the item was removed from the playlist.
73
+ def delete
74
+ items = -> (body) { [{}] } # the response body is empty
75
+
76
+ Relation.new(PlaylistItem, id: id, extract_items: items) do |options|
77
+ delete '/youtube/v3/playlistItems', id: options[:id]
78
+ end.any?
79
+ end
58
80
  end
59
81
  end
@@ -9,7 +9,7 @@ module Yt
9
9
  # @yield [Hash] the options to change which items to iterate through.
10
10
  def initialize(item_class, options = {}, &item_block)
11
11
  @options = {parts: %i(id), limit: Float::INFINITY, item_class: item_class,
12
- initial_items: -> {[]}}
12
+ initial_items: -> {[]}, extract_items: -> (body) {body['items']}}
13
13
  @options.merge! options
14
14
  @item_block = item_block
15
15
  end
@@ -27,10 +27,10 @@ module Yt
27
27
  @items ||= initial_items.dup
28
28
  if @items[@last_index].nil? && more_pages?
29
29
  response = Response.new(@options, &@item_block).run
30
- more_items = response.body['items'].map do |item|
30
+ more_items = @options[:extract_items].call(response.body).map do |item|
31
31
  @options[:item_class].new attributes_for_new_item(item)
32
32
  end
33
- @options.merge! offset: response.body['nextPageToken']
33
+ @options.merge! offset: response.body['nextPageToken'] if response.body
34
34
  @items.concat more_items
35
35
  end
36
36
  @items[(@last_index +=1) -1]
@@ -34,7 +34,7 @@ module Yt
34
34
  def self.where(conditions = {})
35
35
  @where ||= Relation.new(self) do |options|
36
36
  slicing_conditions_every(50) do |slice_options|
37
- fetch resources_path, where_params(slice_options)
37
+ get resources_path, where_params(slice_options)
38
38
  end
39
39
  end
40
40
  @where.where conditions
@@ -50,7 +50,7 @@ module Yt
50
50
  define_method name do
51
51
  keys = Array(options[:in]) + [name]
52
52
  part = keys.shift
53
- value = @data[part] || fetch_part(part)
53
+ value = @data[part] || get_part(part)
54
54
  keys.each{|key| value = value[camelize key]}
55
55
  if value.nil? && options[:default]
56
56
  value = options[:default]
@@ -60,9 +60,9 @@ module Yt
60
60
  end
61
61
  end
62
62
 
63
- def fetch_part(required_part)
63
+ def get_part(required_part)
64
64
  resources = Relation.new(self.class, ids: [id]) do |options|
65
- fetch resources_path, resource_params(options)
65
+ get resources_path, resource_params(options)
66
66
  end
67
67
 
68
68
  parts = (@selected_data_parts + [required_part]).uniq
@@ -12,8 +12,47 @@ module Yt
12
12
 
13
13
  private
14
14
 
15
- def fetch(path, params)
16
- HTTPRequest.new(path: path, params: params).run
15
+ def get(path, params = {})
16
+ request :get, path: path, params: params
17
+ end
18
+
19
+ def post(path, params = {}, body = {})
20
+ request :post, path: path, params: params, body: body
21
+ end
22
+
23
+ def delete(path, params = {})
24
+ request :delete, path: path, params: params
25
+ end
26
+
27
+ def request(method, options = {})
28
+ HTTPRequest.new(request_options options.merge method: method).run
29
+ rescue HTTPError => error
30
+ if unauthorized?(error) && refresh_access_token
31
+ retry
32
+ else
33
+ raise
34
+ end
35
+ end
36
+
37
+ def request_options(options)
38
+ options[:error_message] = ->(body) {JSON(body)['error']['message']}
39
+ if access_token = Yt.configuration.access_token || refresh_access_token
40
+ options[:headers] = {'Authorization' => "Bearer #{access_token}"}
41
+ else
42
+ options[:params] = options[:params].merge key: Yt.configuration.api_key
43
+ end
44
+ options
45
+ end
46
+
47
+ def unauthorized?(error)
48
+ error.response.is_a? Net::HTTPUnauthorized
49
+ end
50
+
51
+ def refresh_access_token
52
+ if Yt.configuration.refresh_token
53
+ auth = Auth.new refresh_token: Yt.configuration.refresh_token
54
+ Yt.configuration.access_token = auth.access_token
55
+ end
17
56
  end
18
57
 
19
58
  def resources_path
@@ -55,7 +94,6 @@ module Yt
55
94
  def default_params(options)
56
95
  {}.tap do |params|
57
96
  params[:max_results] = 50
58
- params[:key] = Yt.configuration.api_key
59
97
  params[:part] = options[:parts].join ','
60
98
  params[:page_token] = options[:offset]
61
99
  end
@@ -90,7 +128,7 @@ module Yt
90
128
  else
91
129
  options[:ids] = items.body['items'].map{|item| item['id']}
92
130
  options[:offset] = nil
93
- fetch('/youtube/v3/videos', resource_params(options)).tap do |response|
131
+ get('/youtube/v3/videos', resource_params(options)).tap do |response|
94
132
  response.body['nextPageToken'] = items.body['nextPageToken']
95
133
  end
96
134
  end
@@ -183,7 +183,7 @@ module Yt
183
183
  # @return [Yt::Relation<Yt::CommentThread>] the threads of the video.
184
184
  def threads
185
185
  @threads ||= Relation.new(CommentThread, video_id: id) do |options|
186
- fetch '/youtube/v3/commentThreads', video_threads_params(options)
186
+ get '/youtube/v3/commentThreads', video_threads_params(options)
187
187
  end
188
188
  end
189
189
 
@@ -23,7 +23,8 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ['lib']
25
25
 
26
- spec.add_dependency 'yt-support', '>= 0.1.2'
26
+ spec.add_dependency 'yt-auth', '>= 0.2.3'
27
+ spec.add_dependency 'yt-support', '>= 0.1.3'
27
28
 
28
29
  spec.add_development_dependency 'bundler', '~> 1.14'
29
30
  spec.add_development_dependency 'rspec', '~> 3.5'
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yt-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-06-06 00:00:00.000000000 Z
11
+ date: 2017-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: yt-auth
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.3
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: yt-support
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - ">="
18
32
  - !ruby/object:Gem::Version
19
- version: 0.1.2
33
+ version: 0.1.3
20
34
  type: :runtime
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - ">="
25
39
  - !ruby/object:Gem::Version
26
- version: 0.1.2
40
+ version: 0.1.3
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement