tmm1-youtube-g 0.4.5

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,165 @@
1
+ require 'cgi'
2
+ require 'open-uri'
3
+ require 'rexml/document'
4
+
5
+ class YouTubeG
6
+ module Parser
7
+ class FeedParser
8
+ def initialize(url)
9
+ @url = url
10
+ end
11
+
12
+ def parse
13
+ parse_content open(@url).read
14
+ end
15
+ end
16
+
17
+ class VideoFeedParser < FeedParser
18
+
19
+ def parse_content(content)
20
+ doc = REXML::Document.new(content)
21
+ entry = doc.elements["entry"]
22
+
23
+ parse_entry(entry)
24
+ end
25
+
26
+ protected
27
+ def parse_entry(entry)
28
+ video_id = entry.elements["id"].text
29
+ published_at = Time.parse(entry.elements["published"].text)
30
+ updated_at = Time.parse(entry.elements["updated"].text)
31
+
32
+ # parse the category and keyword lists
33
+ categories = []
34
+ keywords = []
35
+ entry.elements.each("category") do |category|
36
+ # determine if it's really a category, or just a keyword
37
+ scheme = category.attributes["scheme"]
38
+ if (scheme =~ /\/categories\.cat$/)
39
+ # it's a category
40
+ categories << YouTubeG::Model::Category.new(
41
+ :term => category.attributes["term"],
42
+ :label => category.attributes["label"])
43
+
44
+ elsif (scheme =~ /\/keywords\.cat$/)
45
+ # it's a keyword
46
+ keywords << category.attributes["term"]
47
+ end
48
+ end
49
+
50
+ title = entry.elements["title"].text
51
+ html_content = entry.elements["content"].text
52
+
53
+ # parse the author
54
+ author_element = entry.elements["author"]
55
+ author = nil
56
+ if author_element
57
+ author = YouTubeG::Model::Author.new(
58
+ :name => author_element.elements["name"].text,
59
+ :uri => author_element.elements["uri"].text)
60
+ end
61
+
62
+ media_group = entry.elements["media:group"]
63
+ description = media_group.elements["media:description"].text
64
+ duration = media_group.elements["yt:duration"].attributes["seconds"].to_i
65
+
66
+ media_content = []
67
+ media_group.elements.each("media:content") do |mce|
68
+ media_content << parse_media_content(mce)
69
+ end
70
+
71
+ player_url = media_group.elements["media:player"].attributes["url"]
72
+
73
+ # parse thumbnails
74
+ thumbnails = []
75
+ media_group.elements.each("media:thumbnail") do |thumb_element|
76
+ # TODO: convert time HH:MM:ss string to seconds?
77
+ thumbnails << YouTubeG::Model::Thumbnail.new(
78
+ :url => thumb_element.attributes["url"],
79
+ :height => thumb_element.attributes["height"].to_i,
80
+ :width => thumb_element.attributes["width"].to_i,
81
+ :time => thumb_element.attributes["time"])
82
+ end
83
+
84
+ rating_element = entry.elements["gd:rating"]
85
+ rating = nil
86
+ if rating_element
87
+ rating = YouTubeG::Model::Rating.new(
88
+ :min => rating_element.attributes["min"].to_i,
89
+ :max => rating_element.attributes["max"].to_i,
90
+ :rater_count => rating_element.attributes["numRaters"].to_i,
91
+ :average => rating_element.attributes["average"].to_f)
92
+ end
93
+
94
+ view_count = (el = entry.elements["yt:statistics"]) ? el.attributes["viewCount"].to_i : 0
95
+
96
+ noembed = entry.elements["yt:noembed"] ? true : false
97
+ racy = entry.elements["yt:racy"] ? true : false
98
+
99
+ YouTubeG::Model::Video.new(
100
+ :video_id => video_id,
101
+ :published_at => published_at,
102
+ :updated_at => updated_at,
103
+ :categories => categories,
104
+ :keywords => keywords,
105
+ :title => title,
106
+ :html_content => html_content,
107
+ :author => author,
108
+ :description => description,
109
+ :duration => duration,
110
+ :media_content => media_content,
111
+ :player_url => player_url,
112
+ :thumbnails => thumbnails,
113
+ :rating => rating,
114
+ :view_count => view_count,
115
+ :noembed => noembed,
116
+ :racy => racy)
117
+ end
118
+
119
+ def parse_media_content (media_content_element)
120
+ content_url = media_content_element.attributes["url"]
121
+ format_code = media_content_element.attributes["yt:format"].to_i
122
+ format = YouTubeG::Model::Video::Format.by_code(format_code)
123
+ duration = media_content_element.attributes["duration"].to_i
124
+ mime_type = media_content_element.attributes["type"]
125
+ default = (media_content_element.attributes["isDefault"] == "true")
126
+
127
+ YouTubeG::Model::Content.new(
128
+ :url => content_url,
129
+ :format => format,
130
+ :duration => duration,
131
+ :mime_type => mime_type,
132
+ :default => default)
133
+ end
134
+ end
135
+
136
+ class VideosFeedParser < VideoFeedParser
137
+
138
+ private
139
+ def parse_content(content)
140
+ doc = REXML::Document.new(content)
141
+ feed = doc.elements["feed"]
142
+
143
+ feed_id = feed.elements["id"].text
144
+ updated_at = Time.parse(feed.elements["updated"].text)
145
+ total_result_count = feed.elements["openSearch:totalResults"].text.to_i
146
+ offset = feed.elements["openSearch:startIndex"].text.to_i
147
+ max_result_count = feed.elements["openSearch:itemsPerPage"].text.to_i
148
+
149
+ videos = []
150
+ feed.elements.each("entry") do |entry|
151
+ videos << parse_entry(entry)
152
+ end
153
+
154
+ YouTubeG::Response::VideoSearch.new(
155
+ :feed_id => feed_id,
156
+ :updated_at => updated_at,
157
+ :total_result_count => total_result_count,
158
+ :offset => offset,
159
+ :max_result_count => max_result_count,
160
+ :videos => videos)
161
+ end
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,12 @@
1
+ class YouTubeG
2
+ class Record
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,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,112 @@
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
+ uploadBody = generate_upload_body(boundary, video_xml, data)
47
+
48
+ uploadHeader = {
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" => "#{uploadBody.length}"
55
+ }
56
+
57
+ Net::HTTP.start(base_url) do |upload|
58
+ response = upload.post('/feeds/api/users/' << @user << '/uploads', uploadBody, uploadHeader)
59
+ xml = REXML::Document.new(response.body)
60
+ return xml.elements["//id"].text[/videos\/(.+)/, 1]
61
+ end
62
+
63
+ end
64
+
65
+ private
66
+
67
+ def base_url
68
+ "uploads.gdata.youtube.com"
69
+ end
70
+
71
+ def boundary
72
+ "An43094fu"
73
+ end
74
+
75
+ def auth_token
76
+ unless @auth_token
77
+ http = Net::HTTP.new("www.google.com", 443)
78
+ http.use_ssl = true
79
+ body = "Email=#{CGI::escape @user}&Passwd=#{CGI::escape @pass}&service=youtube&source=#{CGI::escape @client_id}"
80
+ response = http.post("/youtube/accounts/ClientLogin", body, "Content-Type" => "application/x-www-form-urlencoded")
81
+ raise UploadError, response.body[/Error=(.+)/,1] if response.code.to_i != 200
82
+ @auth_token = response.body[/Auth=(.+)/, 1]
83
+
84
+ end
85
+ @auth_token
86
+ end
87
+
88
+ def video_xml
89
+ %[<?xml version="1.0"?>
90
+ <entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007">
91
+ <media:group>
92
+ <media:title type="plain">#{@opts[:title]}</media:title>
93
+ <media:description type="plain">#{@opts[:description]}</media:description>
94
+ <media:category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">#{@opts[:category]}</media:category>
95
+ <media:keywords>#{@opts[:keywords].join ","}</media:keywords>
96
+ </media:group></entry> ]
97
+ end
98
+
99
+ def generate_upload_body(boundary, video_xml, data)
100
+ uploadBody = ""
101
+ uploadBody << "--#{boundary}\r\n"
102
+ uploadBody << "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n"
103
+ uploadBody << video_xml
104
+ uploadBody << "\r\n--#{boundary}\r\n"
105
+ uploadBody << "Content-Type: #{@opts[:mime_type]}\r\nContent-Transfer-Encoding: binary\r\n\r\n"
106
+ uploadBody << data
107
+ uploadBody << "\r\n--#{boundary}--\r\n"
108
+ end
109
+
110
+ end
111
+ end
112
+ end