pius-youtube-g 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,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
+ @url << build_query_params(to_youtube_params)
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 != 200
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::XML.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", :type => "plain") { @opts[:title] }
193
+ mg.tag!("media:description", :type => "plain") { @opts[:description] }
194
+ mg.tag!("media:keywords") { @opts[:keywords].join(",") }
195
+ mg.tag!('media:category', :scheme => "http://gdata.youtube.com/schemas/2007/categories.cat") { @opts[:category] }
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
@@ -0,0 +1,41 @@
1
+ class YouTubeG
2
+ module Response
3
+ class VideoSearch < YouTubeG::Record
4
+ # *String*:: Unique feed identifying url.
5
+ attr_reader :feed_id
6
+
7
+ # *Fixnum*:: Number of results per page.
8
+ attr_reader :max_result_count
9
+
10
+ # *Fixnum*:: 1-based offset index into the full result set.
11
+ attr_reader :offset
12
+
13
+ # *Fixnum*:: Total number of results available for the original request.
14
+ attr_reader :total_result_count
15
+
16
+ # *Time*:: Date and time at which the feed was last updated
17
+ attr_reader :updated_at
18
+
19
+ # *Array*:: Array of YouTubeG::Model::Video records
20
+ attr_reader :videos
21
+
22
+ def current_page
23
+ ((offset - 1) / max_result_count) + 1
24
+ end
25
+
26
+ # current_page + 1 or nil if there is no next page
27
+ def next_page
28
+ current_page < total_pages ? (current_page + 1) : nil
29
+ end
30
+
31
+ # current_page - 1 or nil if there is no previous page
32
+ def previous_page
33
+ current_page > 1 ? (current_page - 1) : nil
34
+ end
35
+
36
+ def total_pages
37
+ (total_result_count / max_result_count.to_f).ceil
38
+ end
39
+ end
40
+ end
41
+ end