msp-youtube-g 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,168 @@
1
+ class YouTubeG
2
+
3
+ # The goal of the classes in this module is to build the request URLs for each type of search
4
+ module Request
5
+
6
+ class BaseSearch
7
+ attr_reader :url
8
+
9
+ def base_url
10
+ "http://gdata.youtube.com/feeds/api/"
11
+ end
12
+ end
13
+
14
+ class UserSearch < BaseSearch
15
+
16
+ def initialize(params, options={})
17
+ @url = base_url
18
+ return @url << "#{options[:user]}/favorites" if params == :favorites
19
+ @url << "#{params[:user]}/uploads" if params[:user]
20
+ end
21
+
22
+ def base_url
23
+ super << "users/"
24
+ end
25
+ end
26
+
27
+ class StandardSearch < BaseSearch
28
+ TYPES = [ :most_viewed, :top_rated, :recently_featured, :watch_on_mobile ]
29
+ TIMES = [ :all_time, :today, :this_week, :this_month ]
30
+
31
+ def initialize(type, options={})
32
+ if TYPES.include?(type)
33
+ @url = base_url << type.to_s
34
+ @url << "?time=#{CGI.escape(options.delete(:time).to_s)}" if TIMES.include?(options[:time])
35
+ else
36
+ raise "Invalid type, must be one of: #{ TYPES.map { |t| t.to_s }.join(", ") }"
37
+ end
38
+ end
39
+
40
+ def base_url
41
+ super << "standardfeeds/"
42
+ end
43
+ end
44
+
45
+ class VideoSearch < BaseSearch
46
+ # From here: http://code.google.com/apis/youtube/reference.html#yt_format
47
+ ONLY_EMBEDDABLE = 5
48
+
49
+ attr_reader :max_results # max_results
50
+ attr_reader :order_by # orderby, ([relevance], viewCount, published, rating)
51
+ attr_reader :offset # start-index
52
+ attr_reader :query # vq
53
+ attr_reader :response_format # alt, ([atom], rss, json)
54
+ attr_reader :tags # /-/tag1/tag2
55
+ attr_reader :categories # /-/Category1/Category2
56
+ attr_reader :video_format # format (1=mobile devices)
57
+ attr_reader :racy # racy ([exclude], include)
58
+ attr_reader :author
59
+
60
+ def initialize(params={})
61
+ # XXX I think we want to delete the line below
62
+ return if params.nil?
63
+
64
+ # initialize our various member data to avoid warnings and so we'll
65
+ # automatically fall back to the youtube api defaults
66
+ @max_results = nil
67
+ @order_by = nil
68
+ @offset = nil
69
+ @query = nil
70
+ @response_format = nil
71
+ @video_format = nil
72
+ @racy = nil
73
+ @author = nil
74
+
75
+ # build up the url corresponding to this request
76
+ @url = base_url
77
+
78
+ # http://gdata.youtube.com/feeds/videos/T7YazwP8GtY
79
+ return @url << "/" << params[:video_id] if params[:video_id]
80
+
81
+ @url << "/-/" if (params[:categories] || params[:tags])
82
+ @url << categories_to_params(params.delete(:categories)) if params[:categories]
83
+ @url << tags_to_params(params.delete(:tags)) if params[:tags]
84
+
85
+ params.each do |key, value|
86
+ name = key.to_s
87
+ instance_variable_set("@#{name}", value) if respond_to?(name)
88
+ end
89
+
90
+ if( params[ :only_embeddable ] )
91
+ @video_format = ONLY_EMBEDDABLE
92
+ end
93
+
94
+ @url << build_query_params(to_youtube_params)
95
+ end
96
+
97
+ def base_url
98
+ super << "videos"
99
+ end
100
+
101
+ def to_youtube_params
102
+ {
103
+ 'max-results' => @max_results,
104
+ 'orderby' => @order_by,
105
+ 'start-index' => @offset,
106
+ 'vq' => @query,
107
+ 'alt' => @response_format,
108
+ 'format' => @video_format,
109
+ 'racy' => @racy,
110
+ 'author' => @author
111
+ }
112
+ end
113
+
114
+ private
115
+ # Convert category symbols into strings and build the URL. GData requires categories to be capitalized.
116
+ # Categories defined like: categories => { :include => [:news], :exclude => [:sports], :either => [..] }
117
+ # or like: categories => [:news, :sports]
118
+ def categories_to_params(categories)
119
+ if categories.respond_to?(:keys) and categories.respond_to?(:[])
120
+ s = ""
121
+ s << categories[:either].map { |c| c.to_s.capitalize }.join("%7C") << '/' if categories[:either]
122
+ s << categories[:include].map { |c| c.to_s.capitalize }.join("/") << '/' if categories[:include]
123
+ s << ("-" << categories[:exclude].map { |c| c.to_s.capitalize }.join("/-")) << '/' if categories[:exclude]
124
+ s
125
+ else
126
+ categories.map { |c| c.to_s.capitalize }.join("/") << '/'
127
+ end
128
+ end
129
+
130
+ # Tags defined like: tags => { :include => [:football], :exclude => [:soccer], :either => [:polo, :tennis] }
131
+ # or tags => [:football, :soccer]
132
+ def tags_to_params(tags)
133
+ if tags.respond_to?(:keys) and tags.respond_to?(:[])
134
+ s = ""
135
+ s << tags[:either].map { |t| CGI.escape(t.to_s) }.join("%7C") << '/' if tags[:either]
136
+ s << tags[:include].map { |t| CGI.escape(t.to_s) }.join("/") << '/' if tags[:include]
137
+ s << ("-" << tags[:exclude].map { |t| CGI.escape(t.to_s) }.join("/-")) << '/' if tags[:exclude]
138
+ s
139
+ else
140
+ tags.map { |t| CGI.escape(t.to_s) }.join("/") << '/'
141
+ end
142
+ end
143
+
144
+ def build_query_params(params)
145
+ # nothing to do if there are no params
146
+ return '' if (!params || params.empty?)
147
+
148
+ # build up the query param string, tacking on every key/value
149
+ # pair for which the value is non-nil
150
+ u = '?'
151
+ item_count = 0
152
+ params.keys.each do |key|
153
+ value = params[key]
154
+ next if value.nil?
155
+
156
+ u << '&' if (item_count > 0)
157
+ u << "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
158
+ item_count += 1
159
+ end
160
+
161
+ # if we found no non-nil values, we've got no params so just
162
+ # return an empty string
163
+ (item_count == 0) ? '' : u
164
+ end
165
+
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,126 @@
1
+ require 'net/https'
2
+ require 'digest/md5'
3
+ require 'rexml/document'
4
+ require 'cgi'
5
+
6
+ class YouTubeG
7
+
8
+ module Upload
9
+ class UploadError < Exception; end
10
+
11
+ # require 'youtube_g'
12
+ #
13
+ # uploader = YouTubeG::Upload::VideoUpload.new("user", "pass", "dev-key")
14
+ # uploader.upload File.open("test.m4v"), :title => 'test',
15
+ # :description => 'cool vid d00d',
16
+ # :category => 'People',
17
+ # :keywords => %w[cool blah test]
18
+
19
+ class VideoUpload
20
+
21
+ def initialize user, pass, dev_key, client_id = 'youtube_g'
22
+ @user, @pass, @dev_key, @client_id = user, pass, dev_key, client_id
23
+ end
24
+
25
+ #
26
+ # Upload "data" to youtube, where data is either an IO object or
27
+ # raw file data.
28
+ # The hash keys for opts (which specify video info) are as follows:
29
+ # :mime_type
30
+ # :filename
31
+ # :title
32
+ # :description
33
+ # :category
34
+ # :keywords
35
+ #
36
+
37
+ def upload data, opts = {}
38
+ data = data.respond_to?(:read) ? data.read : data
39
+ @opts = { :mime_type => 'video/mp4',
40
+ :filename => Digest::MD5.hexdigest(data),
41
+ :title => '',
42
+ :description => '',
43
+ :category => '',
44
+ :keywords => [] }.merge(opts)
45
+
46
+ upload_body = generate_upload_body(boundary, video_xml, data)
47
+
48
+ upload_header = {
49
+ "Authorization" => "GoogleLogin auth=#{auth_token}",
50
+ "X-GData-Client" => "#{@client_id}",
51
+ "X-GData-Key" => "key=#{@dev_key}",
52
+ "Slug" => "#{@opts[:filename]}",
53
+ "Content-Type" => "multipart/related; boundary=#{boundary}",
54
+ "Content-Length" => "#{upload_body.length}",
55
+ }
56
+ puts("MSP upload_header [#{upload_header}]")
57
+
58
+ direct_upload_url = "/feeds/api/users/#{@user}/uploads"
59
+ puts("MSP direct_upload_url [#{direct_upload_url}]")
60
+
61
+ Net::HTTP.start(base_url) do |upload|
62
+ response = upload.post(direct_upload_url, upload_body, upload_header)
63
+ xml = REXML::Document.new(response.body)
64
+ if (xml.elements["//id"])
65
+ puts("MSP response xml [#{xml}]")
66
+ return xml.elements["//id"].text[/videos\/(.+)/, 1]
67
+ else
68
+ return xml
69
+ end
70
+
71
+
72
+ end
73
+
74
+ end
75
+
76
+ private
77
+
78
+ def base_url
79
+ "uploads.gdata.youtube.com"
80
+ end
81
+
82
+ def boundary
83
+ "An43094fu"
84
+ end
85
+
86
+ def auth_token
87
+ unless @auth_token
88
+ http = Net::HTTP.new("www.google.com", 443)
89
+ http.use_ssl = true
90
+ body = "Email=#{CGI::escape @user}&Passwd=#{CGI::escape @pass}&service=youtube&source=#{CGI::escape @client_id}"
91
+ puts("MSP auth body [#{body}]")
92
+ response = http.post("/youtube/accounts/ClientLogin", body, "Content-Type" => "application/x-www-form-urlencoded")
93
+ raise UploadError, "MSP "+response.body[/Error=(.+)/,1] if response.code.to_i != 200
94
+ puts("MSP response.body [#{response.body}]")
95
+ @auth_token = response.body[/Auth=(.+)/, 1]
96
+
97
+ end
98
+ puts "MSP auth_token [#{@auth_token}]"
99
+ @auth_token
100
+ end
101
+
102
+ def video_xml
103
+ %[<?xml version="1.0"?>
104
+ <entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007">
105
+ <media:group>
106
+ <media:title type="plain">#{@opts[:title]}</media:title>
107
+ <media:description type="plain">#{@opts[:description]}</media:description>
108
+ <media:category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">#{@opts[:category]}</media:category>
109
+ <media:keywords>#{@opts[:keywords].join ","}</media:keywords>
110
+ </media:group></entry> ]
111
+ end
112
+
113
+ def generate_upload_body(boundary, video_xml, data)
114
+ upload_body = ""
115
+ upload_body << "--#{boundary}\r\n"
116
+ upload_body << "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n"
117
+ upload_body << video_xml
118
+ upload_body << "\r\n--#{boundary}\r\n"
119
+ upload_body << "Content-Type: #{@opts[:mime_type]}\r\nContent-Transfer-Encoding: binary\r\n\r\n"
120
+ upload_body << data
121
+ upload_body << "\r\n--#{boundary}--\r\n"
122
+ end
123
+
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,23 @@
1
+ class YouTubeG
2
+ module Response
3
+ class VideoSearch < YouTubeG::Record
4
+ # the unique feed identifying url
5
+ attr_reader :feed_id
6
+
7
+ # the number of results per page
8
+ attr_reader :max_result_count
9
+
10
+ # the 1-based offset index into the full result set
11
+ attr_reader :offset
12
+
13
+ # the total number of results available for the original request
14
+ attr_reader :total_result_count
15
+
16
+ # the date and time at which the feed was last updated
17
+ attr_reader :updated_at
18
+
19
+ # the list of Video records
20
+ attr_reader :videos
21
+ end
22
+ end
23
+ end
data/lib/youtube_g.rb ADDED
@@ -0,0 +1,19 @@
1
+ require File.dirname(__FILE__) + '/youtube_g/client'
2
+ require File.dirname(__FILE__) + '/youtube_g/record'
3
+ require File.dirname(__FILE__) + '/youtube_g/parser'
4
+ require File.dirname(__FILE__) + '/youtube_g/model/author'
5
+ require File.dirname(__FILE__) + '/youtube_g/model/category'
6
+ require File.dirname(__FILE__) + '/youtube_g/model/contact'
7
+ require File.dirname(__FILE__) + '/youtube_g/model/content'
8
+ require File.dirname(__FILE__) + '/youtube_g/model/playlist'
9
+ require File.dirname(__FILE__) + '/youtube_g/model/rating'
10
+ require File.dirname(__FILE__) + '/youtube_g/model/thumbnail'
11
+ require File.dirname(__FILE__) + '/youtube_g/model/user'
12
+ require File.dirname(__FILE__) + '/youtube_g/model/video'
13
+ require File.dirname(__FILE__) + '/youtube_g/request/video_upload'
14
+ require File.dirname(__FILE__) + '/youtube_g/request/video_search'
15
+ require File.dirname(__FILE__) + '/youtube_g/response/video_search'
16
+
17
+ class YouTubeG
18
+ VERSION = '0.4.5'
19
+ end
@@ -0,0 +1,226 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'pp'
4
+
5
+ require 'youtube_g'
6
+
7
+ class TestClient < Test::Unit::TestCase
8
+ def setup
9
+ @client = YouTubeG::Client.new
10
+ end
11
+
12
+ def test_should_respond_to_a_basic_query
13
+ response = @client.videos_by(:query => "penguin")
14
+
15
+ assert_equal "http://gdata.youtube.com/feeds/api/videos?start-index=1&max-results=25&vq=penguin", response.feed_id
16
+ assert_equal 25, response.max_result_count
17
+ assert_equal 25, response.videos.length
18
+ assert_equal 1, response.offset
19
+ assert(response.total_result_count > 100)
20
+ assert_instance_of Time, response.updated_at
21
+
22
+ response.videos.each { |v| assert_valid_video v }
23
+ end
24
+
25
+ def test_should_get_videos_for_multiword_metasearch_query
26
+ response = @client.videos_by(:query => 'christina ricci')
27
+
28
+ assert_equal "http://gdata.youtube.com/feeds/api/videos?start-index=1&max-results=25&vq=christina+ricci", response.feed_id
29
+ assert_equal 25, response.max_result_count
30
+ assert_equal 25, response.videos.length
31
+ assert_equal 1, response.offset
32
+ assert(response.total_result_count > 100)
33
+ assert_instance_of Time, response.updated_at
34
+
35
+ response.videos.each { |v| assert_valid_video v }
36
+ end
37
+
38
+ def test_should_handle_video_not_yet_viewed
39
+ response = @client.videos_by(:query => "YnqHZDh_t2Q")
40
+
41
+ assert_equal 1, response.videos.length
42
+ response.videos.each { |v| assert_valid_video v }
43
+ end
44
+
45
+ # TODO: this doesn't work because the returned feed is in an unknown format
46
+ # def test_should_get_video_for_search_by_video_id
47
+ # response = @client.videos_by(:video_id => "T7YazwP8GtY")
48
+ # response.videos.each { |v| assert_valid_video v }
49
+ # end
50
+
51
+ def test_should_get_videos_for_one_tag
52
+ response = @client.videos_by(:tags => ['panther'])
53
+ response.videos.each { |v| assert_valid_video v }
54
+ end
55
+
56
+ def test_should_get_videos_for_multiple_tags
57
+ response = @client.videos_by(:tags => ['tiger', 'leopard'])
58
+ response.videos.each { |v| assert_valid_video v }
59
+ end
60
+
61
+ def test_should_get_videos_for_one_category
62
+ response = @client.videos_by(:categories => [:news])
63
+ response.videos.each { |v| assert_valid_video v }
64
+ end
65
+
66
+ def test_should_get_videos_for_multiple_categories
67
+ response = @client.videos_by(:categories => [:news, :sports])
68
+ response.videos.each { |v| assert_valid_video v }
69
+ end
70
+
71
+ # TODO: Need to do more specific checking in these tests
72
+ # Currently, if a URL is valid, and videos are found, the test passes regardless of search criteria
73
+ def test_should_get_videos_for_categories_and_tags
74
+ response = @client.videos_by(:categories => [:news, :sports], :tags => ['soccer', 'football'])
75
+ response.videos.each { |v| assert_valid_video v }
76
+ end
77
+
78
+ def test_should_get_most_viewed_videos
79
+ response = @client.videos_by(:most_viewed)
80
+ response.videos.each { |v| assert_valid_video v }
81
+ end
82
+
83
+ def test_should_get_top_rated_videos_for_today
84
+ response = @client.videos_by(:top_rated, :time => :today)
85
+ response.videos.each { |v| assert_valid_video v }
86
+ end
87
+
88
+ def test_should_get_videos_for_categories_and_tags_with_category_boolean_operators
89
+ response = @client.videos_by(:categories => { :either => [:news, :sports], :exclude => [:comedy] },
90
+ :tags => { :include => ['football'], :exclude => ['soccer'] })
91
+ response.videos.each { |v| assert_valid_video v }
92
+ end
93
+
94
+ def test_should_get_videos_for_categories_and_tags_with_tag_boolean_operators
95
+ response = @client.videos_by(:categories => { :either => [:news, :sports], :exclude => [:comedy] },
96
+ :tags => { :either => ['football', 'soccer', 'polo'] })
97
+ response.videos.each { |v| assert_valid_video v }
98
+ end
99
+
100
+ def test_should_get_videos_by_user
101
+ response = @client.videos_by(:user => 'liz')
102
+ response.videos.each { |v| assert_valid_video v }
103
+ end
104
+
105
+ # HTTP 403 Error
106
+ # def test_should_get_favorite_videos_by_user
107
+ # response = @client.videos_by(:favorites, :user => 'liz')
108
+ # response.videos.each { |v| assert_valid_video v }
109
+ # end
110
+
111
+ def test_should_get_videos_for_query_search_with_categories_excluded
112
+ response = @client.videos_by(:query => 'bench press', :categories => { :exclude => [:comedy, :entertainment] },
113
+ :max_results => 10)
114
+ assert_equal "<object width=\"425\" height=\"350\">\n <param name=\"movie\" value=\"http://www.youtube.com/v/BlDWdfTAx8o\"></param>\n <param name=\"wmode\" value=\"transparent\"></param>\n <embed src=\"http://www.youtube.com/v/BlDWdfTAx8o\" type=\"application/x-shockwave-flash\" \n wmode=\"transparent\" width=\"425\" height=\"350\"></embed>\n</object>\n", response.videos.first.embed_html
115
+ response.videos.each { |v| assert_valid_video v }
116
+ end
117
+
118
+ def test_should_be_able_to_pass_in_logger
119
+ @client = YouTubeG::Client.new(Logger.new(STDOUT))
120
+ assert_not_nil @client.logger
121
+ end
122
+
123
+ def test_should_create_logger_if_not_passed_in
124
+ @client = YouTubeG::Client.new
125
+ assert_not_nil @client.logger
126
+ end
127
+
128
+ def test_should_determine_if_nonembeddable_video_is_embeddable
129
+ response = @client.videos_by(:query => "avril lavigne girlfriend")
130
+
131
+ video = response.videos.first
132
+ assert !video.can_embed?
133
+ end
134
+
135
+ def test_should_determine_if_embeddable_video_is_embeddable
136
+ response = @client.videos_by(:query => "strongbad")
137
+
138
+ video = response.videos.first
139
+ assert video.can_embed?
140
+ end
141
+
142
+ def test_should_retrieve_video_by_id
143
+ video = @client.video_by("http://gdata.youtube.com/feeds/videos/EkF4JD2rO3Q")
144
+ assert_valid_video video
145
+
146
+ video = @client.video_by("EkF4JD2rO3Q")
147
+ assert_valid_video video
148
+ end
149
+
150
+ private
151
+
152
+ def assert_valid_video (video)
153
+ # pp video
154
+
155
+ # check general attributes
156
+ assert_instance_of YouTubeG::Model::Video, video
157
+ assert_instance_of Fixnum, video.duration
158
+ assert(video.duration > 0)
159
+ #assert_match(/^<div style=.*?<\/div>/m, video.html_content)
160
+ assert_instance_of String, video.html_content
161
+
162
+ # validate media content records
163
+ video.media_content.each do |media_content|
164
+ # http://www.youtube.com/v/IHVaXG1thXM
165
+ assert_valid_url media_content.url
166
+ assert(media_content.duration > 0)
167
+ assert_instance_of YouTubeG::Model::Video::Format, media_content.format
168
+ assert_instance_of String, media_content.mime_type
169
+ assert_match(/^[^\/]+\/[^\/]+$/, media_content.mime_type)
170
+ end
171
+
172
+ default_content = video.default_media_content
173
+ if default_content
174
+ assert_instance_of YouTubeG::Model::Content, default_content
175
+ assert default_content.is_default?
176
+ end
177
+
178
+ # validate keywords
179
+ video.keywords.each { |kw| assert_instance_of(String, kw) }
180
+
181
+ # http://www.youtube.com/watch?v=IHVaXG1thXM
182
+ assert_valid_url video.player_url
183
+ assert_instance_of Time, video.published_at
184
+
185
+ # validate optionally-present rating
186
+ if video.rating
187
+ assert_instance_of YouTubeG::Model::Rating, video.rating
188
+ assert_instance_of Float, video.rating.average
189
+ assert_instance_of Fixnum, video.rating.max
190
+ assert_instance_of Fixnum, video.rating.min
191
+ assert_instance_of Fixnum, video.rating.rater_count
192
+ end
193
+
194
+ # validate thumbnails
195
+ assert(video.thumbnails.size > 0)
196
+
197
+ assert_not_nil video.title
198
+ assert_instance_of String, video.title
199
+ assert(video.title.length > 0)
200
+
201
+ assert_instance_of Time, video.updated_at
202
+ # http://gdata.youtube.com/feeds/videos/IHVaXG1thXM
203
+ assert_valid_url video.video_id
204
+ assert_instance_of Fixnum, video.view_count
205
+
206
+ # validate author
207
+ assert_instance_of YouTubeG::Model::Author, video.author
208
+ assert_instance_of String, video.author.name
209
+ assert(video.author.name.length > 0)
210
+ assert_valid_url video.author.uri
211
+
212
+ # validate categories
213
+ video.categories.each do |cat|
214
+ assert_instance_of YouTubeG::Model::Category, cat
215
+ assert_instance_of String, cat.label
216
+ assert_instance_of String, cat.term
217
+ end
218
+ end
219
+
220
+ def assert_valid_url (url)
221
+ URI::parse(url)
222
+ return true
223
+ rescue
224
+ return false
225
+ end
226
+ end
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'pp'
4
+
5
+ require 'youtube_g'
6
+
7
+ class TestVideo < Test::Unit::TestCase
8
+ def test_should_extract_unique_id_from_video_id
9
+ video = YouTubeG::Model::Video.new(:video_id => "http://gdata.youtube.com/feeds/videos/ZTUVgYoeN_o")
10
+ assert_equal "ZTUVgYoeN_o", video.unique_id
11
+ end
12
+
13
+ def test_should_extract_unique_id_with_hypen_from_video_id
14
+ video = YouTubeG::Model::Video.new(:video_id => "http://gdata.youtube.com/feeds/videos/BDqs-OZWw9o")
15
+ assert_equal "BDqs-OZWw9o", video.unique_id
16
+ end
17
+
18
+ def test_should_have_related_videos
19
+ video = YouTubeG::Model::Video.new(:video_id => "http://gdata.youtube.com/feeds/videos/BDqs-OZWw9o")
20
+ response = video.related
21
+
22
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/BDqs-OZWw9o/related", response.feed_id
23
+ assert_equal 25, response.max_result_count
24
+ assert_equal 25, response.videos.length
25
+ assert_equal 1, response.offset
26
+ puts response.total_result_count
27
+ assert(response.total_result_count > 0)
28
+ assert_instance_of Time, response.updated_at
29
+ end
30
+ end