tubeclip 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +41 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/lib/tubeclip/chain_io.rb +86 -0
  13. data/lib/tubeclip/client.rb +541 -0
  14. data/lib/tubeclip/middleware/faraday_authheader.rb +24 -0
  15. data/lib/tubeclip/middleware/faraday_oauth.rb +21 -0
  16. data/lib/tubeclip/middleware/faraday_oauth2.rb +13 -0
  17. data/lib/tubeclip/middleware/faraday_tubeclip.rb +38 -0
  18. data/lib/tubeclip/model/activity.rb +17 -0
  19. data/lib/tubeclip/model/author.rb +13 -0
  20. data/lib/tubeclip/model/caption.rb +7 -0
  21. data/lib/tubeclip/model/category.rb +11 -0
  22. data/lib/tubeclip/model/comment.rb +18 -0
  23. data/lib/tubeclip/model/contact.rb +19 -0
  24. data/lib/tubeclip/model/content.rb +18 -0
  25. data/lib/tubeclip/model/message.rb +12 -0
  26. data/lib/tubeclip/model/playlist.rb +11 -0
  27. data/lib/tubeclip/model/rating.rb +23 -0
  28. data/lib/tubeclip/model/subscription.rb +7 -0
  29. data/lib/tubeclip/model/thumbnail.rb +20 -0
  30. data/lib/tubeclip/model/user.rb +35 -0
  31. data/lib/tubeclip/model/video.rb +302 -0
  32. data/lib/tubeclip/parser.rb +643 -0
  33. data/lib/tubeclip/record.rb +12 -0
  34. data/lib/tubeclip/request/base_search.rb +76 -0
  35. data/lib/tubeclip/request/error.rb +21 -0
  36. data/lib/tubeclip/request/remote_file.rb +70 -0
  37. data/lib/tubeclip/request/standard_search.rb +49 -0
  38. data/lib/tubeclip/request/user_search.rb +47 -0
  39. data/lib/tubeclip/request/video_search.rb +125 -0
  40. data/lib/tubeclip/request/video_upload.rb +762 -0
  41. data/lib/tubeclip/response/video_search.rb +41 -0
  42. data/lib/tubeclip/version.rb +3 -0
  43. data/lib/tubeclip.rb +85 -0
  44. data/tubeclip.gemspec +44 -0
  45. metadata +259 -0
@@ -0,0 +1,762 @@
1
+ class Tubeclip
2
+ module Upload
3
+ # Implements video uploads/updates/deletions
4
+ #
5
+ # require 'youtube_it'
6
+ #
7
+ # uploader = Tubeclip::Upload::VideoUpload.new("user", "pass", "dev-key")
8
+ # uploader.upload File.open("test.m4v"), :title => 'test',
9
+ # :description => 'cool vid d00d',
10
+ # :category => 'People',
11
+ # :keywords => %w[cool blah test]
12
+ #
13
+ class VideoUpload
14
+ include Tubeclip::Logging
15
+
16
+ def initialize *params
17
+ if params.first.is_a?(Hash)
18
+ hash_options = params.first
19
+ @user = hash_options[:username]
20
+ @password = hash_options[:password]
21
+ @dev_key = hash_options[:dev_key]
22
+ @access_token = hash_options[:access_token]
23
+ @authsub_token = hash_options[:authsub_token]
24
+ @client_id = hash_options[:client_id] || "youtube_it"
25
+ @config_token = hash_options[:config_token]
26
+ else
27
+ puts "* warning: the method Tubeclip::Upload::VideoUpload.new(username, password, dev_key) is deprecated, use Tubeclip::Upload::VideoUpload.new(:username => 'user', :password => 'passwd', :dev_key => 'dev_key')"
28
+ @user = params.shift
29
+ @password = params.shift
30
+ @dev_key = params.shift
31
+ @access_token = params.shift
32
+ @authsub_token = params.shift
33
+ @client_id = params.shift || "youtube_it"
34
+ @config_token = params.shift
35
+ end
36
+ end
37
+
38
+
39
+ def enable_http_debugging
40
+ @http_debugging = true
41
+ end
42
+
43
+ def uri?(string)
44
+ uri = URI.parse(string)
45
+ %w( http https ).include?(uri.scheme)
46
+ rescue URI::BadURIError
47
+ false
48
+ rescue URI::InvalidURIError
49
+ false
50
+ end
51
+ #
52
+ # Upload "data" to youtube, where data is either an IO object or
53
+ # raw file data.
54
+ # The hash keys for opts (which specify video info) are as follows:
55
+ # :mime_type
56
+ # :filename
57
+ # :title
58
+ # :description
59
+ # :category
60
+ # :keywords
61
+ # :private
62
+ # New V2 api hash keys for accessControl:
63
+ # :rate
64
+ # :comment
65
+ # :commentVote
66
+ # :videoRespond
67
+ # :list
68
+ # :embed
69
+ # :syndicate
70
+ # Specifying :private will make the video private, otherwise it will be public.
71
+ #
72
+ # When one of the fields is invalid according to YouTube,
73
+ # an UploadError will be raised. Its message contains a list of newline separated
74
+ # errors, containing the key and its error code.
75
+ #
76
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
77
+ def upload(video_data, opts = {})
78
+
79
+ if video_data.is_a?(String) && uri?(video_data)
80
+ data = Tubeclip::Upload::RemoteFile.new(video_data, opts)
81
+ else
82
+ data = video_data
83
+ end
84
+
85
+ @opts = { :mime_type => 'video/mp4',
86
+ :title => '',
87
+ :description => '',
88
+ :category => 'People',
89
+ :keywords => [] }.merge(opts)
90
+
91
+ @opts[:filename] ||= generate_uniq_filename_from(data)
92
+
93
+ post_body_io = generate_upload_io(video_xml, data)
94
+
95
+ upload_header = {
96
+ "Slug" => "#{@opts[:filename]}",
97
+ "Content-Type" => "multipart/related; boundary=#{boundary}",
98
+ "Content-Length" => "#{post_body_io.expected_length}",
99
+ }
100
+
101
+ upload_url = "/feeds/api/users/default/uploads"
102
+ response = yt_session(uploads_url).post(upload_url, post_body_io, upload_header)
103
+
104
+ return Tubeclip::Parser::VideoFeedParser.new(response.body).parse rescue nil
105
+ end
106
+
107
+ # Updates a video in YouTube. Requires:
108
+ # :title
109
+ # :description
110
+ # :category
111
+ # :keywords
112
+ # The following are optional attributes:
113
+ # :private
114
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
115
+ def update(video_id, options)
116
+ @opts = { :title => '',
117
+ :description => '',
118
+ :category => 'People',
119
+ :keywords => [] }.merge(options)
120
+
121
+ update_body = video_xml
122
+ update_url = "/feeds/api/users/default/uploads/%s" % video_id
123
+ response = yt_session.put(update_url, update_body)
124
+
125
+ return Tubeclip::Parser::VideoFeedParser.new(response.body).parse rescue nil
126
+ end
127
+
128
+ # Partial updates to a video.
129
+ def partial_update(video_id, options)
130
+ update_body = partial_video_xml(options)
131
+ update_url = "/feeds/api/users/default/uploads/%s" % video_id
132
+ update_header = { "Content-Type" => "application/xml" }
133
+ response = yt_session.patch(update_url, update_body, update_header)
134
+
135
+ return Tubeclip::Parser::VideoFeedParser.new(response.body).parse rescue nil
136
+ end
137
+
138
+ def captions_update(video_id, data, options)
139
+ @opts = {
140
+ :language => 'en-US',
141
+ :slug => ''
142
+ }.merge(options)
143
+
144
+ upload_header = {
145
+ "Slug" => "#{URI.escape(@opts[:slug])}",
146
+ "Content-Language"=>@opts[:language],
147
+ "Content-Type" => "application/vnd.youtube.timedtext; charset=UTF-8",
148
+ "Content-Length" => "#{data.length}",
149
+ }
150
+ upload_url = "/feeds/api/videos/#{video_id}/captions"
151
+ response = yt_session(base_url).post(upload_url, data, upload_header)
152
+ return Tubeclip::Parser::CaptionFeedParser.new(response.body).parse
153
+ end
154
+
155
+ # Fetches the currently authenticated user's contacts (i.e. friends).
156
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
157
+ def get_my_contacts(opts)
158
+ contacts_url = "/feeds/api/users/default/contacts?v=#{Tubeclip::API_VERSION}"
159
+ contacts_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
160
+ response = yt_session.get(contacts_url)
161
+
162
+ return Tubeclip::Parser::ContactsParser.new(response).parse
163
+ end
164
+
165
+ def send_message(opts)
166
+ message_body = message_xml_for(opts)
167
+ message_url = "/feeds/api/users/%s/inbox" % opts[:recipient_id]
168
+ response = yt_session.post(message_url, message_body)
169
+
170
+ return {:code => response.status, :body => response.body}
171
+ end
172
+
173
+ # Fetches the currently authenticated user's messages (i.e. inbox).
174
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
175
+ def get_my_messages(opts)
176
+ messages_url = "/feeds/api/users/default/inbox"
177
+ messages_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
178
+ response = yt_session.get(messages_url)
179
+
180
+ return Tubeclip::Parser::MessagesParser.new(response).parse
181
+ end
182
+
183
+ # Fetches the data of a video, which may be private. The video must be owned by this user.
184
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
185
+ def get_my_video(video_id)
186
+ get_url = "/feeds/api/users/default/uploads/%s" % video_id
187
+ response = yt_session.get(get_url)
188
+
189
+ return Tubeclip::Parser::VideoFeedParser.new(response.body).parse rescue nil
190
+ end
191
+
192
+ # Fetches the data of the videos of the current user, which may be private.
193
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
194
+ def get_my_videos(opts)
195
+ max_results = opts[:per_page] || 50
196
+ start_index = ((opts[:page] || 1) -1) * max_results +1
197
+ get_url = "/feeds/api/users/default/uploads?max-results=#{max_results}&start-index=#{start_index}"
198
+ response = yt_session.get(get_url)
199
+
200
+ return Tubeclip::Parser::VideosFeedParser.new(response.body).parse
201
+ end
202
+
203
+ # Delete a video on YouTube
204
+ def delete(video_id)
205
+ delete_url = "/feeds/api/users/default/uploads/%s" % video_id
206
+ response = yt_session.delete(delete_url)
207
+
208
+ return true
209
+ end
210
+
211
+ # Delete a video message
212
+ def delete_message(message_id)
213
+ delete_url = "/feeds/api/users/default/inbox/%s" % message_id
214
+ response = yt_session.delete(delete_url)
215
+
216
+ return true
217
+ end
218
+
219
+ def get_upload_token(options, nexturl)
220
+ @opts = options
221
+ token_body = video_xml
222
+ token_url = "/action/GetUploadToken"
223
+ response = yt_session.post(token_url, token_body)
224
+
225
+ return {:url => "#{response.body[/<url>(.+)<\/url>/, 1]}?nexturl=#{nexturl}",
226
+ :token => response.body[/<token>(.+)<\/token>/, 1]}
227
+ end
228
+
229
+ def add_comment(video_id, comment, opts = {})
230
+ reply_to = opts.delete :reply_to
231
+ reply_to = reply_to.unique_id if reply_to.is_a? Tubeclip::Model::Comment
232
+ comment_body = comment_xml_for(:comment => comment, :reply_to => reply_to_url(video_id, reply_to))
233
+ comment_url = "/feeds/api/videos/%s/comments" % video_id
234
+ response = yt_session(base_ssl_url).post(comment_url, comment_body)
235
+ comment = Tubeclip::Parser::CommentsFeedParser.new(response.body).parse_single_entry
236
+ return {:code => response.status, :body => response.body, :comment => comment}
237
+ end
238
+
239
+ def delete_comment(video_id, comment_id)
240
+ comment_id = comment_id.unique_id if comment_id.is_a? Tubeclip::Model::Comment
241
+ url = "/feeds/api/videos/%s/comments/%s" % [video_id, comment_id]
242
+ response = yt_session(base_ssl_url).delete(url)
243
+
244
+ return response.status == 200
245
+ end
246
+
247
+ def comments(video_id, opts = {})
248
+ comment_url = "/feeds/api/videos/%s/comments?" % video_id
249
+ comment_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
250
+ response = yt_session.get(comment_url)
251
+ return Tubeclip::Parser::CommentsFeedParser.new(response).parse
252
+ end
253
+
254
+ def add_favorite(video_id)
255
+ favorite_body = video_xml_for(:favorite => video_id)
256
+ favorite_url = "/feeds/api/users/default/favorites"
257
+ response = yt_session.post(favorite_url, favorite_body)
258
+
259
+ return {:code => response.status, :body => response.body, :favorite_entry_id => get_entry_id(response.body)}
260
+ end
261
+
262
+ def delete_favorite(video_id)
263
+ favorite_url = "/feeds/api/users/default/favorites/%s" % video_id
264
+ response = yt_session.delete(favorite_url)
265
+
266
+ return true
267
+ end
268
+
269
+ def profile(user=nil)
270
+ response = yt_session.get(profile_url(user))
271
+
272
+ return Tubeclip::Parser::ProfileFeedParser.new(response).parse
273
+ end
274
+
275
+ def videos(idxes_to_fetch)
276
+ idxes_to_fetch.each_slice(50).map do |idxes|
277
+ post = Nokogiri::XML <<-BATCH
278
+ <feed
279
+ xmlns='http://www.w3.org/2005/Atom'
280
+ xmlns:media='http://search.yahoo.com/mrss/'
281
+ xmlns:batch='http://schemas.google.com/gdata/batch'
282
+ xmlns:yt='http://gdata.youtube.com/schemas/2007'>
283
+ </feed>
284
+ BATCH
285
+ idxes.each do |idx|
286
+ post.at('feed').add_child <<-ENTRY
287
+ <entry>
288
+ <batch:operation type="query" />
289
+ <id>/feeds/api/videos/#{idx}?v=#{Tubeclip::API_VERSION}</id>
290
+ <batch:id>#{idx}</batch:id>
291
+ </entry>
292
+ ENTRY
293
+ end
294
+
295
+ post_body = StringIO.new('')
296
+ post.write_to( post_body, :indent => 2 )
297
+ post_body_io = StringIO.new(post_body.string)
298
+
299
+ response = yt_session.post('feeds/api/videos/batch', post_body_io )
300
+ Tubeclip::Parser::BatchVideoFeedParser.new(response).parse
301
+ end.reduce({},:merge)
302
+ end
303
+
304
+ def profiles(usernames_to_fetch)
305
+ usernames_to_fetch.each_slice(50).map do |usernames|
306
+ post = Nokogiri::XML <<-BATCH
307
+ <feed
308
+ xmlns='http://www.w3.org/2005/Atom'
309
+ xmlns:media='http://search.yahoo.com/mrss/'
310
+ xmlns:batch='http://schemas.google.com/gdata/batch'
311
+ xmlns:yt='http://gdata.youtube.com/schemas/2007'>
312
+ </feed>
313
+ BATCH
314
+ usernames.each do |username|
315
+ post.at('feed').add_child <<-ENTRY
316
+ <entry>
317
+ <batch:operation type="query" />
318
+ <id>#{profile_url(username)}</id>
319
+ <batch:id>#{username}</batch:id>
320
+ </entry>
321
+ ENTRY
322
+ end
323
+
324
+ post_body = StringIO.new('')
325
+ post.write_to( post_body, :indent => 2 )
326
+ post_body_io = StringIO.new(post_body.string)
327
+
328
+ response = yt_session.post('feeds/api/users/batch', post_body_io )
329
+ Tubeclip::Parser::BatchProfileFeedParser.new(response).parse
330
+ end.reduce({},:merge)
331
+ end
332
+
333
+ def profile_url(user=nil)
334
+ "/feeds/api/users/%s?v=#{Tubeclip::API_VERSION}" % (user || "default")
335
+ end
336
+
337
+ # Return's a user's activity feed.
338
+ def get_activity(user, opts)
339
+ activity_url = "/feeds/api/events?author=%s&v=#{Tubeclip::API_VERSION}&" % (user ? user : "default")
340
+ activity_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
341
+ response = yt_session.get(activity_url)
342
+
343
+ return Tubeclip::Parser::ActivityParser.new(response).parse
344
+ end
345
+
346
+ def watchlater(user)
347
+ watchlater_url = "/feeds/api/users/%s/watch_later?v=#{Tubeclip::API_VERSION}" % (user ? user : "default")
348
+ response = yt_session.get(watchlater_url)
349
+
350
+ return Tubeclip::Parser::PlaylistFeedParser.new(response).parse
351
+ end
352
+
353
+ def add_video_to_watchlater(video_id)
354
+ playlist_body = video_xml_for(:playlist => video_id)
355
+ playlist_url = "/feeds/api/users/default/watch_later"
356
+ response = yt_session.post(playlist_url, playlist_body)
357
+
358
+ return {:code => response.status, :body => response.body, :watchlater_entry_id => get_entry_id(response.body)}
359
+ end
360
+
361
+ def delete_video_from_watchlater(video_id)
362
+ playlist_url = "/feeds/api/users/default/watch_later/%s" % video_id
363
+ response = yt_session.delete(playlist_url)
364
+
365
+ return true
366
+ end
367
+
368
+ def playlist(playlist_id, opts = {})
369
+ playlist_url = "/feeds/api/playlists/%s" % playlist_id
370
+ params = {'v' => 2, 'orderby' => 'position'}
371
+ params.merge!(opts) if opts
372
+ playlist_url << "?#{params.collect { |k,v| [k,v].join '=' }.join('&')}"
373
+ response = yt_session.get(playlist_url)
374
+
375
+ return Tubeclip::Parser::PlaylistFeedParser.new(response).parse
376
+ end
377
+
378
+ # Fetches playlists for the given user. An optional hash of parameters can be given and will
379
+ # be appended to the request. Paging parameters will need to be used to access playlists
380
+ # beyond the most recent 25 (page size default for YouTube API at the time of this writing)
381
+ # if a user has more than 25 playlists.
382
+ #
383
+ # Paging parameters include the following
384
+ # start-index - 1-based index of which playlist to start from (default is 1)
385
+ # max-results - maximum number of playlists to fetch, up to 25 (default is 25)
386
+ def playlists(user, opts={})
387
+ playlist_url = "/feeds/api/users/%s/playlists" % (user ? user : "default")
388
+ params = {'v' => Tubeclip::API_VERSION}
389
+ params.merge!(opts) if opts
390
+ playlist_url << "?#{params.collect { |k,v| [k,v].join '=' }.join('&')}"
391
+ response = yt_session.get(playlist_url)
392
+
393
+ return Tubeclip::Parser::PlaylistsFeedParser.new(response).parse
394
+ end
395
+
396
+ def add_playlist(options)
397
+ playlist_body = video_xml_for_playlist(options)
398
+ playlist_url = "/feeds/api/users/default/playlists"
399
+ response = yt_session.post(playlist_url, playlist_body)
400
+
401
+ return Tubeclip::Parser::PlaylistFeedParser.new(response).parse
402
+ end
403
+
404
+ def add_video_to_playlist(playlist_id, video_id, position)
405
+ playlist_body = video_xml_for(:playlist => video_id, :position => position)
406
+ playlist_url = "/feeds/api/playlists/%s" % playlist_id
407
+ response = yt_session.post(playlist_url, playlist_body)
408
+
409
+ return {:code => response.status, :body => response.body, :playlist_entry_id => get_entry_id(response.body)}
410
+ end
411
+
412
+ def update_position_video_from_playlist(playlist_id, playlist_entry_id, position)
413
+ playlist_body = video_xml_for(:position => position)
414
+ playlist_url = "/feeds/api/playlists/%s/%s" % [playlist_id, playlist_entry_id]
415
+ response = yt_session.put(playlist_url, playlist_body)
416
+
417
+ return {:code => response.status, :body => response.body, :playlist_entry_id => get_entry_id(response.body)}
418
+ end
419
+
420
+ def update_playlist(playlist_id, options)
421
+ playlist_body = video_xml_for_playlist(options)
422
+ playlist_url = "/feeds/api/users/default/playlists/%s" % playlist_id
423
+ response = yt_session.put(playlist_url, playlist_body)
424
+
425
+ return Tubeclip::Parser::PlaylistFeedParser.new(response).parse
426
+ end
427
+
428
+ def delete_video_from_playlist(playlist_id, playlist_entry_id)
429
+ playlist_url = "/feeds/api/playlists/%s/%s" % [playlist_id, playlist_entry_id]
430
+ response = yt_session.delete(playlist_url)
431
+
432
+ return true
433
+ end
434
+
435
+ def delete_playlist(playlist_id)
436
+ playlist_url = "/feeds/api/users/default/playlists/%s" % playlist_id
437
+ response = yt_session.delete(playlist_url)
438
+
439
+ return true
440
+ end
441
+
442
+ def rate_video(video_id, rating)
443
+ rating_body = video_xml_for(:rating => rating)
444
+ rating_url = "/feeds/api/videos/#{video_id}/ratings"
445
+ response = yt_session.post(rating_url, rating_body)
446
+
447
+ return {:code => response.status, :body => response.body}
448
+ end
449
+
450
+ def subscriptions(user)
451
+ subscription_url = "/feeds/api/users/%s/subscriptions?v=#{Tubeclip::API_VERSION}" % (user ? user : "default")
452
+ response = yt_session.get(subscription_url)
453
+
454
+ return Tubeclip::Parser::SubscriptionFeedParser.new(response).parse
455
+ end
456
+
457
+ def subscribe_channel(channel_name)
458
+ subscribe_body = video_xml_for(:subscribe => channel_name)
459
+ subscribe_url = "/feeds/api/users/default/subscriptions"
460
+ response = yt_session.post(subscribe_url, subscribe_body)
461
+
462
+ return {:code => response.status, :body => response.body}
463
+ end
464
+
465
+ def unsubscribe_channel(subscription_id)
466
+ unsubscribe_url = "/feeds/api/users/default/subscriptions/%s" % subscription_id
467
+ response = yt_session.delete(unsubscribe_url)
468
+
469
+ return {:code => response.status, :body => response.body}
470
+ end
471
+
472
+ def favorites(user, opts = {})
473
+ favorite_url = "/feeds/api/users/%s/favorites#{opts.empty? ? '' : '?#{opts.to_param}'}" % (user ? user : "default")
474
+ response = yt_session.get(favorite_url)
475
+
476
+ return Tubeclip::Parser::VideosFeedParser.new(response.body).parse
477
+ end
478
+
479
+ def get_current_user
480
+ current_user_url = "/feeds/api/users/default"
481
+ response = yt_session.get(current_user_url)
482
+
483
+ return Nokogiri::XML(response.body).at("entry/author/name").text
484
+ end
485
+
486
+ def add_response(original_video_id, response_video_id)
487
+ response_body = video_xml_for(:response => response_video_id)
488
+ response_url = "/feeds/api/videos/%s/responses" % original_video_id
489
+ response = yt_session.post(response_url, response_body)
490
+
491
+ return {:code => response.status, :body => response.body}
492
+ end
493
+
494
+ def delete_response(original_video_id, response_video_id)
495
+ response_url = "/feeds/api/videos/%s/responses/%s" % [original_video_id, response_video_id]
496
+ response = yt_session.delete(response_url)
497
+
498
+ return {:code => response.status, :body => response.body}
499
+ end
500
+
501
+ def get_watch_history
502
+ watch_history_url = "/feeds/api/users/default/watch_history?v=#{Tubeclip::API_VERSION}"
503
+ response = yt_session.get(watch_history_url)
504
+
505
+ return Tubeclip::Parser::VideosFeedParser.new(response.body).parse
506
+ end
507
+
508
+ def new_subscription_videos(user)
509
+ subscription_url = "/feeds/api/users/%s/newsubscriptionvideos?v=#{Tubeclip::API_VERSION}" % (user ? user : "default")
510
+ response = yt_session.get(subscription_url)
511
+
512
+ return Tubeclip::Parser::VideosFeedParser.new(response.body).parse
513
+ end
514
+
515
+ private
516
+
517
+ def uploads_url
518
+ ["http://uploads", base_url.sub("http://","")].join('.')
519
+ end
520
+
521
+ def base_url
522
+ "http://gdata.youtube.com"
523
+ end
524
+
525
+ def base_ssl_url
526
+ "https://gdata.youtube.com"
527
+ end
528
+
529
+ def boundary
530
+ "An43094fu"
531
+ end
532
+
533
+ def authorization_headers
534
+ header = {"X-GData-Client" => "#{@client_id}"}
535
+ header.merge!("X-GData-Key" => "key=#{@dev_key}") if @dev_key
536
+ if @authsub_token
537
+ header.merge!("Authorization" => "AuthSub token=#{@authsub_token}")
538
+ elsif @access_token.nil? && @authsub_token.nil? && @user
539
+ header.merge!("Authorization" => "GoogleLogin auth=#{auth_token}")
540
+ end
541
+ header
542
+ end
543
+
544
+ def parse_upload_error_from(string)
545
+ xml = Nokogiri::XML(string).at('errors')
546
+ if xml
547
+ xml.css("error").inject('') do |all_faults, error|
548
+ if error.at("internalReason")
549
+ msg_error = error.at("internalReason").text
550
+ elsif error.at("location")
551
+ msg_error = error.at("location").text[/media:group\/media:(.*)\/text\(\)/,1]
552
+ else
553
+ msg_error = "Unspecified error"
554
+ end
555
+ code = error.at("code").text if error.at("code")
556
+ all_faults + sprintf("%s: %s\n", msg_error, code)
557
+ end
558
+ else
559
+ string[/<TITLE>(.+)<\/TITLE>/, 1] || string
560
+ end
561
+ end
562
+
563
+ def uploaded_video_id_from(string)
564
+ xml = Nokogiri::XML(string)
565
+ xml.at("id").text[/videos\/(.+)/, 1]
566
+ end
567
+
568
+ def playlist_id_from(string)
569
+ xml = Nokogiri::XML(string)
570
+ xml.at("entry/id").text[/playlist([^<]+)/, 1].sub(':','')
571
+ end
572
+
573
+ # If data can be read, use the first 1024 bytes as filename. If data
574
+ # is a file, use path. If data is a string, checksum it
575
+ def generate_uniq_filename_from(data)
576
+ if data.respond_to?(:path)
577
+ Digest::MD5.hexdigest(data.path)
578
+ elsif data.respond_to?(:read)
579
+ chunk = data.read(1024)
580
+ data.rewind
581
+ Digest::MD5.hexdigest(chunk)
582
+ else
583
+ Digest::MD5.hexdigest(data)
584
+ end
585
+ end
586
+
587
+ def auth_token
588
+ @auth_token ||= begin
589
+ http = Faraday.new("https://www.google.com", :ssl => {:verify => false})
590
+ body = "Email=#{Tubeclip.esc @user}&Passwd=#{Tubeclip.esc @password}&service=youtube&source=#{Tubeclip.esc @client_id}"
591
+ response = http.post("/accounts/ClientLogin", body, "Content-Type" => "application/x-www-form-urlencoded")
592
+ raise ::Tubeclip::AuthenticationError.new(response.body[/Error=(.+)/,1], response.status.to_i) if response.status.to_i != 200
593
+ @auth_token = response.body[/Auth=(.+)/, 1]
594
+ end
595
+ end
596
+
597
+ # TODO: isn't there a cleaner way to output top-notch XML without requiring stuff all over the place?
598
+ def video_xml
599
+ b = Builder::XmlMarkup.new
600
+ b.instruct!
601
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom",
602
+ 'xmlns:media' => "http://search.yahoo.com/mrss/",
603
+ 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007",
604
+ 'xmlns:gml' => 'http://www.opengis.net/gml',
605
+ 'xmlns:georss' => 'http://www.georss.org/georss') do | m |
606
+ m.tag!("media:group") do | mg |
607
+ mg.tag!("media:title", @opts[:title], :type => "plain")
608
+ mg.tag!("media:description", @opts[:description], :type => "plain")
609
+ mg.tag!("media:keywords", @opts[:keywords].join(","))
610
+ mg.tag!('media:category', @opts[:category], :scheme => "http://gdata.youtube.com/schemas/2007/categories.cat")
611
+ mg.tag!('yt:private') if @opts[:private]
612
+ mg.tag!('media:category', @opts[:dev_tag], :scheme => "http://gdata.youtube.com/schemas/2007/developertags.cat") if @opts[:dev_tag]
613
+ end
614
+ m.tag!("yt:accessControl", :action => "rate", :permission => @opts[:rate]) if @opts[:rate]
615
+ m.tag!("yt:accessControl", :action => "comment", :permission => @opts[:comment]) if @opts[:comment]
616
+ m.tag!("yt:accessControl", :action => "commentVote", :permission => @opts[:commentVote]) if @opts[:commentVote]
617
+ m.tag!("yt:accessControl", :action => "videoRespond", :permission => @opts[:videoRespond]) if @opts[:videoRespond]
618
+ m.tag!("yt:accessControl", :action => "list", :permission => @opts[:list]) if @opts[:list]
619
+ m.tag!("yt:accessControl", :action => "embed", :permission => @opts[:embed]) if @opts[:embed]
620
+ m.tag!("yt:accessControl", :action => "syndicate", :permission => @opts[:syndicate]) if @opts[:syndicate]
621
+ if @opts[:latitude] and @opts[:longitude]
622
+ m.tag!("georss:where") do |geo|
623
+ geo.tag!("gml:Point") do |point|
624
+ point.tag!("gml:pos", @opts.values_at(:latitude, :longitude).join(' '))
625
+ end
626
+ end
627
+ end
628
+ end.to_s
629
+ end
630
+
631
+ def partial_video_xml(opts)
632
+ perms = [ :rate, :comment, :commentVote, :videoRespond, :list, :embed, :syndicate ]
633
+ delete_attrs = []
634
+ perms.each do |perm|
635
+ delete_attrs << "@action='#{perm}'" if opts[perm]
636
+ end
637
+
638
+ entry_attrs = {
639
+ :xmlns => "http://www.w3.org/2005/Atom",
640
+ 'xmlns:media' => "http://search.yahoo.com/mrss/",
641
+ 'xmlns:gd' => "http://schemas.google.com/g/2005",
642
+ 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007",
643
+ 'xmlns:gml' => 'http://www.opengis.net/gml',
644
+ 'xmlns:georss' => 'http://www.georss.org/georss' }
645
+
646
+ if !delete_attrs.empty?
647
+ entry_attrs['gd:fields'] = "yt:accessControl[#{delete_attrs.join(' or ')}]"
648
+ end
649
+
650
+ b = Builder::XmlMarkup.new
651
+ b.instruct!
652
+ b.entry(entry_attrs) do | m |
653
+
654
+ m.tag!("media:group") do | mg |
655
+ mg.tag!("media:title", opts[:title], :type => "plain") if opts[:title]
656
+ mg.tag!("media:description", opts[:description], :type => "plain") if opts[:description]
657
+ mg.tag!("media:keywords", opts[:keywords].join(",")) if opts[:keywords]
658
+ mg.tag!('media:category', opts[:category], :scheme => "http://gdata.youtube.com/schemas/2007/categories.cat") if opts[:category]
659
+ mg.tag!('yt:private') if opts[:private]
660
+ mg.tag!('media:category', opts[:dev_tag], :scheme => "http://gdata.youtube.com/schemas/2007/developertags.cat") if opts[:dev_tag]
661
+ end
662
+
663
+ perms.each do |perm|
664
+ m.tag!("yt:accessControl", :action => perm.to_s, :permission => opts[perm]) if opts[perm]
665
+ end
666
+
667
+ if opts[:latitude] and opts[:longitude]
668
+ m.tag!("georss:where") do |geo|
669
+ geo.tag!("gml:Point") do |point|
670
+ point.tag!("gml:pos", opts.values_at(:latitude, :longitude).join(' '))
671
+ end
672
+ end
673
+ end
674
+ end.to_s
675
+ end
676
+
677
+ def video_xml_for(data)
678
+ b = Builder::XmlMarkup.new
679
+ b.instruct!
680
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
681
+ m.id(data[:favorite] || data[:playlist] || data[:response]) if data[:favorite] || data[:playlist] || data[:response]
682
+ m.tag!("yt:rating", :value => data[:rating]) if data[:rating]
683
+ m.tag!("yt:position", data[:position]) if data[:position]
684
+ if(data[:subscribe])
685
+ m.category(:scheme => "http://gdata.youtube.com/schemas/2007/subscriptiontypes.cat", :term => "channel")
686
+ m.tag!("yt:username", data[:subscribe])
687
+ end
688
+ end.to_s
689
+ end
690
+
691
+ def reply_to_url video_id, reply_to
692
+ 'https://gdata.youtube.com/feeds/api/videos/%s/comments/%s' % [video_id, reply_to] if reply_to
693
+ end
694
+
695
+ def comment_xml_for(data)
696
+ b = Builder::XmlMarkup.new
697
+ b.instruct!
698
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
699
+ m.link(:rel => 'http://gdata.youtube.com/schemas/2007#in-reply-to', :type => 'application/atom+xml', :href => data[:reply_to]) if data[:reply_to]
700
+ m.content(data[:comment]) if data[:comment]
701
+ end.to_s
702
+ end
703
+
704
+ def message_xml_for(data)
705
+ b = Builder::XmlMarkup.new
706
+ b.instruct!
707
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
708
+ m.id(data[:vedio_id]) #if data[:vedio_id]
709
+ m.title(data[:title]) if data[:title]
710
+ m.summary(data[:message])
711
+ end.to_s
712
+ end
713
+
714
+ def video_xml_for_playlist(data)
715
+ b = Builder::XmlMarkup.new
716
+ b.instruct!
717
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
718
+ m.title(data[:title]) if data[:title]
719
+ m.summary(data[:description] || data[:summary]) if data[:description] || data[:summary]
720
+ m.tag!('yt:private') if data[:private]
721
+ end.to_s
722
+ end
723
+
724
+ def generate_upload_io(video_xml, data)
725
+ post_body = [
726
+ "--#{boundary}\r\n",
727
+ "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n",
728
+ video_xml,
729
+ "\r\n--#{boundary}\r\n",
730
+ "Content-Type: #{@opts[:mime_type]}\r\nContent-Transfer-Encoding: binary\r\n\r\n",
731
+ data,
732
+ "\r\n--#{boundary}--\r\n",
733
+ ]
734
+
735
+ # Use Greedy IO to not be limited by 1K chunks
736
+ Tubeclip::GreedyChainIO.new(post_body)
737
+ end
738
+
739
+ def get_entry_id(string)
740
+ entry_xml = Nokogiri::XML(string)
741
+ entry_xml.css("entry").each do |item|
742
+ return item.at("id").text[/^.*:([^:]+)$/,1]
743
+ end
744
+ end
745
+
746
+ def yt_session(url = nil)
747
+ Faraday.new(:url => (url ? url : base_url), :ssl => {:verify => false}) do |builder|
748
+ if @access_token
749
+ if @config_token
750
+ builder.use FaradayMiddleware::YoutubeOAuth, @config_token
751
+ else
752
+ builder.use FaradayMiddleware::YoutubeOAuth2, @access_token
753
+ end
754
+ end
755
+ builder.use FaradayMiddleware::YoutubeAuthHeader, authorization_headers
756
+ builder.use Faraday::Response::Tubeclip
757
+ builder.adapter Faraday.default_adapter
758
+ end
759
+ end
760
+ end
761
+ end
762
+ end