agiley-youtube-g 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ class YouTubeG
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,26 @@
1
+ class YouTubeG
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 : "#{YouTubeG.esc(k)}=#{YouTubeG.esc(v)}" }.compact.sort.join('&')
21
+ qs.empty? ? '' : "?#{qs}"
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ class YouTubeG
2
+ module Request #:nodoc:
3
+ class CommentsSearch < BaseSearch #:nodoc:
4
+ attr_reader :max_results # max_results
5
+ attr_reader :offset # start-index
6
+
7
+ def initialize(video_id, options={})
8
+ @max_results, @order_by, @offset = nil
9
+ @url = base_url
10
+
11
+ @url << "videos/#{video_id}/comments"
12
+ set_instance_variables(options)
13
+
14
+ if options.has_key?(:per_page)
15
+ @max_results = options[:per_page]
16
+ end
17
+
18
+ if options.has_key?(:page)
19
+ per_page = @max_results || 25
20
+ @offset = calculate_offset(options[:page], per_page)
21
+ end
22
+
23
+ @url << build_query_params(to_youtube_params)
24
+ end
25
+
26
+ private
27
+
28
+ def to_youtube_params
29
+ {
30
+ 'max-results' => @max_results,
31
+ 'start-index' => @offset
32
+ }
33
+ end
34
+ private
35
+ def calculate_offset(page, per_page)
36
+ page == 1 ? 1 : ((per_page * page) - per_page + 1)
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+ class YouTubeG
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
+ if TYPES.include?(type)
15
+ @max_results, @order_by, @offset, @time = nil
16
+ set_instance_variables(options)
17
+ @url = base_url + type.to_s << build_query_params(to_youtube_params)
18
+ else
19
+ raise "Invalid type, must be one of: #{ TYPES.map { |t| t.to_s }.join(", ") }"
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def base_url
26
+ super << "standardfeeds/"
27
+ end
28
+
29
+ def to_youtube_params
30
+ {
31
+ 'max-results' => @max_results,
32
+ 'orderby' => @order_by,
33
+ 'start-index' => @offset,
34
+ 'time' => @time
35
+ }
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,43 @@
1
+ class YouTubeG
2
+ module Request #:nodoc:
3
+ class UserSearch < 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
+
8
+ def initialize(params, options={})
9
+ @max_results, @order_by, @offset = nil
10
+ @url = base_url
11
+
12
+ if params == :favorites
13
+ @url << "#{options[:user]}/favorites"
14
+ set_instance_variables(options)
15
+ elsif params[:user] && options[:favorites]
16
+ @url << "#{params[:user]}/favorites"
17
+ set_instance_variables(params)
18
+ return
19
+ elsif params[:user]
20
+ @url << "#{params[:user]}/uploads"
21
+ set_instance_variables(params)
22
+ end
23
+
24
+ @url << build_query_params(to_youtube_params)
25
+ end
26
+
27
+ private
28
+
29
+ def base_url
30
+ super << "users/"
31
+ end
32
+
33
+ def to_youtube_params
34
+ {
35
+ 'max-results' => @max_results,
36
+ 'orderby' => @order_by,
37
+ 'start-index' => @offset
38
+ }
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,93 @@
1
+ class YouTubeG
2
+ module Request #:nodoc:
3
+ class VideoSearch < BaseSearch #:nodoc:
4
+ # From here: http://code.google.com/apis/youtube/reference.html#yt_format
5
+ ONLY_EMBEDDABLE = 5
6
+
7
+ attr_reader :max_results # max_results
8
+ attr_reader :order_by # orderby, ([relevance], viewCount, published, rating)
9
+ attr_reader :offset # start-index
10
+ attr_reader :query # vq
11
+ attr_reader :response_format # alt, ([atom], rss, json)
12
+ attr_reader :tags # /-/tag1/tag2
13
+ attr_reader :categories # /-/Category1/Category2
14
+ attr_reader :video_format # format (1=mobile devices)
15
+ attr_reader :racy # racy ([exclude], include)
16
+ attr_reader :author
17
+
18
+ def initialize(params={})
19
+ # Initialize our various member data to avoid warnings and so we'll
20
+ # automatically fall back to the youtube api defaults
21
+ @max_results, @order_by,
22
+ @offset, @query,
23
+ @response_format, @video_format,
24
+ @racy, @author = nil
25
+ @url = base_url
26
+
27
+ # Return a single video (base_url + /T7YazwP8GtY)
28
+ return @url << "/" << params[:video_id] if params[:video_id]
29
+
30
+ @url << "/-/" if (params[:categories] || params[:tags])
31
+ @url << categories_to_params(params.delete(:categories)) if params[:categories]
32
+ @url << tags_to_params(params.delete(:tags)) if params[:tags]
33
+
34
+ set_instance_variables(params)
35
+
36
+ if( params[ :only_embeddable ] )
37
+ @video_format = ONLY_EMBEDDABLE
38
+ end
39
+
40
+ @url << build_query_params(to_youtube_params)
41
+ end
42
+
43
+ private
44
+
45
+ def base_url
46
+ super << "videos"
47
+ end
48
+
49
+ def to_youtube_params
50
+ {
51
+ 'max-results' => @max_results,
52
+ 'orderby' => @order_by,
53
+ 'start-index' => @offset,
54
+ 'vq' => @query,
55
+ 'alt' => @response_format,
56
+ 'format' => @video_format,
57
+ 'racy' => @racy,
58
+ 'author' => @author
59
+ }
60
+ end
61
+
62
+ # Convert category symbols into strings and build the URL. GData requires categories to be capitalized.
63
+ # Categories defined like: categories => { :include => [:news], :exclude => [:sports], :either => [..] }
64
+ # or like: categories => [:news, :sports]
65
+ def categories_to_params(categories)
66
+ if categories.respond_to?(:keys) and categories.respond_to?(:[])
67
+ s = ""
68
+ s << categories[:either].map { |c| c.to_s.capitalize }.join("%7C") << '/' if categories[:either]
69
+ s << categories[:include].map { |c| c.to_s.capitalize }.join("/") << '/' if categories[:include]
70
+ s << ("-" << categories[:exclude].map { |c| c.to_s.capitalize }.join("/-")) << '/' if categories[:exclude]
71
+ s
72
+ else
73
+ categories.map { |c| c.to_s.capitalize }.join("/") << '/'
74
+ end
75
+ end
76
+
77
+ # Tags defined like: tags => { :include => [:football], :exclude => [:soccer], :either => [:polo, :tennis] }
78
+ # or tags => [:football, :soccer]
79
+ def tags_to_params(tags)
80
+ if tags.respond_to?(:keys) and tags.respond_to?(:[])
81
+ s = ""
82
+ s << tags[:either].map { |t| YouTubeG.esc(t.to_s) }.join("%7C") << '/' if tags[:either]
83
+ s << tags[:include].map { |t| YouTubeG.esc(t.to_s) }.join("/") << '/' if tags[:include]
84
+ s << ("-" << tags[:exclude].map { |t| YouTubeG.esc(t.to_s) }.join("/-")) << '/' if tags[:exclude]
85
+ s
86
+ else
87
+ tags.map { |t| YouTubeG.esc(t.to_s) }.join("/") << '/'
88
+ end
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,218 @@
1
+ class YouTubeG
2
+
3
+ module Upload
4
+ class UploadError < YouTubeG::Error; end
5
+ class AuthenticationError < YouTubeG::Error; end
6
+
7
+ # Implements video uploads/updates/deletions
8
+ #
9
+ # require 'youtube_g'
10
+ #
11
+ # uploader = YouTubeG::Upload::VideoUpload.new("user", "pass", "dev-key")
12
+ # uploader.upload File.open("test.m4v"), :title => 'test',
13
+ # :description => 'cool vid d00d',
14
+ # :category => 'People',
15
+ # :keywords => %w[cool blah test]
16
+ #
17
+ class VideoUpload
18
+
19
+ def initialize user, pass, dev_key, client_id = 'youtube_g'
20
+ @user, @pass, @dev_key, @client_id = user, pass, dev_key, client_id
21
+ end
22
+
23
+ #
24
+ # Upload "data" to youtube, where data is either an IO object or
25
+ # raw file data.
26
+ # The hash keys for opts (which specify video info) are as follows:
27
+ # :mime_type
28
+ # :filename
29
+ # :title
30
+ # :description
31
+ # :category
32
+ # :keywords
33
+ # :private
34
+ # Specifying :private will make the video private, otherwise it will be public.
35
+ #
36
+ # When one of the fields is invalid according to YouTube,
37
+ # an UploadError will be raised. Its message contains a list of newline separated
38
+ # errors, containing the key and its error code.
39
+ #
40
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
41
+ def upload data, opts = {}
42
+ @opts = { :mime_type => 'video/mp4',
43
+ :title => '',
44
+ :description => '',
45
+ :category => '',
46
+ :keywords => [] }.merge(opts)
47
+
48
+ @opts[:filename] ||= generate_uniq_filename_from(data)
49
+
50
+ post_body_io = generate_upload_io(video_xml, data)
51
+
52
+ upload_headers = authorization_headers.merge({
53
+ "Slug" => "#{@opts[:filename]}",
54
+ "Content-Type" => "multipart/related; boundary=#{boundary}",
55
+ "Content-Length" => "#{post_body_io.expected_length}", # required per YouTube spec
56
+ # "Transfer-Encoding" => "chunked" # We will stream instead of posting at once
57
+ })
58
+
59
+ path = '/feeds/api/users/%s/uploads' % @user
60
+
61
+ Net::HTTP.start(uploads_url) do | session |
62
+
63
+ # Use the chained IO as body so that Net::HTTP reads into the socket for us
64
+ post = Net::HTTP::Post.new(path, upload_headers)
65
+ post.body_stream = post_body_io
66
+
67
+ response = session.request(post)
68
+ raise_on_faulty_response(response)
69
+
70
+ return uploaded_video_id_from(response.body)
71
+ end
72
+ end
73
+
74
+ # Updates a video in YouTube. Requires:
75
+ # :title
76
+ # :description
77
+ # :category
78
+ # :keywords
79
+ # The following are optional attributes:
80
+ # :private
81
+ # When the authentication credentials are incorrect, an AuthenticationError will be raised.
82
+ def update(video_id, options)
83
+ @opts = options
84
+
85
+ update_body = video_xml
86
+
87
+ update_header = authorization_headers.merge({
88
+ "Content-Type" => "application/atom+xml",
89
+ "Content-Length" => "#{update_body.length}",
90
+ })
91
+
92
+ update_url = "/feeds/api/users/#{@user}/uploads/#{video_id}"
93
+
94
+ Net::HTTP.start(base_url) do | session |
95
+ response = session.put(update_url, update_body, update_header)
96
+ raise_on_faulty_response(response)
97
+
98
+ return YouTubeG::Parser::VideoFeedParser.new(response.body).parse
99
+ end
100
+ end
101
+
102
+ # Delete a video on YouTube
103
+ def delete(video_id)
104
+ delete_header = authorization_headers.merge({
105
+ "Content-Type" => "application/atom+xml",
106
+ "Content-Length" => "0",
107
+ })
108
+
109
+ delete_url = "/feeds/api/users/#{@user}/uploads/#{video_id}"
110
+
111
+ Net::HTTP.start(base_url) do |session|
112
+ response = session.delete(delete_url, delete_header)
113
+ raise_on_faulty_response(response)
114
+ return true
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def uploads_url
121
+ ["uploads", base_url].join('.')
122
+ end
123
+
124
+ def base_url
125
+ "gdata.youtube.com"
126
+ end
127
+
128
+ def boundary
129
+ "An43094fu"
130
+ end
131
+
132
+ def authorization_headers
133
+ {
134
+ "Authorization" => "GoogleLogin auth=#{auth_token}",
135
+ "X-GData-Client" => "#{@client_id}",
136
+ "X-GData-Key" => "key=#{@dev_key}"
137
+ }
138
+ end
139
+
140
+ def parse_upload_error_from(string)
141
+ REXML::Document.new(string).elements["//errors"].inject('') do | all_faults, error|
142
+ location = error.elements["location"].text[/media:group\/media:(.*)\/text\(\)/,1]
143
+ code = error.elements["code"].text
144
+ all_faults + sprintf("%s: %s\n", location, code)
145
+ end
146
+ end
147
+
148
+ def raise_on_faulty_response(response)
149
+ if response.code.to_i == 403
150
+ raise AuthenticationError, response.body[/<TITLE>(.+)<\/TITLE>/, 1]
151
+ elsif response.code.to_i / 10 != 20 # Response in 20x means success
152
+ raise UploadError, parse_upload_error_from(response.body)
153
+ end
154
+ end
155
+
156
+ def uploaded_video_id_from(string)
157
+ xml = REXML::Document.new(string)
158
+ xml.elements["//id"].text[/videos\/(.+)/, 1]
159
+ end
160
+
161
+ # If data can be read, use the first 1024 bytes as filename. If data
162
+ # is a file, use path. If data is a string, checksum it
163
+ def generate_uniq_filename_from(data)
164
+ if data.respond_to?(:path)
165
+ Digest::MD5.hexdigest(data.path)
166
+ elsif data.respond_to?(:read)
167
+ chunk = data.read(1024)
168
+ data.rewind
169
+ Digest::MD5.hexdigest(chunk)
170
+ else
171
+ Digest::MD5.hexdigest(data)
172
+ end
173
+ end
174
+
175
+ def auth_token
176
+ @auth_token ||= begin
177
+ http = Net::HTTP.new("www.google.com", 443)
178
+ http.use_ssl = true
179
+ body = "Email=#{YouTubeG.esc @user}&Passwd=#{YouTubeG.esc @pass}&service=youtube&source=#{YouTubeG.esc @client_id}"
180
+ response = http.post("/youtube/accounts/ClientLogin", body, "Content-Type" => "application/x-www-form-urlencoded")
181
+ raise UploadError, response.body[/Error=(.+)/,1] if response.code.to_i != 200
182
+ @auth_token = response.body[/Auth=(.+)/, 1]
183
+ end
184
+ end
185
+
186
+ # TODO: isn't there a cleaner way to output top-notch XML without requiring stuff all over the place?
187
+ def video_xml
188
+ b = Builder::XmlMarkup.new
189
+ b.instruct!
190
+ 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 |
191
+ m.tag!("media:group") do | mg |
192
+ mg.tag!("media:title", @opts[:title], :type => "plain")
193
+ mg.tag!("media:description", @opts[:description], :type => "plain")
194
+ mg.tag!("media:keywords", @opts[:keywords].join(","))
195
+ mg.tag!('media:category', @opts[:category], :scheme => "http://gdata.youtube.com/schemas/2007/categories.cat")
196
+ mg.tag!('yt:private') if @opts[:private]
197
+ end
198
+ end.to_s
199
+ end
200
+
201
+ def generate_upload_io(video_xml, data)
202
+ post_body = [
203
+ "--#{boundary}\r\n",
204
+ "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n",
205
+ video_xml,
206
+ "\r\n--#{boundary}\r\n",
207
+ "Content-Type: #{@opts[:mime_type]}\r\nContent-Transfer-Encoding: binary\r\n\r\n",
208
+ data,
209
+ "\r\n--#{boundary}--\r\n",
210
+ ]
211
+
212
+ # Use Greedy IO to not be limited by 1K chunks
213
+ YouTubeG::GreedyChainIO.new(post_body)
214
+ end
215
+
216
+ end
217
+ end
218
+ end