vibedeck-youtube_it 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/README.rdoc +234 -0
  2. data/Rakefile +35 -0
  3. data/lib/youtube_it/chain_io.rb +76 -0
  4. data/lib/youtube_it/client.rb +367 -0
  5. data/lib/youtube_it/middleware/faraday_authheader.rb +24 -0
  6. data/lib/youtube_it/middleware/faraday_oauth.rb +21 -0
  7. data/lib/youtube_it/middleware/faraday_youtubeit.rb +30 -0
  8. data/lib/youtube_it/model/author.rb +13 -0
  9. data/lib/youtube_it/model/category.rb +11 -0
  10. data/lib/youtube_it/model/comment.rb +16 -0
  11. data/lib/youtube_it/model/contact.rb +16 -0
  12. data/lib/youtube_it/model/content.rb +18 -0
  13. data/lib/youtube_it/model/playlist.rb +11 -0
  14. data/lib/youtube_it/model/rating.rb +23 -0
  15. data/lib/youtube_it/model/subscription.rb +7 -0
  16. data/lib/youtube_it/model/thumbnail.rb +17 -0
  17. data/lib/youtube_it/model/user.rb +26 -0
  18. data/lib/youtube_it/model/video.rb +225 -0
  19. data/lib/youtube_it/parser.rb +357 -0
  20. data/lib/youtube_it/record.rb +12 -0
  21. data/lib/youtube_it/request/base_search.rb +72 -0
  22. data/lib/youtube_it/request/error.rb +15 -0
  23. data/lib/youtube_it/request/standard_search.rb +43 -0
  24. data/lib/youtube_it/request/user_search.rb +47 -0
  25. data/lib/youtube_it/request/video_search.rb +102 -0
  26. data/lib/youtube_it/request/video_upload.rb +415 -0
  27. data/lib/youtube_it/response/video_search.rb +41 -0
  28. data/lib/youtube_it/version.rb +4 -0
  29. data/lib/youtube_it.rb +75 -0
  30. data/test/helper.rb +10 -0
  31. data/test/test_chain_io.rb +63 -0
  32. data/test/test_client.rb +418 -0
  33. data/test/test_field_search.rb +48 -0
  34. data/test/test_video.rb +43 -0
  35. data/test/test_video_feed_parser.rb +271 -0
  36. data/test/test_video_search.rb +141 -0
  37. metadata +150 -0
@@ -0,0 +1,367 @@
1
+ class YouTubeIt
2
+ class Client
3
+ include YouTubeIt::Logging
4
+ # Previously this was a logger instance but we now do it globally
5
+
6
+ def initialize *params
7
+ if params.first.is_a?(Hash)
8
+ hash_options = params.first
9
+ @user = hash_options[:username]
10
+ @pass = hash_options[:password]
11
+ @dev_key = hash_options[:dev_key]
12
+ @client_id = hash_options[:client_id] || "youtube_it"
13
+ @legacy_debug_flag = hash_options[:debug]
14
+ elsif params.first
15
+ puts "* warning: the method YouTubeIt::Client.new(user, passwd, dev_key) is deprecated, use YouTubeIt::Client.new(:username => 'user', :password => 'passwd', :dev_key => 'dev_key')"
16
+ @user = params.shift
17
+ @pass = params.shift
18
+ @dev_key = params.shift
19
+ @client_id = params.shift || "youtube_it"
20
+ @legacy_debug_flag = params.shift
21
+ end
22
+ end
23
+
24
+ # Retrieves an array of standard feed, custom query, or user videos.
25
+ #
26
+ # === Parameters
27
+ # If fetching videos for a standard feed:
28
+ # params<Symbol>:: Accepts a symbol of :top_rated, :top_favorites, :most_viewed,
29
+ # :most_popular, :most_recent, :most_discussed, :most_linked,
30
+ # :most_responded, :recently_featured, and :watch_on_mobile.
31
+ #
32
+ # You can find out more specific information about what each standard feed provides
33
+ # by visiting: http://code.google.com/apis/youtube/reference.html#Standard_feeds
34
+ #
35
+ # options<Hash> (optional):: Accepts the options of :time, :page (default is 1),
36
+ # and :per_page (default is 25). :offset and :max_results
37
+ # can also be passed for a custom offset.
38
+ #
39
+ # If fetching videos by tags, categories, query:
40
+ # params<Hash>:: Accepts the keys :tags, :categories, :query, :order_by,
41
+ # :author, :racy, :response_format, :video_format, :page (default is 1),
42
+ # and :per_page(default is 25)
43
+ #
44
+ # options<Hash>:: Not used. (Optional)
45
+ #
46
+ # If fetching videos for a particular user:
47
+ # params<Hash>:: Key of :user with a value of the username.
48
+ # options<Hash>:: Not used. (Optional)
49
+ # === Returns
50
+ # YouTubeIt::Response::VideoSearch
51
+ def videos_by(params, options={})
52
+ request_params = params.respond_to?(:to_hash) ? params : options
53
+ request_params[:page] = integer_or_default(request_params[:page], 1)
54
+
55
+ request_params[:dev_key] = @dev_key if @dev_key
56
+
57
+ unless request_params[:max_results]
58
+ request_params[:max_results] = integer_or_default(request_params[:per_page], 25)
59
+ end
60
+
61
+ unless request_params[:offset]
62
+ request_params[:offset] = calculate_offset(request_params[:page], request_params[:max_results] )
63
+ end
64
+
65
+ if params.respond_to?(:to_hash) and not params[:user]
66
+ request = YouTubeIt::Request::VideoSearch.new(request_params)
67
+ elsif (params.respond_to?(:to_hash) && params[:user]) || (params == :favorites)
68
+ request = YouTubeIt::Request::UserSearch.new(params, request_params)
69
+ else
70
+ request = YouTubeIt::Request::StandardSearch.new(params, request_params)
71
+ end
72
+
73
+ logger.debug "Submitting request [url=#{request.url}]." if @legacy_debug_flag
74
+ parser = YouTubeIt::Parser::VideosFeedParser.new(request.url)
75
+ parser.parse
76
+ end
77
+
78
+ # Retrieves a single YouTube video.
79
+ #
80
+ # === Parameters
81
+ # vid<String>:: The ID or URL of the video that you'd like to retrieve.
82
+ # user<String>:: The user that uploaded the video that you'd like to retrieve.
83
+ #
84
+ # === Returns
85
+ # YouTubeIt::Model::Video
86
+ def video_by(vid)
87
+ video_id = vid =~ /^http/ ? vid : "http://gdata.youtube.com/feeds/api/videos/#{vid}?v=2#{@dev_key ? '&key='+@dev_key : ''}"
88
+ parser = YouTubeIt::Parser::VideoFeedParser.new(video_id)
89
+ parser.parse
90
+ end
91
+
92
+ def video_by_user(user, vid)
93
+ video_id = "http://gdata.youtube.com/feeds/api/users/#{user}/uploads/#{vid}?v=2#{@dev_key ? '&key='+@dev_key : ''}"
94
+ parser = YouTubeIt::Parser::VideoFeedParser.new(video_id)
95
+ parser.parse
96
+ end
97
+
98
+ def video_upload(data, opts = {})
99
+ client.upload(data, opts)
100
+ end
101
+
102
+ def video_update(video_id, opts = {})
103
+ client.update(video_id, opts)
104
+ end
105
+
106
+ def video_delete(video_id)
107
+ client.delete(video_id)
108
+ end
109
+
110
+ def upload_token(options, nexturl = "http://www.youtube.com/my_videos")
111
+ client.get_upload_token(options, nexturl)
112
+ end
113
+
114
+ def add_comment(video_id, comment)
115
+ client.add_comment(video_id, comment)
116
+ end
117
+
118
+ # opts is converted to get params and appended to comments gdata api url
119
+ # eg opts = { 'max-results' => 10, 'start-index' => 20 }
120
+ # hash does _not_ play nice with symbols
121
+ def comments(video_id, opts = {})
122
+ client.comments(video_id, opts)
123
+ end
124
+
125
+ def add_favorite(video_id)
126
+ client.add_favorite(video_id)
127
+ end
128
+
129
+ def delete_favorite(video_id)
130
+ client.delete_favorite(video_id)
131
+ end
132
+
133
+ def favorites(user = nil, opts = {})
134
+ client.favorites(user, opts)
135
+ end
136
+
137
+ def profile(user = nil)
138
+ client.profile(user)
139
+ end
140
+
141
+ def playlist(playlist_id)
142
+ client.playlist playlist_id
143
+ end
144
+
145
+ def playlists(user = nil)
146
+ client.playlists(user)
147
+ end
148
+
149
+ def add_playlist(options)
150
+ client.add_playlist(options)
151
+ end
152
+
153
+ def update_playlist(playlist_id, options)
154
+ client.update_playlist(playlist_id, options)
155
+ end
156
+
157
+ def add_video_to_playlist(playlist_id, video_id)
158
+ client.add_video_to_playlist(playlist_id, video_id)
159
+ end
160
+
161
+ def delete_video_from_playlist(playlist_id, playlist_entry_id)
162
+ client.delete_video_from_playlist(playlist_id, playlist_entry_id)
163
+ end
164
+
165
+ def delete_playlist(playlist_id)
166
+ client.delete_playlist(playlist_id)
167
+ end
168
+
169
+ def like_video(video_id)
170
+ client.rate_video(video_id, 'like')
171
+ end
172
+
173
+ def dislike_video(video_id)
174
+ client.rate_video(video_id, 'dislike')
175
+ end
176
+
177
+ def subscribe_channel(channel_name)
178
+ client.subscribe_channel(channel_name)
179
+ end
180
+
181
+ def unsubscribe_channel(subscription_id)
182
+ client.unsubscribe_channel(subscription_id)
183
+ end
184
+
185
+ def subscriptions(user_id = nil)
186
+ client.subscriptions(user_id)
187
+ end
188
+
189
+ def enable_http_debugging
190
+ client.enable_http_debugging
191
+ end
192
+
193
+ def current_user
194
+ client.get_current_user
195
+ end
196
+
197
+ # Gets the authenticated users video with the given ID. It may be private.
198
+ def my_video(video_id)
199
+ client.get_my_video(video_id)
200
+ end
201
+
202
+ # Gets all videos
203
+ def my_videos(opts = {})
204
+ client.get_my_videos(opts)
205
+ end
206
+
207
+ private
208
+
209
+ def client
210
+ @client ||= YouTubeIt::Upload::VideoUpload.new(:username => @user, :password => @pass, :dev_key => @dev_key)
211
+ end
212
+
213
+ def calculate_offset(page, per_page)
214
+ page == 1 ? 1 : ((per_page * page) - per_page + 1)
215
+ end
216
+
217
+ def integer_or_default(value, default)
218
+ value = value.to_i
219
+ value > 0 ? value : default
220
+ end
221
+ end
222
+
223
+ class AuthSubClient < Client
224
+ def initialize *params
225
+ if params.first.is_a?(Hash)
226
+ hash_options = params.first
227
+ @authsub_token = hash_options[:token]
228
+ @dev_key = hash_options[:dev_key]
229
+ @client_id = hash_options[:client_id] || "youtube_it"
230
+ @legacy_debug_flag = hash_options[:debug]
231
+ else
232
+ puts "* warning: the method YouTubeIt::AuthSubClient.new(token, dev_key) is depricated, use YouTubeIt::AuthSubClient.new(:token => 'token', :dev_key => 'dev_key')"
233
+ @authsub_token = params.shift
234
+ @dev_key = params.shift
235
+ @client_id = params.shift || "youtube_it"
236
+ @legacy_debug_flag = params.shift
237
+ end
238
+ end
239
+
240
+ def create_session_token
241
+ response = nil
242
+ session_token_url = "/accounts/AuthSubSessionToken"
243
+
244
+ http_connection do |session|
245
+ response = session.get2('https://%s' % session_token_url,session_token_header).body
246
+ end
247
+ @authsub_token = response.sub('Token=','')
248
+ end
249
+
250
+ def revoke_session_token
251
+ response = nil
252
+ session_token_url = "/accounts/AuthSubRevokeToken"
253
+
254
+ http_connection do |session|
255
+ response = session.get2('https://%s' % session_token_url,session_token_header).code
256
+ end
257
+ response.to_s == '200' ? true : false
258
+ end
259
+
260
+ def session_token_info
261
+ response = nil
262
+ session_token_url = "/accounts/AuthSubTokenInfo"
263
+
264
+ http_connection do |session|
265
+ response = session.get2('https://%s' % session_token_url,session_token_header)
266
+ end
267
+ {:code => response.code, :body => response.body }
268
+ end
269
+
270
+ private
271
+ def client
272
+ @client ||= YouTubeIt::Upload::VideoUpload.new(:dev_key => @dev_key, :authsub_token => @authsub_token)
273
+ end
274
+
275
+ def session_token_header
276
+ {
277
+ "Content-Type" => "application/x-www-form-urlencoded",
278
+ "Authorization" => "AuthSub token=#{@authsub_token}"
279
+ }
280
+ end
281
+
282
+ def http_connection
283
+ http = Net::HTTP.new("www.google.com")
284
+ http.set_debug_output(logger) if @http_debugging
285
+ http.start do |session|
286
+ yield(session)
287
+ end
288
+ end
289
+ end
290
+
291
+ class OAuthClient < Client
292
+ def initialize *params
293
+ if params.first.is_a?(Hash)
294
+ hash_options = params.first
295
+ @consumer_key = hash_options[:consumer_key]
296
+ @consumer_secret = hash_options[:consumer_secret]
297
+ @user = hash_options[:username]
298
+ @dev_key = hash_options[:dev_key]
299
+ @client_id = hash_options[:client_id] || "youtube_it"
300
+ @legacy_debug_flag = hash_options[:debug]
301
+ else
302
+ puts "* warning: the method YouTubeIt::OAuthClient.new(consumer_key, consumer_secrect, dev_key) is depricated, use YouTubeIt::OAuthClient.new(:consumer_key => 'consumer key', :consumer_secret => 'consumer secret', :dev_key => 'dev_key')"
303
+ @consumer_key = params.shift
304
+ @consumer_secret = params.shift
305
+ @dev_key = params.shift
306
+ @user = params.shift
307
+ @client_id = params.shift || "youtube_it"
308
+ @legacy_debug_flag = params.shift
309
+ end
310
+ end
311
+
312
+ def consumer
313
+ @consumer ||= ::OAuth::Consumer.new(@consumer_key,@consumer_secret,{
314
+ :site=>"https://www.google.com",
315
+ :request_token_path=>"/accounts/OAuthGetRequestToken",
316
+ :authorize_path=>"/accounts/OAuthAuthorizeToken",
317
+ :access_token_path=>"/accounts/OAuthGetAccessToken"})
318
+ end
319
+
320
+ def request_token(callback)
321
+ @request_token = consumer.get_request_token({:oauth_callback => callback},{:scope => "http://gdata.youtube.com"})
322
+ end
323
+
324
+ def access_token
325
+ @access_token = ::OAuth::AccessToken.new(consumer, @atoken, @asecret)
326
+ end
327
+
328
+ def config_token
329
+ {
330
+ :consumer_key => @consumer_key,
331
+ :consumer_secret => @consumer_secret,
332
+ :token => @atoken,
333
+ :token_secret => @asecret
334
+ }
335
+ end
336
+
337
+ def authorize_from_request(rtoken,rsecret,verifier)
338
+ request_token = ::OAuth::RequestToken.new(consumer,rtoken,rsecret)
339
+ access_token = request_token.get_access_token({:oauth_verifier => verifier})
340
+ @atoken,@asecret = access_token.token, access_token.secret
341
+ end
342
+
343
+ def authorize_from_access(atoken,asecret)
344
+ @atoken,@asecret = atoken, asecret
345
+ end
346
+
347
+ def current_user
348
+ yt_session = Faraday.new(:url => "http://gdata.youtube.com") do |builder|
349
+ builder.use Faraday::Response::YouTubeIt
350
+ builder.use Faraday::Request::OAuth, config_token
351
+ builder.adapter Faraday.default_adapter
352
+ end
353
+
354
+ body = yt_session.get("/feeds/api/users/default").body
355
+ REXML::Document.new(body).elements["entry"].elements['author'].elements['name'].text
356
+ end
357
+
358
+ private
359
+
360
+ def client
361
+ # IMPORTANT: make sure authorize_from_access is called before client is fetched
362
+ @client ||= YouTubeIt::Upload::VideoUpload.new(:username => current_user, :dev_key => @dev_key, :access_token => access_token, :config_token => config_token)
363
+ end
364
+
365
+ end
366
+ end
367
+
@@ -0,0 +1,24 @@
1
+ module Faraday
2
+ class Request::AuthHeader < Faraday::Middleware
3
+
4
+ def call(env)
5
+ req_headers = env[:request_headers]
6
+ req_headers.merge!(@headers)
7
+ unless req_headers.include?("GData-Version")
8
+ req_headers.merge!("GData-Version" => "2")
9
+ end
10
+ unless req_headers.include?("Content-Type")
11
+ req_headers.merge!("Content-Type" => "application/atom+xml; charset=UTF-8")
12
+ end
13
+ unless req_headers.include?("Content-Length")
14
+ req_headers.merge!("Content-Length" => env[:body] ? "#{env[:body].length}" : "0")
15
+ end
16
+
17
+ @app.call(env)
18
+ end
19
+
20
+ def initialize(app, headers)
21
+ @app, @headers = app, headers
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ module Faraday
2
+ class Request::OAuth < Faraday::Middleware
3
+ dependency 'simple_oauth'
4
+
5
+ def call(env)
6
+ params = env[:body].is_a?(Hash) ? env[:body] : {}
7
+
8
+ signature_params = params.reject{ |k,v| v.respond_to?(:content_type) }
9
+
10
+ header = SimpleOAuth::Header.new(env[:method], env[:url], signature_params, @options)
11
+
12
+ env[:request_headers]['Authorization'] = header.to_s
13
+
14
+ @app.call(env)
15
+ end
16
+
17
+ def initialize(app, options)
18
+ @app, @options = app, options
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ module Faraday
2
+ class Response::YouTubeIt < Response::Middleware
3
+ def parse_upload_error_from(string)
4
+ begin
5
+ REXML::Document.new(string).elements["//errors"].inject('') do | all_faults, error|
6
+ if error.elements["internalReason"]
7
+ msg_error = error.elements["internalReason"].text
8
+ elsif error.elements["location"]
9
+ msg_error = error.elements["location"].text[/media:group\/media:(.*)\/text\(\)/,1]
10
+ else
11
+ msg_error = "Unspecified error"
12
+ end
13
+ code = error.elements["code"].text if error.elements["code"]
14
+ all_faults + sprintf("%s: %s\n", msg_error, code)
15
+ end
16
+ rescue
17
+ string[/<TITLE>(.+)<\/TITLE>/, 1] || string
18
+ end
19
+ end
20
+
21
+ def on_complete(env) #this method is called after finish request
22
+ msg = parse_upload_error_from(env[:body].gsub(/\n/, ''))
23
+ if env[:status] == 403 || env[:status] == 401
24
+ raise ::AuthenticationError.new(msg, env[:status])
25
+ elsif env[:status] / 10 != 20
26
+ raise ::UploadError.new(msg, env[:status])
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ class YouTubeIt
2
+ module Model
3
+ class Author < YouTubeIt::Record
4
+ # *String*: Author's YouTube username.
5
+ attr_reader :name
6
+
7
+ # *String*: Feed URL of the author.
8
+ attr_reader :uri
9
+
10
+ attr_reader :thumbnail_url
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ class YouTubeIt
2
+ module Model
3
+ class Category < YouTubeIt::Record
4
+ # *String*:: Name of the YouTube category
5
+ attr_reader :label
6
+
7
+ # *String*:: Identifies the type of item described.
8
+ attr_reader :term
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ class YouTubeIt
2
+ module Model
3
+ class Comment < YouTubeIt::Record
4
+ attr_reader :content, :published, :title, :updated, :url
5
+
6
+ # YouTubeIt::Model::Author:: Information about the YouTube user who owns a piece of video content.
7
+ attr_reader :author
8
+
9
+ # unique ID of the comment.
10
+ def unique_id
11
+ url.split("/").last
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,16 @@
1
+ class YouTubeIt
2
+ module Model
3
+ class Contact < YouTubeIt::Record
4
+ # *String*:: Identifies the status of a contact.
5
+ #
6
+ # * The tag's value will be accepted if the authenticated user and the contact have marked each other as friends.
7
+ # * The tag's value will be requested if the contact has asked to be added to the authenticated user's contact list, but the request has not yet been accepted (or rejected).
8
+ # * The tag's value will be pending if the authenticated user has asked to be added to the contact's contact list, but the request has not yet been accepted or rejected.
9
+ #
10
+ attr_reader :status
11
+
12
+ # *String*:: The Youtube username of the contact.
13
+ attr_reader :username
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ class YouTubeIt
2
+ module Model
3
+ class Content < YouTubeIt::Record
4
+ # *Boolean*:: Description of the video.
5
+ attr_reader :default
6
+ # *Fixnum*:: Length of the video in seconds.
7
+ attr_reader :duration
8
+ # YouTubeIt::Model::Video::Format:: Specifies the video format of the video object
9
+ attr_reader :format
10
+ # *String*:: Specifies the MIME type of the media object.
11
+ attr_reader :mime_type
12
+ # *String*:: Specifies the URL for the media object.
13
+ attr_reader :url
14
+
15
+ alias :is_default? :default
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ class YouTubeIt
2
+ module Model
3
+ class Playlist < YouTubeIt::Record
4
+ attr_reader :title, :description, :summary, :playlist_id, :xml, :published, :response_code
5
+ def videos
6
+ YouTubeIt::Parser::VideosFeedParser.new("http://gdata.youtube.com/feeds/api/playlists/#{playlist_id}?v=2").parse_videos
7
+ end
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,23 @@
1
+ class YouTubeIt
2
+ module Model
3
+ class Rating < YouTubeIt::Record
4
+ # *Float*:: Average rating given to the video
5
+ attr_reader :average
6
+
7
+ # *Fixnum*:: Maximum rating that can be assigned to the video
8
+ attr_reader :max
9
+
10
+ # *Fixnum*:: Minimum rating that can be assigned to the video
11
+ attr_reader :min
12
+
13
+ # *Fixnum*:: Indicates how many people have rated the video
14
+ attr_reader :rater_count
15
+
16
+ # *Fixnum*:: Indicates how many people likes this video
17
+ attr_reader :likes
18
+
19
+ # *Fixnum*:: Indicates how many people dislikes this video
20
+ attr_reader :dislikes
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ class YouTubeIt
2
+ module Model
3
+ class Subscription < YouTubeIt::Record
4
+ attr_reader :id, :title, :published
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ class YouTubeIt
2
+ module Model
3
+ class Thumbnail < YouTubeIt::Record
4
+ # *String*:: URL for the thumbnail image.
5
+ attr_reader :url
6
+
7
+ # *Fixnum*:: Height of the thumbnail image.
8
+ attr_reader :height
9
+
10
+ # *Fixnum*:: Width of the thumbnail image.
11
+ attr_reader :width
12
+
13
+ # *String*:: Specifies the time offset at which the frame shown in the thumbnail image appears in the video.
14
+ attr_reader :time
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ class YouTubeIt
2
+ module Model
3
+ class User < YouTubeIt::Record
4
+ attr_reader :age
5
+ attr_reader :books
6
+ attr_reader :company
7
+ attr_reader :description
8
+ attr_reader :gender
9
+ attr_reader :hobbies
10
+ attr_reader :hometown
11
+ attr_reader :last_login
12
+ attr_reader :location
13
+ attr_reader :join_date
14
+ attr_reader :movies
15
+ attr_reader :music
16
+ attr_reader :occupation
17
+ attr_reader :relationship
18
+ attr_reader :school
19
+ attr_reader :subscribers
20
+ attr_reader :upload_views
21
+ attr_reader :username
22
+ attr_reader :videos_watched
23
+ attr_reader :view_count
24
+ end
25
+ end
26
+ end