nddrylliog_youtube_it 2.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/Gemfile +5 -0
  2. data/Gemfile.lock +38 -0
  3. data/Manifest.txt +37 -0
  4. data/README.rdoc +296 -0
  5. data/Rakefile +21 -0
  6. data/VERSION +1 -0
  7. data/lib/youtube_it.rb +91 -0
  8. data/lib/youtube_it/chain_io.rb +76 -0
  9. data/lib/youtube_it/client.rb +469 -0
  10. data/lib/youtube_it/middleware/faraday_authheader.rb +24 -0
  11. data/lib/youtube_it/middleware/faraday_oauth.rb +21 -0
  12. data/lib/youtube_it/middleware/faraday_oauth2.rb +13 -0
  13. data/lib/youtube_it/middleware/faraday_youtubeit.rb +30 -0
  14. data/lib/youtube_it/model/activity.rb +17 -0
  15. data/lib/youtube_it/model/author.rb +13 -0
  16. data/lib/youtube_it/model/category.rb +11 -0
  17. data/lib/youtube_it/model/comment.rb +16 -0
  18. data/lib/youtube_it/model/contact.rb +19 -0
  19. data/lib/youtube_it/model/content.rb +18 -0
  20. data/lib/youtube_it/model/message.rb +12 -0
  21. data/lib/youtube_it/model/playlist.rb +11 -0
  22. data/lib/youtube_it/model/rating.rb +23 -0
  23. data/lib/youtube_it/model/subscription.rb +7 -0
  24. data/lib/youtube_it/model/thumbnail.rb +17 -0
  25. data/lib/youtube_it/model/user.rb +28 -0
  26. data/lib/youtube_it/model/video.rb +243 -0
  27. data/lib/youtube_it/parser.rb +543 -0
  28. data/lib/youtube_it/record.rb +12 -0
  29. data/lib/youtube_it/request/base_search.rb +76 -0
  30. data/lib/youtube_it/request/error.rb +15 -0
  31. data/lib/youtube_it/request/standard_search.rb +43 -0
  32. data/lib/youtube_it/request/user_search.rb +47 -0
  33. data/lib/youtube_it/request/video_search.rb +105 -0
  34. data/lib/youtube_it/request/video_upload.rb +552 -0
  35. data/lib/youtube_it/response/video_search.rb +41 -0
  36. data/lib/youtube_it/version.rb +4 -0
  37. data/test/files/recorded_response.xml +1 -0
  38. data/test/files/youtube_video_response.xml +53 -0
  39. data/test/helper.rb +9 -0
  40. data/test/test.mov +0 -0
  41. data/test/test_chain_io.rb +63 -0
  42. data/test/test_client.rb +504 -0
  43. data/test/test_field_search.rb +48 -0
  44. data/test/test_video.rb +48 -0
  45. data/test/test_video_feed_parser.rb +264 -0
  46. data/test/test_video_search.rb +147 -0
  47. data/youtube_it.gemspec +95 -0
  48. metadata +149 -0
@@ -0,0 +1,12 @@
1
+ class YouTubeIt
2
+ class Record #:nodoc:
3
+ def initialize (params)
4
+ return if params.nil?
5
+
6
+ params.each do |key, value|
7
+ name = key.to_s
8
+ instance_variable_set("@#{name}", value) if respond_to?(name)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,76 @@
1
+ class YouTubeIt
2
+ module Request #:nodoc:
3
+ class BaseSearch #:nodoc:
4
+ attr_reader :url
5
+
6
+ private
7
+
8
+ def base_url
9
+ "http://gdata.youtube.com/feeds/api/"
10
+ end
11
+
12
+ def set_instance_variables( variables )
13
+ variables.each do |key, value|
14
+ name = key.to_s
15
+ instance_variable_set("@#{name}", value) if respond_to?(name)
16
+ end
17
+ end
18
+
19
+ def build_query_params(params)
20
+ qs = params.to_a.map { | k, v | v.nil? ? nil : "#{YouTubeIt.esc(k)}=#{YouTubeIt.esc(v)}" }.compact.sort.join('&')
21
+ qs.empty? ? '' : (@dev_key ? "?#{qs}&key=#{@dev_key}" : "?#{qs}")
22
+ end
23
+ end
24
+
25
+
26
+ module FieldSearch
27
+ def default_fields
28
+ "id,updated,openSearch:totalResults,openSearch:startIndex,openSearch:itemsPerPage"
29
+ end
30
+
31
+ def fields_to_params(fields)
32
+ return "" unless fields
33
+
34
+ fields_param = [default_fields]
35
+
36
+ if fields[:recorded]
37
+ if fields[:recorded].is_a? Range
38
+ fields_param << "entry[xs:date(yt:recorded) > xs:date('#{formatted_date(fields[:recorded].first)}') and xs:date(yt:recorded) < xs:date('#{formatted_date(fields[:recorded].last)}')]"
39
+ else
40
+ fields_param << "entry[xs:date(yt:recorded) = xs:date('#{formatted_date(fields[:recorded])}')]"
41
+ end
42
+ end
43
+
44
+ if fields[:published]
45
+ if fields[:published].is_a? Range
46
+ fields_param << "entry[xs:dateTime(published) > xs:dateTime('#{formatted_date(fields[:published].first)}T00:00:00') and xs:dateTime(published) < xs:dateTime('#{formatted_date(fields[:published].last)}T00:00:00')]"
47
+ else
48
+ fields_param << "entry[xs:date(published) = xs:date('#{formatted_date(fields[:published])}')]"
49
+ end
50
+ end
51
+
52
+ if fields[:view_count]
53
+ fields_param << "entry[yt:statistics/@viewCount > #{fields[:view_count]}]"
54
+ end
55
+
56
+ if fields[:entry]
57
+ fields_param << "entry[#{fields[:entry]}]"
58
+ end
59
+
60
+
61
+ return "&fields=#{URI.escape(fields_param.join(","))}"
62
+ end
63
+
64
+ #youtube taked dates that look like 'YYYY-MM-DD'
65
+ def formatted_date(date)
66
+ return date if date.is_a? String
67
+ if date.respond_to? :strftime
68
+ date.strftime("%Y-%m-%d")
69
+ else
70
+ ""
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,15 @@
1
+ class UploadError < YouTubeIt::Error
2
+ attr_reader :code
3
+ def initialize(msg, code = 0)
4
+ super(msg)
5
+ @code = code
6
+ end
7
+ end
8
+
9
+ class AuthenticationError < YouTubeIt::Error
10
+ attr_reader :code
11
+ def initialize(msg, code = 0)
12
+ super(msg)
13
+ @code = code
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ class YouTubeIt
2
+ module Request #:nodoc:
3
+ class StandardSearch < BaseSearch #:nodoc:
4
+ attr_reader :max_results # max_results
5
+ attr_reader :order_by # orderby, ([relevance], viewCount, published, rating)
6
+ attr_reader :offset # start-index
7
+ attr_reader :time # time
8
+
9
+ TYPES = [ :top_rated, :top_favorites, :most_viewed, :most_popular,
10
+ :most_recent, :most_discussed, :most_linked, :most_responded,
11
+ :recently_featured, :watch_on_mobile ]
12
+
13
+ def initialize(type, options={})
14
+ @dev_key = options[:dev_key] if options[:dev_key]
15
+ if TYPES.include?(type)
16
+ @max_results, @order_by, @offset, @time = nil
17
+ set_instance_variables(options)
18
+ @url = base_url + type.to_s << build_query_params(to_youtube_params)
19
+ else
20
+ raise "Invalid type, must be one of: #{ TYPES.map { |t| t.to_s }.join(", ") }"
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def base_url
27
+ super << "standardfeeds/"
28
+ end
29
+
30
+ def to_youtube_params
31
+ {
32
+ 'max-results' => @max_results,
33
+ 'orderby' => @order_by,
34
+ 'start-index' => @offset,
35
+ 'time' => @time,
36
+ 'v' => 2
37
+ }
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+
@@ -0,0 +1,47 @@
1
+ class YouTubeIt
2
+ module Request #:nodoc:
3
+ class UserSearch < BaseSearch #:nodoc:
4
+ include FieldSearch
5
+ attr_reader :max_results # max_results
6
+ attr_reader :order_by # orderby, ([relevance], viewCount, published, rating)
7
+ attr_reader :offset # start-index
8
+
9
+ def initialize(params, options={})
10
+ @max_results, @order_by, @offset = nil
11
+ @url = base_url
12
+ @dev_key = options[:dev_key] if options[:dev_key]
13
+ if params == :favorites
14
+ @url << "#{options[:user]}/favorites"
15
+ set_instance_variables(options)
16
+ elsif params[:user] && options[:favorites]
17
+ @url << "#{params[:user]}/favorites"
18
+ set_instance_variables(params)
19
+ return
20
+ elsif params[:user]
21
+ @url << "#{params[:user]}/uploads"
22
+ set_instance_variables(params)
23
+ end
24
+
25
+ @url << build_query_params(to_youtube_params)
26
+ @url << fields_to_params(params.delete(:fields)) if params != :favorites && params[:fields]
27
+ end
28
+
29
+ private
30
+
31
+ def base_url
32
+ super << "users/"
33
+ end
34
+
35
+ def to_youtube_params
36
+ {
37
+ 'max-results' => @max_results,
38
+ 'orderby' => @order_by,
39
+ 'start-index' => @offset,
40
+ 'v' => 2
41
+ }
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+
@@ -0,0 +1,105 @@
1
+ class YouTubeIt
2
+ module Request #:nodoc:
3
+ class VideoSearch < BaseSearch #:nodoc:
4
+ include FieldSearch
5
+
6
+ # From here: http://code.google.com/apis/youtube/reference.html#yt_format
7
+ ONLY_EMBEDDABLE = 5
8
+
9
+ attr_reader :max_results # max_results
10
+ attr_reader :order_by # orderby, ([relevance], viewCount, published, rating)
11
+ attr_reader :offset # start-index
12
+ attr_reader :query # vq
13
+ attr_reader :response_format # alt, ([atom], rss, json)
14
+ attr_reader :tags # /-/tag1/tag2
15
+ attr_reader :categories # /-/Category1/Category2
16
+ attr_reader :video_format # format (1=mobile devices)
17
+ attr_reader :safe_search # safeSearch (none, [moderate], strict)
18
+ attr_reader :author
19
+ attr_reader :lang # lt
20
+ attr_reader :restriction
21
+
22
+
23
+ def initialize(params={})
24
+ # Initialize our various member data to avoid warnings and so we'll
25
+ # automatically fall back to the youtube api defaults
26
+ @max_results, @order_by,
27
+ @offset, @query,
28
+ @response_format, @video_format,
29
+ @safe_search, @author, @lang = nil
30
+ @url = base_url
31
+ @dev_key = params[:dev_key] if params[:dev_key]
32
+
33
+ # Return a single video (base_url + /T7YazwP8GtY)
34
+ return @url << "/" << params[:video_id] << "?v=2" if params[:video_id]
35
+
36
+ @url << "/-/" if (params[:categories] || params[:tags])
37
+ @url << categories_to_params(params.delete(:categories)) if params[:categories]
38
+ @url << tags_to_params(params.delete(:tags)) if params[:tags]
39
+
40
+ set_instance_variables(params)
41
+
42
+ if( params[ :only_embeddable ] )
43
+ @video_format = ONLY_EMBEDDABLE
44
+ end
45
+
46
+ @url << build_query_params(to_youtube_params)
47
+ @url << fields_to_params(params.delete(:fields)) if params[:fields]
48
+ end
49
+
50
+ private
51
+
52
+ def base_url
53
+ super << "videos"
54
+ end
55
+
56
+ def to_youtube_params
57
+ {
58
+ 'max-results' => @max_results,
59
+ 'orderby' => @order_by,
60
+ 'start-index' => @offset,
61
+ 'v' => 2,
62
+ 'q' => @query,
63
+ 'alt' => @response_format,
64
+ 'format' => @video_format,
65
+ 'safeSearch' => @safe_search,
66
+ 'author' => @author,
67
+ 'restriction' => @restriction,
68
+ 'lr' => @lang
69
+ }
70
+ end
71
+
72
+
73
+ # Convert category symbols into strings and build the URL. GData requires categories to be capitalized.
74
+ # Categories defined like: categories => { :include => [:news], :exclude => [:sports], :either => [..] }
75
+ # or like: categories => [:news, :sports]
76
+ def categories_to_params(categories)
77
+ if categories.respond_to?(:keys) and categories.respond_to?(:[])
78
+ s = ""
79
+ s << categories[:either].map { |c| c.to_s.capitalize }.join("%7C") << '/' if categories[:either]
80
+ s << categories[:include].map { |c| c.to_s.capitalize }.join("/") << '/' if categories[:include]
81
+ s << ("-" << categories[:exclude].map { |c| c.to_s.capitalize }.join("/-")) << '/' if categories[:exclude]
82
+ s
83
+ else
84
+ categories.map { |c| c.to_s.capitalize }.join("/") << '/'
85
+ end
86
+ end
87
+
88
+ # Tags defined like: tags => { :include => [:football], :exclude => [:soccer], :either => [:polo, :tennis] }
89
+ # or tags => [:football, :soccer]
90
+ def tags_to_params(tags)
91
+ if tags.respond_to?(:keys) and tags.respond_to?(:[])
92
+ s = ""
93
+ s << tags[:either].map { |t| YouTubeIt.esc(t.to_s) }.join("%7C") << '/' if tags[:either]
94
+ s << tags[:include].map { |t| YouTubeIt.esc(t.to_s) }.join("/") << '/' if tags[:include]
95
+ s << ("-" << tags[:exclude].map { |t| YouTubeIt.esc(t.to_s) }.join("/-")) << '/' if tags[:exclude]
96
+ s
97
+ else
98
+ tags.map { |t| YouTubeIt.esc(t.to_s) }.join("/") << '/'
99
+ end
100
+ end
101
+
102
+ end
103
+ end
104
+ end
105
+
@@ -0,0 +1,552 @@
1
+ class YouTubeIt
2
+ module Upload
3
+
4
+ class UploadError < YouTubeIt::Error; end
5
+
6
+ class AuthenticationError < YouTubeIt::Error; end
7
+
8
+ # Implements video uploads/updates/deletions
9
+ #
10
+ # require 'youtube_it'
11
+ #
12
+ # uploader = YouTubeIt::Upload::VideoUpload.new("user", "pass", "dev-key")
13
+ # uploader.upload File.open("test.m4v"), :title => 'test',
14
+ # :description => 'cool vid d00d',
15
+ # :category => 'People',
16
+ # :keywords => %w[cool blah test]
17
+ #
18
+ class VideoUpload
19
+ include YouTubeIt::Logging
20
+
21
+ def initialize *params
22
+ if params.first.is_a?(Hash)
23
+ hash_options = params.first
24
+ @user = hash_options[:username]
25
+ @password = hash_options[:password]
26
+ @dev_key = hash_options[:dev_key]
27
+ @access_token = hash_options[:access_token]
28
+ @authsub_token = hash_options[:authsub_token]
29
+ @client_id = hash_options[:client_id] || "youtube_it"
30
+ @config_token = hash_options[:config_token]
31
+ else
32
+ puts "* warning: the method YouTubeIt::Upload::VideoUpload.new(username, password, dev_key) is depricated, use YouTubeIt::Upload::VideoUpload.new(:username => 'user', :password => 'passwd', :dev_key => 'dev_key')"
33
+ @user = params.shift
34
+ @password = params.shift
35
+ @dev_key = params.shift
36
+ @access_token = params.shift
37
+ @authsub_token = params.shift
38
+ @client_id = params.shift || "youtube_it"
39
+ @config_token = params.shift
40
+ end
41
+ end
42
+
43
+
44
+ def enable_http_debugging
45
+ @http_debugging = true
46
+ end
47
+
48
+ #
49
+ # Upload "data" to youtube, where data is either an IO object or
50
+ # raw file data.
51
+ # The hash keys for opts (which specify video info) are as follows:
52
+ # :mime_type
53
+ # :filename
54
+ # :title
55
+ # :description
56
+ # :category
57
+ # :keywords
58
+ # :private
59
+ # New V2 api hash keys for accessControl:
60
+ # :rate
61
+ # :comment
62
+ # :commentVote
63
+ # :videoRespond
64
+ # :list
65
+ # :embed
66
+ # :syndicate
67
+ # Specifying :private will make the video private, otherwise it will be public.
68
+ #
69
+ # When one of the fields is invalid according to YouTube,
70
+ # an UploadError will be raised. Its message contains a list of newline separated
71
+ # errors, containing the key and its error code.
72
+ #
73
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
74
+ def upload(data, opts = {})
75
+ @opts = { :mime_type => 'video/mp4',
76
+ :title => '',
77
+ :description => '',
78
+ :category => 'People',
79
+ :keywords => [] }.merge(opts)
80
+
81
+ @opts[:filename] ||= generate_uniq_filename_from(data)
82
+
83
+ post_body_io = generate_upload_io(video_xml, data)
84
+
85
+ upload_header = {
86
+ "Slug" => "#{@opts[:filename]}",
87
+ "Content-Type" => "multipart/related; boundary=#{boundary}",
88
+ "Content-Length" => "#{post_body_io.expected_length}",
89
+ }
90
+
91
+ upload_url = "/feeds/api/users/default/uploads"
92
+ response = yt_session(uploads_url).post(upload_url, post_body_io, upload_header)
93
+
94
+ return YouTubeIt::Parser::VideoFeedParser.new(response.body).parse
95
+ end
96
+
97
+ # Updates a video in YouTube. Requires:
98
+ # :title
99
+ # :description
100
+ # :category
101
+ # :keywords
102
+ # The following are optional attributes:
103
+ # :private
104
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
105
+ def update(video_id, options)
106
+ @opts = { :title => '',
107
+ :description => '',
108
+ :category => 'People',
109
+ :keywords => [] }.merge(options)
110
+
111
+ update_body = video_xml
112
+ update_url = "/feeds/api/users/default/uploads/%s" % video_id
113
+ response = yt_session.put(update_url, update_body)
114
+
115
+ return YouTubeIt::Parser::VideoFeedParser.new(response.body).parse
116
+ end
117
+
118
+ # Fetches the currently authenticated user's contacts (i.e. friends).
119
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
120
+ def get_my_contacts(opts)
121
+ contacts_url = "/feeds/api/users/default/contacts?v=2"
122
+ contacts_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
123
+ response = yt_session.get(contacts_url)
124
+
125
+ return YouTubeIt::Parser::ContactsParser.new(response).parse
126
+ end
127
+
128
+ def send_message(opts)
129
+ message_body = message_xml_for(opts)
130
+ message_url = "/feeds/api/users/%s/inbox" % opts[:recipient_id]
131
+ response = yt_session.post(message_url, message_body)
132
+
133
+ return {:code => response.status, :body => response.body}
134
+ end
135
+
136
+ # Fetches the currently authenticated user's messages (i.e. inbox).
137
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
138
+ def get_my_messages(opts)
139
+ messages_url = "/feeds/api/users/default/inbox"
140
+ messages_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
141
+ response = yt_session.get(messages_url)
142
+
143
+ return YouTubeIt::Parser::MessagesParser.new(response).parse
144
+ end
145
+
146
+ # Fetches the data of a video, which may be private. The video must be owned by this user.
147
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
148
+ def get_my_video(video_id)
149
+ get_url = "/feeds/api/users/default/uploads/%s" % video_id
150
+ response = yt_session.get(get_url)
151
+
152
+ return YouTubeIt::Parser::VideoFeedParser.new(response.body).parse
153
+ end
154
+
155
+ # Fetches the data of the videos of the current user, which may be private.
156
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
157
+ def get_my_videos(opts)
158
+ max_results = opts[:per_page] || 50
159
+ start_index = ((opts[:page] || 1) -1) * max_results +1
160
+ get_url = "/feeds/api/users/default/uploads?max-results=#{max_results}&start-index=#{start_index}"
161
+ response = yt_session.get(get_url)
162
+
163
+ return YouTubeIt::Parser::VideosFeedParser.new(response.body).parse
164
+ end
165
+
166
+ # Delete a video on YouTube
167
+ def delete(video_id)
168
+ delete_url = "/feeds/api/users/default/uploads/%s" % video_id
169
+ response = yt_session.delete(delete_url)
170
+
171
+ return true
172
+ end
173
+
174
+ # Delete a video message
175
+ def delete_message(message_id)
176
+ delete_url = "/feeds/api/users/default/inbox/%s" % message_id
177
+ response = yt_session.delete(delete_url)
178
+
179
+ return true
180
+ end
181
+
182
+ def get_upload_token(options, nexturl)
183
+ @opts = options
184
+ token_body = video_xml
185
+ token_url = "/action/GetUploadToken"
186
+ response = yt_session.post(token_url, token_body)
187
+
188
+ return {:url => "#{response.body[/<url>(.+)<\/url>/, 1]}?nexturl=#{nexturl}",
189
+ :token => response.body[/<token>(.+)<\/token>/, 1]}
190
+ end
191
+
192
+ def add_comment(video_id, comment)
193
+ comment_body = video_xml_for(:comment => comment)
194
+ comment_url = "/feeds/api/videos/%s/comments" % video_id
195
+ response = yt_session.post(comment_url, comment_body)
196
+
197
+ return {:code => response.status, :body => response.body}
198
+ end
199
+
200
+ def comments(video_id, opts = {})
201
+ comment_url = "/feeds/api/videos/%s/comments?" % video_id
202
+ comment_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
203
+ response = yt_session.get(comment_url)
204
+ return YouTubeIt::Parser::CommentsFeedParser.new(response).parse
205
+ end
206
+
207
+ def add_favorite(video_id)
208
+ favorite_body = video_xml_for(:favorite => video_id)
209
+ favorite_url = "/feeds/api/users/default/favorites"
210
+ response = yt_session.post(favorite_url, favorite_body)
211
+
212
+ return {:code => response.status, :body => response.body}
213
+ end
214
+
215
+ def delete_favorite(video_id)
216
+ favorite_header = {
217
+ "GData-Version" => "1",
218
+ }
219
+ favorite_url = "/feeds/api/users/default/favorites/%s" % video_id
220
+ response = yt_session.delete(favorite_url, favorite_header)
221
+
222
+ return true
223
+ end
224
+
225
+ def profile(user)
226
+ profile_url = "/feeds/api/users/%s?v=2" % (user ? user : "default")
227
+ response = yt_session.get(profile_url)
228
+
229
+ return YouTubeIt::Parser::ProfileFeedParser.new(response).parse
230
+ end
231
+
232
+ # Return's a user's activity feed.
233
+ def get_activity(user, opts)
234
+ activity_url = "/feeds/api/events?author=%s&v=2&" % (user ? user : "default")
235
+ activity_url << opts.collect { |k,p| [k,p].join '=' }.join('&')
236
+ response = yt_session.get(activity_url)
237
+
238
+ return YouTubeIt::Parser::ActivityParser.new(response).parse
239
+ end
240
+
241
+ def playlist(playlist_id)
242
+ playlist_url = "/feeds/api/playlists/%s?v=2" % playlist_id
243
+ response = yt_session.get(playlist_url)
244
+
245
+ return YouTubeIt::Parser::PlaylistFeedParser.new(response).parse
246
+ end
247
+
248
+ def playlists(user)
249
+ playlist_url = "/feeds/api/users/%s/playlists?v=2" % (user ? user : "default")
250
+ response = yt_session.get(playlist_url)
251
+
252
+ return YouTubeIt::Parser::PlaylistsFeedParser.new(response).parse
253
+ end
254
+
255
+ def add_playlist(options)
256
+ playlist_body = video_xml_for_playlist(options)
257
+ playlist_url = "/feeds/api/users/default/playlists"
258
+ response = yt_session.post(playlist_url, playlist_body)
259
+
260
+ return YouTubeIt::Parser::PlaylistFeedParser.new(response).parse
261
+ end
262
+
263
+ def add_video_to_playlist(playlist_id, video_id)
264
+ playlist_body = video_xml_for(:playlist => video_id)
265
+ playlist_url = "/feeds/api/playlists/%s" % playlist_id
266
+ response = yt_session.post(playlist_url, playlist_body)
267
+
268
+ return {:code => response.status, :body => response.body, :playlist_entry_id => playlist_entry_id_from_playlist(response.body)}
269
+ end
270
+
271
+ def update_playlist(playlist_id, options)
272
+ playlist_body = video_xml_for_playlist(options)
273
+ playlist_url = "/feeds/api/users/default/playlists/%s" % playlist_id
274
+ response = yt_session.put(playlist_url, playlist_body)
275
+
276
+ return YouTubeIt::Parser::PlaylistFeedParser.new(response).parse
277
+ end
278
+
279
+ def delete_video_from_playlist(playlist_id, playlist_entry_id)
280
+ playlist_url = "/feeds/api/playlists/%s/%s" % [playlist_id, playlist_entry_id]
281
+ response = yt_session.delete(playlist_url)
282
+
283
+ return true
284
+ end
285
+
286
+ def delete_playlist(playlist_id)
287
+ playlist_url = "/feeds/api/users/default/playlists/%s" % playlist_id
288
+ response = yt_session.delete(playlist_url)
289
+
290
+ return true
291
+ end
292
+
293
+ def rate_video(video_id, rating)
294
+ rating_body = video_xml_for(:rating => rating)
295
+ rating_url = "/feeds/api/videos/#{video_id}/ratings"
296
+ response = yt_session.post(rating_url, rating_body)
297
+
298
+ return {:code => response.status, :body => response.body}
299
+ end
300
+
301
+ def subscriptions(user)
302
+ subscription_url = "/feeds/api/users/%s/subscriptions?v=2" % (user ? user : "default")
303
+ response = yt_session.get(subscription_url)
304
+
305
+ return YouTubeIt::Parser::SubscriptionFeedParser.new(response).parse
306
+ end
307
+
308
+ def subscribe_channel(channel_name)
309
+ subscribe_body = video_xml_for(:subscribe => channel_name)
310
+ subscribe_url = "/feeds/api/users/default/subscriptions"
311
+ response = yt_session.post(subscribe_url, subscribe_body)
312
+
313
+ return {:code => response.status, :body => response.body}
314
+ end
315
+
316
+ def unsubscribe_channel(subscription_id)
317
+ unsubscribe_url = "/feeds/api/users/default/subscriptions/%s" % subscription_id
318
+ response = yt_session.delete(unsubscribe_url)
319
+
320
+ return {:code => response.status, :body => response.body}
321
+ end
322
+
323
+ def favorites(user, opts = {})
324
+ favorite_url = "/feeds/api/users/%s/favorites#{opts.empty? ? '' : '?#{opts.to_param}'}" % (user ? user : "default")
325
+ response = yt_session.get(favorite_url)
326
+
327
+ return YouTubeIt::Parser::VideosFeedParser.new(response.body).parse
328
+ end
329
+
330
+ def get_current_user
331
+ current_user_url = "/feeds/api/users/default"
332
+ response = yt_session.get(current_user_url)
333
+
334
+ return REXML::Document.new(response.body).elements["entry"].elements['author'].elements['name'].text
335
+ end
336
+
337
+ def add_response(original_video_id, response_video_id)
338
+ response_body = video_xml_for(:response => response_video_id)
339
+ response_url = "/feeds/api/videos/%s/responses" % original_video_id
340
+ response = yt_session.post(response_url, response_body)
341
+
342
+ return {:code => response.status, :body => response.body}
343
+ end
344
+
345
+ def delete_response(original_video_id, response_video_id)
346
+ response_url = "/feeds/api/videos/%s/responses/%s" % [original_video_id, response_video_id]
347
+ response = yt_session.delete(response_url)
348
+
349
+ return {:code => response.status, :body => response.body}
350
+ end
351
+
352
+ def get_watch_history
353
+ watch_history_url = "/feeds/api/users/default/watch_history?v=2"
354
+ response = yt_session.get(watch_history_url)
355
+
356
+ return YouTubeIt::Parser::VideosFeedParser.new(response.body).parse
357
+ end
358
+
359
+ def new_subscription_videos(user)
360
+ subscription_url = "/feeds/api/users/%s/newsubscriptionvideos?v=2" % (user ? user : "default")
361
+ response = yt_session.get(subscription_url)
362
+
363
+ return YouTubeIt::Parser::VideosFeedParser.new(response.body).parse
364
+ end
365
+
366
+ private
367
+
368
+ def uploads_url
369
+ ["http://uploads", base_url.sub("http://","")].join('.')
370
+ end
371
+
372
+ def base_url
373
+ "http://gdata.youtube.com"
374
+ end
375
+
376
+ def boundary
377
+ "An43094fu"
378
+ end
379
+
380
+ def authorization_headers
381
+ header = {"X-GData-Client" => "#{@client_id}"}
382
+ header.merge!("X-GData-Key" => "key=#{@dev_key}") if @dev_key
383
+ if @authsub_token
384
+ header.merge!("Authorization" => "AuthSub token=#{@authsub_token}")
385
+ elsif @access_token.nil? && @authsub_token.nil? && @user
386
+ header.merge!("Authorization" => "GoogleLogin auth=#{auth_token}")
387
+ end
388
+ header
389
+ end
390
+
391
+ def parse_upload_error_from(string)
392
+ begin
393
+ REXML::Document.new(string).elements["//errors"].inject('') do | all_faults, error|
394
+ if error.elements["internalReason"]
395
+ msg_error = error.elements["internalReason"].text
396
+ elsif error.elements["location"]
397
+ msg_error = error.elements["location"].text[/media:group\/media:(.*)\/text\(\)/,1]
398
+ else
399
+ msg_error = "Unspecified error"
400
+ end
401
+ code = error.elements["code"].text if error.elements["code"]
402
+ all_faults + sprintf("%s: %s\n", msg_error, code)
403
+ end
404
+ rescue
405
+ string[/<TITLE>(.+)<\/TITLE>/, 1] || string
406
+ end
407
+ end
408
+
409
+ def raise_on_faulty_response(response)
410
+ response_code = response.code.to_i
411
+ msg = parse_upload_error_from(response.body.gsub(/\n/, ''))
412
+
413
+ if response_code == 403 || response_code == 401
414
+ #if response_code / 10 == 40
415
+ raise AuthenticationError.new(msg, response_code)
416
+ elsif response_code / 10 != 20 # Response in 20x means success
417
+ raise UploadError.new(msg, response_code)
418
+ end
419
+ end
420
+
421
+ def uploaded_video_id_from(string)
422
+ xml = REXML::Document.new(string)
423
+ xml.elements["//id"].text[/videos\/(.+)/, 1]
424
+ end
425
+
426
+ def playlist_id_from(string)
427
+ xml = REXML::Document.new(string)
428
+ entry = xml.elements["entry"]
429
+ entry.elements["id"].text[/playlist([^<]+)/, 1].sub(':','')
430
+ end
431
+
432
+ # If data can be read, use the first 1024 bytes as filename. If data
433
+ # is a file, use path. If data is a string, checksum it
434
+ def generate_uniq_filename_from(data)
435
+ if data.respond_to?(:path)
436
+ Digest::MD5.hexdigest(data.path)
437
+ elsif data.respond_to?(:read)
438
+ chunk = data.read(1024)
439
+ data.rewind
440
+ Digest::MD5.hexdigest(chunk)
441
+ else
442
+ Digest::MD5.hexdigest(data)
443
+ end
444
+ end
445
+
446
+ def auth_token
447
+ @auth_token ||= begin
448
+ http = Faraday.new("https://www.google.com", :ssl => {:verify => false})
449
+ body = "Email=#{YouTubeIt.esc @user}&Passwd=#{YouTubeIt.esc @password}&service=youtube&source=#{YouTubeIt.esc @client_id}"
450
+ response = http.post("/youtube/accounts/ClientLogin", body, "Content-Type" => "application/x-www-form-urlencoded")
451
+ raise ::AuthenticationError.new(response.body[/Error=(.+)/,1], response.status.to_i) if response.status.to_i != 200
452
+ @auth_token = response.body[/Auth=(.+)/, 1]
453
+ end
454
+ end
455
+
456
+ # TODO: isn't there a cleaner way to output top-notch XML without requiring stuff all over the place?
457
+ def video_xml
458
+ b = Builder::XmlMarkup.new
459
+ b.instruct!
460
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:media' => "http://search.yahoo.com/mrss/", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
461
+ m.tag!("media:group") do | mg |
462
+ mg.tag!("media:title", @opts[:title], :type => "plain")
463
+ mg.tag!("media:description", @opts[:description], :type => "plain")
464
+ mg.tag!("media:keywords", @opts[:keywords].join(","))
465
+ mg.tag!('media:category', @opts[:category], :scheme => "http://gdata.youtube.com/schemas/2007/categories.cat")
466
+ mg.tag!('yt:private') if @opts[:private]
467
+ mg.tag!('media:category', @opts[:dev_tag], :scheme => "http://gdata.youtube.com/schemas/2007/developertags.cat") if @opts[:dev_tag]
468
+ end
469
+ m.tag!("yt:accessControl", :action => "rate", :permission => @opts[:rate]) if @opts[:rate]
470
+ m.tag!("yt:accessControl", :action => "comment", :permission => @opts[:comment]) if @opts[:comment]
471
+ m.tag!("yt:accessControl", :action => "commentVote", :permission => @opts[:commentVote]) if @opts[:commentVote]
472
+ m.tag!("yt:accessControl", :action => "videoRespond", :permission => @opts[:videoRespond]) if @opts[:videoRespond]
473
+ m.tag!("yt:accessControl", :action => "list", :permission => @opts[:list]) if @opts[:list]
474
+ m.tag!("yt:accessControl", :action => "embed", :permission => @opts[:embed]) if @opts[:embed]
475
+ m.tag!("yt:accessControl", :action => "syndicate", :permission => @opts[:syndicate]) if @opts[:syndicate]
476
+ end.to_s
477
+ end
478
+
479
+ def video_xml_for(data)
480
+ b = Builder::XmlMarkup.new
481
+ b.instruct!
482
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
483
+ m.content(data[:comment]) if data[:comment]
484
+ m.id(data[:favorite] || data[:playlist] || data[:response]) if data[:favorite] || data[:playlist] || data[:response]
485
+ m.tag!("yt:rating", :value => data[:rating]) if data[:rating]
486
+ if(data[:subscribe])
487
+ m.category(:scheme => "http://gdata.youtube.com/schemas/2007/subscriptiontypes.cat", :term => "channel")
488
+ m.tag!("yt:username", data[:subscribe])
489
+ end
490
+ end.to_s
491
+ end
492
+
493
+ def message_xml_for(data)
494
+ b = Builder::XmlMarkup.new
495
+ b.instruct!
496
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
497
+ m.id(data[:vedio_id]) #if data[:vedio_id]
498
+ m.title(data[:title]) if data[:title]
499
+ m.summary(data[:message])
500
+ end.to_s
501
+ end
502
+
503
+ def video_xml_for_playlist(data)
504
+ b = Builder::XmlMarkup.new
505
+ b.instruct!
506
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
507
+ m.title(data[:title]) if data[:title]
508
+ m.summary(data[:description] || data[:summary]) if data[:description] || data[:summary]
509
+ m.tag!('yt:private') if data[:private]
510
+ end.to_s
511
+ end
512
+
513
+ def generate_upload_io(video_xml, data)
514
+ post_body = [
515
+ "--#{boundary}\r\n",
516
+ "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n",
517
+ video_xml,
518
+ "\r\n--#{boundary}\r\n",
519
+ "Content-Type: #{@opts[:mime_type]}\r\nContent-Transfer-Encoding: binary\r\n\r\n",
520
+ data,
521
+ "\r\n--#{boundary}--\r\n",
522
+ ]
523
+
524
+ # Use Greedy IO to not be limited by 1K chunks
525
+ YouTubeIt::GreedyChainIO.new(post_body)
526
+ end
527
+
528
+ def playlist_entry_id_from_playlist(string)
529
+ playlist_xml = REXML::Document.new(string)
530
+ playlist_xml.elements.each("/entry") do |item|
531
+ return item.elements["id"].text[/^.*:([^:]+)$/,1]
532
+ end
533
+ end
534
+
535
+ def yt_session(url = nil)
536
+ Faraday.new(:url => (url ? url : base_url), :ssl => {:verify => false}) do |builder|
537
+ if @access_token
538
+ if @config_token
539
+ builder.use Faraday::Request::OAuth, @config_token
540
+ else
541
+ builder.use Faraday::Request::OAuth2, @access_token
542
+ end
543
+ end
544
+ builder.use Faraday::Request::AuthHeader, authorization_headers
545
+ builder.use Faraday::Response::YouTubeIt
546
+ builder.adapter YouTubeIt.adapter
547
+
548
+ end
549
+ end
550
+ end
551
+ end
552
+ end