googol 0.1.1 → 0.2.0

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: 6945690068a1360419e43033caf75ed53ee0d231
4
- data.tar.gz: 20ad10bdfe492d8bdbbc356c5c2b4d5758b83c07
3
+ metadata.gz: 4cced40105e7959c839fa4349625b98fddb0857b
4
+ data.tar.gz: ba65e22653877a3cf0aadc9d4ef81838ea09879e
5
5
  SHA512:
6
- metadata.gz: 4e674a9f13fa33db3184969ba36b02df88230165fdef7a67679a2622d6a8fc2dd919edd28f16c2fc7f9cef6f0a8f06d7cb56db58828d6a3020c26cb0292ab241
7
- data.tar.gz: 4e73ed288bb6b2fe9ea416cbad761c82fac7e58816b18b3257bc4c18d153390276612606fc886c54f395be40b0a193784580cdbfd23376be95b6e056f74856af
6
+ metadata.gz: 69a0f394c1059aec6a06159f5e042aa7ec1f539a9646c0bbd06ae9b3e2362ffc9f115c10ad1887dcea477b993e069a7c21e007ee29605c0ca760d35f3962652f
7
+ data.tar.gz: 2be89f5a97ce5285ed09bf0fcb900879c437f656a8232c495b9ddfc9b073b80adb03630d375fb2f6d4509d1d5b9580cd647f325b1de3df9e210c3fd87ab4a8fd
data/README.md CHANGED
@@ -31,8 +31,27 @@ account.email #=> 'user@google.com'
31
31
 
32
32
  ```ruby
33
33
  account = Googol::YoutubeAccount.new auth_params
34
- account.perform! :like, :video, 'Kd5M17e7Wek' # => adds 'Tongue' to your 'Liked videos'
35
- account.perform! :subscribe_to, :channel, 'UC7eaRqtonpyiYw0Pns0Au_g' # => subscribes to R.E.M.’s channel
34
+ account.like! video_id: 'Kd5M17e7Wek' # => adds 'Tongue' to your 'Liked videos'
35
+ account.subscribe_to! channel_id: 'UC7eaRqtonpyiYw0Pns0Au_g' # => subscribes to R.E.M.’s channel
36
+ playlist_id = account.find_or_create_playlist_by title: 'Favorite Music Videos'
37
+ account.add_to! playlist_id: playlist_id, video_id: 'Kd5M17e7Wek' # => adds 'Tongue' to your 'Favorite Music Videos' playlist
38
+
39
+ # Adds a video to a playlist as a Youtube account
40
+ #
41
+ # @param [Hash] target The target of the 'add_to' activity
42
+ # @option target [String] :video_id The ID of the video to add
43
+ # @option target [String] :playlist_id The ID of the playlist to add to
44
+ #
45
+ # @see https://developers.google.com/youtube/v3/docs/playlistItems/insert
46
+ #
47
+ def add_to!(target = {})
48
+ video_id, playlist_id = fetch! target, :video_id, :playlist_id
49
+ resource = {videoId: video_id, kind: 'youtube#video'}
50
+ youtube_request! path: '/playlistItems?part=snippet', json: true,
51
+ method: :post, snippet: {playlistId: playlist_id, resourceId: resource}
52
+ end
53
+
54
+
36
55
  ```
37
56
 
38
57
  The full documentation is available at [rubydoc.info](http://rubydoc.info/github/fullscreeninc/googol/master/frames).
@@ -60,10 +79,8 @@ Youtube accounts
60
79
  Use `Googol::YoutubeAccount` to send and retrieve data to Youtube,
61
80
  impersonating an existing Youtube account.
62
81
 
63
- Available instance methods are `id`, `title`, `description`, and `thumbnail_url`.
64
-
65
- Additionally, the `perform!` method lets you executes promotional actions as
66
- a Youtube account, such as liking a video or subscribing to a channel.
82
+ Available instance methods are `id`, `title`, `description`, `thumbnail_url`,
83
+ `like!`, `subscribe_to!`, `find_or_create_playlist_by`, and `add_to!`.
67
84
 
68
85
  These methods require user authentication (see below).
69
86
 
@@ -139,7 +156,7 @@ To install on your system, run
139
156
 
140
157
  To use inside a bundled Ruby project, add this line to the Gemfile:
141
158
 
142
- gem 'googol', '~> 0.1.0'
159
+ gem 'googol', '~> 0.2.0'
143
160
 
144
161
  Since the gem follows [Semantic Versioning](http://semver.org),
145
162
  indicating the full version in your Gemfile (~> *major*.*minor*.*patch*)
@@ -33,10 +33,8 @@ module Googol
33
33
  # * ...
34
34
  def credentials
35
35
  @credentials ||= request! method: :post,
36
- host: 'https://accounts.google.com',
37
- path: '/o/oauth2/token',
38
- body: credentials_params,
39
- valid_if: -> response, body {response.code == '200'}
36
+ host: 'https://accounts.google.com', path: '/o/oauth2/token',
37
+ body: credentials_params
40
38
  end
41
39
 
42
40
  private
@@ -35,11 +35,8 @@ module Googol
35
35
  # * :locale [String] The account’s preferred locale.
36
36
  # * :hd [String] The hosted domain name for the accounts’s Google Apps.
37
37
  def info
38
- @info ||= request! method: :get,
39
- auth: credentials[:access_token],
40
- host: 'https://www.googleapis.com',
41
- path: '/oauth2/v2/userinfo',
42
- valid_if: -> response, body {response.code == '200'}
38
+ @info ||= request! auth: credentials[:access_token],
39
+ host: 'https://www.googleapis.com', path: '/oauth2/v2/userinfo'
43
40
  end
44
41
 
45
42
  # Define a method to return each attribute of the profile separately.
@@ -17,9 +17,17 @@ module Googol
17
17
  info[:snippet][:description]
18
18
  end
19
19
 
20
- # Return the URL of the Youtube object thumbnail
21
- def thumbnail_url
22
- info[:snippet][:thumbnails][:default][:url]
20
+ # Return the URL of the thumbnail image of the Youtube channel/videp.
21
+ #
22
+ # @option size [Symbol] :default The size of the thumbnail. Valid values are:
23
+ # :default (channel: 88px x 88px, video: 120px x 90px)
24
+ # :medium (channel: 240px x 240px, video: 320px x 180px)
25
+ # :high (channel: 800px x 800px, video: 480px x 360px)
26
+ #
27
+ # @return [String] The thumbnail URL
28
+ def thumbnail_url(size = :default)
29
+ size = :default unless [:medium, :high].include? size
30
+ info[:snippet][:thumbnails][size][:url]
23
31
  end
24
32
 
25
33
  # Return the kind of the Youtube object (either 'channel' or 'video')
@@ -19,13 +19,13 @@ module Googol
19
19
  http = Net::HTTP.new url.host, url.port
20
20
  http.use_ssl = true
21
21
  request = case params[:method]
22
- when :get then Net::HTTP::Get.new params[:path]
23
22
  when :post then
24
23
  if params[:json]
25
24
  Net::HTTP::Post.new params[:path], initheader = {'Content-Type' =>'application/json'}
26
25
  else
27
26
  Net::HTTP::Post.new params[:path]
28
27
  end
28
+ else Net::HTTP::Get.new params[:path]
29
29
  end
30
30
  if params[:json]
31
31
  request.body = params[:body].to_json
@@ -34,12 +34,12 @@ module Googol
34
34
  end if params[:body]
35
35
 
36
36
  request['Authorization'] = 'Bearer ' + params[:auth] if params[:auth]
37
+
37
38
  response = http.request(request)
38
39
 
39
40
  body = JSON.parse response.body if response.body
40
41
 
41
- if params[:valid_if] ? params[:valid_if].call(response, body) : true
42
- body = params[:extract].call body if params[:extract]
42
+ if response.code == params.fetch(:code, 200).to_s
43
43
  body ? deep_symbolize_keys(body) : true
44
44
  else
45
45
  raise RequestError, body
@@ -49,10 +49,18 @@ module Googol
49
49
  private
50
50
 
51
51
  def deep_symbolize_keys(hash)
52
+ def symbolize(value)
53
+ case value
54
+ when Hash then deep_symbolize_keys value
55
+ when Array then value.map{|item| symbolize item}
56
+ else value
57
+ end
58
+ end
59
+
52
60
  {}.tap do |result|
53
61
  hash.each do |k, v|
54
62
  key = k.to_sym rescue k
55
- result[key] = v.is_a?(Hash) ? deep_symbolize_keys(v) : v
63
+ result[key] = symbolize v
56
64
  end
57
65
  end
58
66
  end
@@ -1,3 +1,3 @@
1
1
  module Googol
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -14,7 +14,7 @@ module Googol
14
14
  # * Use the authorization code to initialize the YoutubeAccount and like the video:
15
15
  #
16
16
  # account = Googol::YoutubeAccount.new code: code, redirect_url: redirect_url
17
- # account.perform! :like, :video, 'Kd5M17e7Wek' # => likes the video
17
+ # account.like! video_id: 'Kd5M17e7Wek' # => likes the video
18
18
  #
19
19
  class YoutubeAccount
20
20
  include Authenticable
@@ -36,35 +36,156 @@ module Googol
36
36
  # + :medium [Hash] Medium thumbnail URL (88px x 88px)
37
37
  # + :high [Hash] High thumbnail URL (88px x 88px)
38
38
  def info
39
- @info ||= request! method: :get,
40
- auth: credentials[:access_token],
41
- host: 'https://www.googleapis.com',
42
- path: '/youtube/v3/channels?part=id,snippet&mine=true',
43
- valid_if: -> resp, body {resp.code == '200' && body['items'].any?},
44
- extract: -> body {body['items'].first}
39
+ @info_response ||= youtube_request! path: '/channels?part=id,snippet&mine=true'
40
+ @info_response[:items].first
45
41
  end
46
42
 
47
- ## Promote a Youtube target resource on this Youtube Channel
48
- # Note that liking a video does not also subscribe to a channel
49
- def perform!(activity, target_kind, target_id)
50
- params = {}.tap do |params|
51
- params[:method] = :post
52
- params[:auth] = credentials[:access_token]
53
- params[:host] = 'https://www.googleapis.com'
43
+ # Like a video as a Youtube account
44
+ #
45
+ # @param [Hash] target The target of the 'like' activity
46
+ # @option target [String] :video_id The ID of the video to like
47
+ #
48
+ # @see https://developers.google.com/youtube/v3/docs/videos/rate
49
+ #
50
+ # @note Liking a video does not also subscribe to its channel
51
+ #
52
+ def like!(target = {})
53
+ video_id = fetch! target, :video_id
54
+ path = "/videos/rate?rating=like&id=#{video_id}"
55
+ youtube_request! path: path, method: :post, code: 204
56
+ end
57
+
58
+ # Subscribe a Youtube account to a channel
59
+ #
60
+ # @param [Hash] target The target of the 'subscribe' activity
61
+ # @option target [String] :channel_id The ID of the channel to subscribe to
62
+ #
63
+ # @see https://developers.google.com/youtube/v3/docs/subscriptions/insert
64
+ #
65
+ def subscribe_to!(target = {})
66
+ channel_id = fetch! target, :channel_id
67
+ youtube_request! path: '/subscriptions?part=snippet', json: true,
68
+ method: :post, body: {snippet: {resourceId: {channelId: channel_id}}}
69
+ end
70
+
71
+ # Return the first playlist of the Youtube account matching the attributes.
72
+ # If such a playlist is not found, it gets created and yielded to the block.
73
+ #
74
+ # @note Inspired by Ruby on Rails’ find_or_create_by
75
+ #
76
+ # @param [Hash] target The target of the 'add_to' activity
77
+ # @option target [String] :video_id The ID of the video to add
78
+ # @option target [String] :playlist_id The ID of the playlist to add to
79
+ #
80
+ # @see https://developers.google.com/youtube/v3/docs/playlistItems/insert
81
+ #
82
+ def find_or_create_playlist_by(attributes = {}, &block)
83
+ find_playlist_by(attributes) || create_playlist!(attributes, &block)
84
+ end
85
+
86
+ # Adds a video to a playlist as a Youtube account
87
+ #
88
+ # @param [Hash] target The target of the 'add_to' activity
89
+ # @option target [String] :video_id The ID of the video to add
90
+ # @option target [String] :playlist_id The ID of the playlist to add to
91
+ #
92
+ # @see https://developers.google.com/youtube/v3/docs/playlistItems/insert
93
+ #
94
+ def add_to!(target = {})
95
+ video_id, playlist_id = fetch! target, :video_id, :playlist_id
96
+ resource = {videoId: video_id, kind: 'youtube#video'}
97
+ youtube_request! method: :post, json: true,
98
+ path: '/playlistItems?part=snippet',
99
+ body: {snippet: {playlistId: playlist_id, resourceId: resource}}
100
+ end
54
101
 
55
- case [activity.to_sym, target_kind.to_sym]
56
- when [:like, :video]
57
- params[:path] = "/youtube/v3/videos/rate?rating=like&id=#{target_id}"
58
- params[:valid_if] = -> response, body {response.code == '204'}
59
- when [:subscribe_to, :channel]
60
- params[:json] = true
61
- params[:path] = '/youtube/v3/subscriptions?part=snippet'
62
- params[:body] = {snippet: {resourceId: {channelId: target_id}}}
63
- params[:valid_if] = -> response, body {response.code == '200'}
64
- else
65
- raise RequestError, "#{activity} invalid for #{target_kind} #{target_id}"
66
- end
102
+ private
103
+
104
+ # Creates a playlist for a Youtube account and yield it to the block
105
+ #
106
+ # @param [Hash] attributes The attributes of the new playlist
107
+ # @option attributes [String] :title Title of the playlist
108
+ # @option attributes [String] :description Description of the playlist
109
+ #
110
+ # @return [String] The ID of the playlist
111
+ #
112
+ # @see https://developers.google.com/youtube/v3/docs/playlists/insert
113
+ #
114
+ def create_playlist!(attrs = {}, &block)
115
+ snippet = {title: attrs[:title], description: attrs[:description]}
116
+ playlist = youtube_request! json: true, method: :post,
117
+ path: '/playlists?part=snippet,status',
118
+ body: {snippet: snippet, status: {privacyStatus: :public}}
119
+ yield playlist if block_given?
120
+ playlist[:id]
121
+ end
122
+
123
+ # Return the first playlist of the Youtube account matching the attributes.
124
+ #
125
+ # @param [Hash] filters The filter to query the list of playlists with.
126
+ # @option title [String or Regex] Filter on the title
127
+ # @option description [String or Regex] Filter on the description
128
+ #
129
+ # @example account.find_playlist_by title: 'Title' # => 'e45fXDsdsdsd'
130
+ # @example account.find_playlist_by description: 'xxxx' # => nil
131
+ #
132
+ # @note Google API does not have a "search" endpoint, therefore we have
133
+ # to scan the list of playlist page by page, limiting at 10 pages to
134
+ # prevent this function from running forever (50 playlists per page).
135
+ #
136
+ # @return [String or nil] The ID of the playlist (or nil if not found)
137
+ def find_playlist_by(filters = {})
138
+ page = filters.delete(:page) || 1
139
+
140
+ path = "/playlists?part=id,snippet&mine=true"
141
+ path << "&maxResults=#{filters.delete(:max) || 50 }"
142
+ path << "&pageToken=#{filters.delete :token}" if filters[:token]
143
+
144
+ response = youtube_request! path: path
145
+
146
+ if playlist = response[:items].find{|p| playlist_matches? p, filters}
147
+ playlist[:id]
148
+ elsif page < 10 && token = response[:nextPageToken]
149
+ find_playlist_by filters.merge page: page, token: token
67
150
  end
151
+ end
152
+
153
+ # Checks if the playlist matches the given filters.
154
+ #
155
+ # @param [Hash] filters The filter to query the list of playlists with.
156
+ # @option title [String or Regex] Filter on the title
157
+ # @option description [String or Regex] Filter on the description
158
+ #
159
+ # @note String filters match when they are included in the specified field
160
+ # Regex filters match when they match as a Regex against the field
161
+ #
162
+ # @example Given a playlist x with title: 'A title', description: 'Example'
163
+ # playlist_matches? x, title: 'tit' # => true
164
+ # playlist_matches? x, title: 'TIT' # => false
165
+ # playlist_matches? x, title: /^TIT/i # => true
166
+ # playlist_matches? x, title: /^TIT/i, description: 'Ex' # => true
167
+ # playlist_matches? x, title: /^TIT/i, description: 'xxx' # => false
168
+ #
169
+ # @return [Boolean] Whether the playlist matches the filters
170
+ def playlist_matches?(playlist, filters = {})
171
+ filters.all? do |attribute, string_or_regex|
172
+ playlist[:snippet].fetch(attribute, '')[string_or_regex]
173
+ end
174
+ end
175
+
176
+ # Fetch keys from a hash, raising a custom exception when not found
177
+ def fetch!(hash, *keys)
178
+ values = keys.map do |key|
179
+ hash.fetch(key) {raise RequestError, "Missing #{key} in #{hash}"}
180
+ end
181
+ keys.one? ? values.first : values
182
+ end
183
+
184
+ # Overrides the super.request! with Youtube-specific params
185
+ def youtube_request!(params = {})
186
+ params[:path] = "/youtube/v3#{params[:path]}"
187
+ params[:auth] = credentials[:access_token]
188
+ params[:host] = 'https://www.googleapis.com'
68
189
  request! params
69
190
  end
70
191
 
@@ -42,15 +42,16 @@ module Googol
42
42
  # - :description [String] The channel's description.
43
43
  # - :publishedAt [String] The date and time that the channel was created. The value is specified in ISO 8601 (YYYY-MM-DDThh:mm:ss.sZ) format.
44
44
  # - :thumbnails [Hash]
45
- # + :default [Hash] Default thumbnail URL (88px x 88px)
46
- # + :medium [Hash] Medium thumbnail URL (88px x 88px)
47
- # + :high [Hash] High thumbnail URL (88px x 88px)
45
+ # + :default [Hash] Default thumbnail URL (channel: 88px x 88px, video: 120px x 90px)
46
+ # + :medium [Hash] Medium thumbnail URL (channel: 240px x 240px, video: 320px x 180px)
47
+ # + :high [Hash] High thumbnail URL (channel: 800px x 800px, video: 480px x 360px)
48
48
  def info
49
- @info ||= request! method: :get,
50
- host: 'https://www.googleapis.com',
51
- path: "/youtube/v3/#{info_path}",
52
- valid_if: -> resp, body {resp.code == '200' && body['items'].any?},
53
- extract: -> body {body['items'].first}
49
+ @info_response ||= request! host: 'https://www.googleapis.com', path: "/youtube/v3/#{info_path}"
50
+ if @info_response[:items].any?
51
+ @info_response[:items].first
52
+ else
53
+ raise RequestError, "Youtube resource not found at #{@url}"
54
+ end
54
55
  end
55
56
 
56
57
  private
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: googol
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
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-04-09 00:00:00.000000000 Z
11
+ date: 2014-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler