vibedeck-youtube_it 0.0.1

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