youtube-g 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,12 @@
1
+ == 0.4.0 / 2007-12-18
2
+
3
+ * Fixed API projection in search URL [Pete Higgins]
4
+ * Fixed embeddable video searching [Pete Higgins]
5
+ * Fixed video embeddable detection [Pete Higgins]
6
+ * Fixed unique id hyphen detection [Pete Higgins, Chris Taggart]
7
+
8
+ == 0.3.0 / 2007-09-17
9
+
10
+ * Initial public release
11
+ * Birthday!
12
+
data/Manifest.txt ADDED
@@ -0,0 +1,24 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ TODO.txt
6
+ lib/youtube_g.rb
7
+ lib/youtube_g/client.rb
8
+ lib/youtube_g/logger.rb
9
+ lib/youtube_g/model/author.rb
10
+ lib/youtube_g/model/category.rb
11
+ lib/youtube_g/model/contact.rb
12
+ lib/youtube_g/model/content.rb
13
+ lib/youtube_g/model/playlist.rb
14
+ lib/youtube_g/model/rating.rb
15
+ lib/youtube_g/model/thumbnail.rb
16
+ lib/youtube_g/model/user.rb
17
+ lib/youtube_g/model/video.rb
18
+ lib/youtube_g/parser.rb
19
+ lib/youtube_g/record.rb
20
+ lib/youtube_g/request/video_search.rb
21
+ lib/youtube_g/response/video_search.rb
22
+ test/test_client.rb
23
+ test/test_video.rb
24
+ test/test_video_search.rb
data/README.txt ADDED
@@ -0,0 +1,66 @@
1
+ youtube-g
2
+ by Shane Vitarana and Walter Korman
3
+ http://rubyforge.org/projects/youtube-g/
4
+
5
+ == DESCRIPTION:
6
+
7
+ youtube-g is a pure Ruby client for the YouTube GData API.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Aims to be in parity with Google's YouTube GData API (it is currently not complete)
12
+
13
+ == SYNOPSIS:
14
+
15
+ Basic queries:
16
+
17
+ @client = YouTubeG::Client.new
18
+ @client.videos_by(:query => "penguin")
19
+ @client.videos_by(:tags => ['tiger', 'leopard'])
20
+ @client.videos_by(:categories => [:news, :sports])
21
+ @client.videos_by(:categories => [:news, :sports], :tags => ['soccer', 'football'])
22
+ @client.videos_by(:user => 'liz')
23
+
24
+ Standard feeds:
25
+
26
+ @client.videos_by(:most_viewed)
27
+ @client.videos_by(:top_rated, :time => :today)
28
+
29
+ Advanced queries (with boolean operators OR (either), AND (include), NOT (exclude)):
30
+
31
+ @client.videos_by(:categories => { :either => [:news, :sports], :exclude => [:comedy] }, :tags => { :include => ['football'], :exclude => ['soccer'] })
32
+
33
+
34
+ == REQUIREMENTS:
35
+
36
+ * None
37
+
38
+ == INSTALL:
39
+
40
+ * sudo gem install youtube-g
41
+
42
+ == LICENSE:
43
+
44
+ MIT License
45
+
46
+ Copyright (c) 2007 Shane Vitarana
47
+ Copyright (c) 2007 Walter Korman
48
+
49
+ Permission is hereby granted, free of charge, to any person obtaining
50
+ a copy of this software and associated documentation files (the
51
+ 'Software'), to deal in the Software without restriction, including
52
+ without limitation the rights to use, copy, modify, merge, publish,
53
+ distribute, sublicense, and/or sell copies of the Software, and to
54
+ permit persons to whom the Software is furnished to do so, subject to
55
+ the following conditions:
56
+
57
+ The above copyright notice and this permission notice shall be
58
+ included in all copies or substantial portions of the Software.
59
+
60
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
61
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
62
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
63
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
64
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
65
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
66
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require 'lib/youtube_g'
4
+
5
+ Hoe.new('youtube-g', YouTubeG::VERSION) do |p|
6
+ p.rubyforge_name = 'youtube-g'
7
+ p.author = ["Shane Vitarana", "Walter Korman"]
8
+ p.email = 'shanev@gmail.com'
9
+ p.summary = 'Ruby client for the YouTube GData API'
10
+ p.description = p.paragraphs_of('README.txt', 2..8).join("\n\n")
11
+ p.url = 'http://rubyforge.org/projects/youtube-g/'
12
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
13
+ end
14
+
15
+ desc 'Tag release'
16
+ task :tag do
17
+ svn_root = 'svn+ssh://drummr77@rubyforge.org/var/svn/youtube-g'
18
+ sh %(svn cp #{svn_root}/trunk #{svn_root}/tags/release-#{YouTubeG::VERSION} -m "Tag YouTubeG release #{YouTubeG::VERSION}")
19
+ end
data/TODO.txt ADDED
@@ -0,0 +1,11 @@
1
+ [ ] Tests with mocked out YouTube server
2
+ [ ] Clean up tests using Shoulda to define contexts
3
+ [ ] Consolidate requires
4
+ [ ] Allow :category and :categories for query DSL
5
+
6
+ == API Features TODO
7
+
8
+ [ ] Profile feed parsing
9
+ [ ] Playlist feeds
10
+ [ ] User subscriptions
11
+ [ ] Video comments
data/lib/youtube_g.rb ADDED
@@ -0,0 +1,18 @@
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_search'
14
+ require File.dirname(__FILE__) + '/youtube_g/response/video_search'
15
+
16
+ class YouTubeG
17
+ VERSION = '0.4.0'
18
+ end
@@ -0,0 +1,35 @@
1
+ require 'logger'
2
+
3
+ class YouTubeG
4
+ class Client
5
+ attr_accessor :logger
6
+
7
+ def initialize(logger=Logger.new(STDOUT))
8
+ @logger = logger
9
+ end
10
+
11
+ # Params can be one of :most_viewed, :top_rated, :recently_featured, :watch_on_mobile
12
+ # Or :tags, :categories, :query, :user
13
+ def videos_by(params, options={})
14
+ if params.respond_to?(:to_hash) and not params[:user]
15
+ request = YouTubeG::Request::VideoSearch.new(params)
16
+
17
+ elsif (params.respond_to?(:to_hash) && params[:user]) || (params == :favorites)
18
+ request = YouTubeG::Request::UserSearch.new(params, options)
19
+
20
+ else
21
+ request = YouTubeG::Request::StandardSearch.new(params, options)
22
+ end
23
+
24
+ logger.debug "Submitting request [url=#{request.url}]."
25
+ parser = YouTubeG::Parser::VideosFeedParser.new(request.url)
26
+ parser.parse
27
+ end
28
+
29
+ def video_by(video_id)
30
+ parser = YouTubeG::Parser::VideoFeedParser.new(video_id)
31
+ parser.parse
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ require 'logger'
2
+
3
+ class YouTubeG
4
+
5
+ # TODO: Why is this needed? Does this happen if running standalone w/o Rails?
6
+ # Anyway, isn't it easier to debug w/o the really long timestamp & log level?
7
+ # How often do you look at the timestamp and log level? Wouldn't it be nice to
8
+ # see your logger output first?
9
+
10
+ # Extension of the base ruby Logger class to restore the default log
11
+ # level and timestamp formatting which is so rudely taken forcibly
12
+ # away from us by the Rails app's use of the ActiveSupport library
13
+ # that wholesale-ly modifies the Logger's format_message method.
14
+ #
15
+ class Logger < ::Logger
16
+ private
17
+ begin
18
+ # restore original log formatting to un-screw the screwage that is
19
+ # foisted upon us by the activesupport library's clean_logger.rb
20
+ alias format_message old_format_message
21
+
22
+ rescue NameError
23
+ # nothing for now -- this means we didn't need to alias since the
24
+ # method wasn't overridden
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Author < YouTubeG::Record
4
+ attr_reader :name
5
+ attr_reader :uri
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Category < YouTubeG::Record
4
+ attr_reader :label
5
+ attr_reader :term
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Contact < YouTubeG::Record
4
+ attr_reader :status
5
+ attr_reader :username
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Content < YouTubeG::Record
4
+ attr_reader :default
5
+ attr_reader :duration
6
+ attr_reader :format
7
+ attr_reader :mime_type
8
+ attr_reader :url
9
+
10
+ alias :is_default? :default
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Playlist < YouTubeG::Record
4
+ attr_reader :description
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Rating < YouTubeG::Record
4
+ attr_reader :average
5
+ attr_reader :max
6
+ attr_reader :min
7
+ attr_reader :rater_count
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Thumbnail < YouTubeG::Record
4
+ attr_reader :url
5
+ attr_reader :height
6
+ attr_reader :width
7
+ attr_reader :time
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ class YouTubeG
2
+ module Model
3
+ class User < YouTubeG::Record
4
+ attr_reader :age
5
+ attr_reader :books
6
+ attr_reader :company
7
+ attr_reader :gender
8
+ attr_reader :hobbies
9
+ attr_reader :hometown
10
+ attr_reader :location
11
+ attr_reader :movies
12
+ attr_reader :music
13
+ attr_reader :occupation
14
+ attr_reader :relationship
15
+ attr_reader :school
16
+ attr_reader :description
17
+ attr_reader :username
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,100 @@
1
+ class YouTubeG
2
+ module Model
3
+ class Video < YouTubeG::Record
4
+ # Describes the various file formats in which a Youtube video may be
5
+ # made available and allows looking them up by format code number.
6
+ #
7
+ class Format
8
+ @@formats = Hash.new
9
+
10
+ def initialize(format_code, name)
11
+ @format_code = format_code
12
+ @name = name
13
+
14
+ @@formats[format_code] = self
15
+ end
16
+
17
+ def self.by_code(format_code)
18
+ @@formats[format_code]
19
+ end
20
+
21
+ # Flash format on YouTube site. All videos are available in this
22
+ # format.
23
+ #
24
+ FLASH = YouTubeG::Model::Video::Format.new(0, :flash)
25
+
26
+ # RTSP streaming URL for mobile video playback. H.263 video (176x144)
27
+ # and AMR audio.
28
+ #
29
+ RTSP = YouTubeG::Model::Video::Format.new(1, :rtsp)
30
+
31
+ # HTTP URL to the embeddable player (SWF) for this video. This format
32
+ # is not available for a video that is not embeddable.
33
+ #
34
+ SWF = YouTubeG::Model::Video::Format.new(5, :swf)
35
+ end
36
+
37
+ attr_reader :duration
38
+ attr_reader :noembed
39
+ attr_reader :position
40
+ attr_reader :racy
41
+ attr_reader :statistics
42
+
43
+ attr_reader :video_id
44
+ attr_reader :published_at
45
+ attr_reader :updated_at
46
+ attr_reader :categories
47
+ attr_reader :keywords
48
+ attr_reader :title
49
+ attr_reader :html_content
50
+ attr_reader :author
51
+
52
+ # YouTubeG::Model::Content records describing the individual media content
53
+ # data available for this video. Most, but not all, videos offer this.
54
+ attr_reader :media_content
55
+
56
+ attr_reader :thumbnails # YouTubeG::Model::Thumbnail records
57
+ attr_reader :player_url
58
+ attr_reader :rating
59
+ attr_reader :view_count
60
+
61
+ # TODO:
62
+ # self atom feed
63
+ # alternate youtube watch url
64
+ # responses feed
65
+ # related feed
66
+ # comments feedLink
67
+
68
+ # For convenience, the video_id with the URL stripped out, useful for searching for the video again
69
+ # without having to store it anywhere. A regular query search, with this id will return the same video.
70
+ # http://gdata.youtube.com/feeds/videos/ZTUVgYoeN_o
71
+ def unique_id
72
+ video_id.match(/videos\/([^<]+)/).captures.first
73
+ end
74
+
75
+ def can_embed?
76
+ not @noembed
77
+ end
78
+
79
+ def default_media_content
80
+ @media_content.find { |c| c.is_default? }
81
+ end
82
+
83
+ def embed_html(width = 425, height = 350)
84
+ <<EDOC
85
+ <object width="#{width}" height="#{height}">
86
+ <param name="movie" value="#{embed_url}"></param>
87
+ <param name="wmode" value="transparent"></param>
88
+ <embed src="#{embed_url}" type="application/x-shockwave-flash"
89
+ wmode="transparent" width="#{width}" height="#{height}"></embed>
90
+ </object>
91
+ EDOC
92
+ end
93
+
94
+ def embed_url
95
+ @player_url.sub('watch?', '').sub('=', '/')
96
+ end
97
+
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,163 @@
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 = entry.elements["yt:statistics"].attributes["viewCount"].to_i
95
+
96
+ noembed = entry.elements["yt:noembed"] ? true : false
97
+
98
+ YouTubeG::Model::Video.new(
99
+ :video_id => video_id,
100
+ :published_at => published_at,
101
+ :updated_at => updated_at,
102
+ :categories => categories,
103
+ :keywords => keywords,
104
+ :title => title,
105
+ :html_content => html_content,
106
+ :author => author,
107
+ :description => description,
108
+ :duration => duration,
109
+ :media_content => media_content,
110
+ :player_url => player_url,
111
+ :thumbnails => thumbnails,
112
+ :rating => rating,
113
+ :view_count => view_count,
114
+ :noembed => noembed)
115
+ end
116
+
117
+ def parse_media_content (media_content_element)
118
+ content_url = media_content_element.attributes["url"]
119
+ format_code = media_content_element.attributes["yt:format"].to_i
120
+ format = YouTubeG::Model::Video::Format.by_code(format_code)
121
+ duration = media_content_element.attributes["duration"].to_i
122
+ mime_type = media_content_element.attributes["type"]
123
+ default = (media_content_element.attributes["isDefault"] == "true")
124
+
125
+ YouTubeG::Model::Content.new(
126
+ :url => content_url,
127
+ :format => format,
128
+ :duration => duration,
129
+ :mime_type => mime_type,
130
+ :default => default)
131
+ end
132
+ end
133
+
134
+ class VideosFeedParser < VideoFeedParser
135
+
136
+ private
137
+ def parse_content(content)
138
+ doc = REXML::Document.new(content)
139
+ feed = doc.elements["feed"]
140
+
141
+ feed_id = feed.elements["id"].text
142
+ updated_at = Time.parse(feed.elements["updated"].text)
143
+ total_result_count = feed.elements["openSearch:totalResults"].text.to_i
144
+ offset = feed.elements["openSearch:startIndex"].text.to_i
145
+ max_result_count = feed.elements["openSearch:itemsPerPage"].text.to_i
146
+
147
+ videos = []
148
+ feed.elements.each("entry") do |entry|
149
+ videos << parse_entry(entry)
150
+ end
151
+
152
+ YouTubeG::Response::VideoSearch.new(
153
+ :feed_id => feed_id,
154
+ :updated_at => updated_at,
155
+ :total_result_count => total_result_count,
156
+ :offset => offset,
157
+ :max_result_count => max_result_count,
158
+ :videos => videos)
159
+ end
160
+ end
161
+
162
+ end
163
+ 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,162 @@
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)
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
+
58
+ def initialize(params={})
59
+ # XXX I think we want to delete the line below
60
+ return if params.nil?
61
+
62
+ # initialize our various member data to avoid warnings and so we'll
63
+ # automatically fall back to the youtube api defaults
64
+ @max_results = nil
65
+ @order_by = nil
66
+ @offset = nil
67
+ @query = nil
68
+ @response_format = nil
69
+ @video_format = nil
70
+
71
+ # build up the url corresponding to this request
72
+ @url = base_url
73
+
74
+ # http://gdata.youtube.com/feeds/videos/T7YazwP8GtY
75
+ return @url << "/" << params[:video_id] if params[:video_id]
76
+
77
+ @url << "/-/" if (params[:categories] || params[:tags])
78
+ @url << categories_to_params(params.delete(:categories)) if params[:categories]
79
+ @url << tags_to_params(params.delete(:tags)) if params[:tags]
80
+
81
+ params.each do |key, value|
82
+ name = key.to_s
83
+ instance_variable_set("@#{name}", value) if respond_to?(name)
84
+ end
85
+
86
+ if( params[ :only_embeddable ] )
87
+ @video_format = ONLY_EMBEDDABLE
88
+ end
89
+
90
+ @url << build_query_params(to_youtube_params)
91
+ end
92
+
93
+ def base_url
94
+ super << "videos"
95
+ end
96
+
97
+ def to_youtube_params
98
+ {
99
+ 'max-results' => @max_results,
100
+ 'orderby' => @order_by,
101
+ 'start-index' => @offset,
102
+ 'vq' => @query,
103
+ 'alt' => @response_format,
104
+ 'format' => @video_format
105
+ }
106
+ end
107
+
108
+ private
109
+ # Convert category symbols into strings and build the URL. GData requires categories to be capitalized.
110
+ # Categories defined like: categories => { :include => [:news], :exclude => [:sports], :either => [..] }
111
+ # or like: categories => [:news, :sports]
112
+ def categories_to_params(categories)
113
+ if categories.respond_to?(:keys) and categories.respond_to?(:[])
114
+ s = ""
115
+ s << categories[:either].map { |c| c.to_s.capitalize }.join("%7C") << '/' if categories[:either]
116
+ s << categories[:include].map { |c| c.to_s.capitalize }.join("/") << '/' if categories[:include]
117
+ s << ("-" << categories[:exclude].map { |c| c.to_s.capitalize }.join("/-")) << '/' if categories[:exclude]
118
+ s
119
+ else
120
+ categories.map { |c| c.to_s.capitalize }.join("/") << '/'
121
+ end
122
+ end
123
+
124
+ # Tags defined like: tags => { :include => [:football], :exclude => [:soccer], :either => [:polo, :tennis] }
125
+ # or tags => [:football, :soccer]
126
+ def tags_to_params(tags)
127
+ if tags.respond_to?(:keys) and tags.respond_to?(:[])
128
+ s = ""
129
+ s << tags[:either].map { |t| CGI.escape(t.to_s) }.join("%7C") << '/' if tags[:either]
130
+ s << tags[:include].map { |t| CGI.escape(t.to_s) }.join("/") << '/' if tags[:include]
131
+ s << ("-" << tags[:exclude].map { |t| CGI.escape(t.to_s) }.join("/-")) << '/' if tags[:exclude]
132
+ s
133
+ else
134
+ tags.map { |t| CGI.escape(t.to_s) }.join("/") << '/'
135
+ end
136
+ end
137
+
138
+ def build_query_params(params)
139
+ # nothing to do if there are no params
140
+ return '' if (!params || params.empty?)
141
+
142
+ # build up the query param string, tacking on every key/value
143
+ # pair for which the value is non-nil
144
+ u = '?'
145
+ item_count = 0
146
+ params.keys.each do |key|
147
+ value = params[key]
148
+ next if value.nil?
149
+
150
+ u << '&' if (item_count > 0)
151
+ u << "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
152
+ item_count += 1
153
+ end
154
+
155
+ # if we found no non-nil values, we've got no params so just
156
+ # return an empty string
157
+ (item_count == 0) ? '' : u
158
+ end
159
+
160
+ end
161
+ end
162
+ 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
@@ -0,0 +1,216 @@
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", response.feed_id
16
+ assert_equal 25, response.max_result_count
17
+ assert_equal 24, 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", 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
+ # TODO: this doesn't work because the returned feed is in an unknown format
39
+ # def test_should_get_video_for_search_by_video_id
40
+ # response = @client.videos_by(:video_id => "T7YazwP8GtY")
41
+ # response.videos.each { |v| assert_valid_video v }
42
+ # end
43
+
44
+ def test_should_get_videos_for_one_tag
45
+ response = @client.videos_by(:tags => ['panther'])
46
+ response.videos.each { |v| assert_valid_video v }
47
+ end
48
+
49
+ def test_should_get_videos_for_multiple_tags
50
+ response = @client.videos_by(:tags => ['tiger', 'leopard'])
51
+ response.videos.each { |v| assert_valid_video v }
52
+ end
53
+
54
+ def test_should_get_videos_for_one_category
55
+ response = @client.videos_by(:categories => [:news])
56
+ response.videos.each { |v| assert_valid_video v }
57
+ end
58
+
59
+ def test_should_get_videos_for_multiple_categories
60
+ response = @client.videos_by(:categories => [:news, :sports])
61
+ response.videos.each { |v| assert_valid_video v }
62
+ end
63
+
64
+ # TODO: Need to do more specific checking in these tests
65
+ # Currently, if a URL is valid, and videos are found, the test passes regardless of search criteria
66
+ def test_should_get_videos_for_categories_and_tags
67
+ response = @client.videos_by(:categories => [:news, :sports], :tags => ['soccer', 'football'])
68
+ response.videos.each { |v| assert_valid_video v }
69
+ end
70
+
71
+ def test_should_get_most_viewed_videos
72
+ response = @client.videos_by(:most_viewed)
73
+ response.videos.each { |v| assert_valid_video v }
74
+ end
75
+
76
+ def test_should_get_top_rated_videos_for_today
77
+ response = @client.videos_by(:top_rated, :time => :today)
78
+ response.videos.each { |v| assert_valid_video v }
79
+ end
80
+
81
+ def test_should_get_videos_for_categories_and_tags_with_category_boolean_operators
82
+ response = @client.videos_by(:categories => { :either => [:news, :sports], :exclude => [:comedy] },
83
+ :tags => { :include => ['football'], :exclude => ['soccer'] })
84
+ response.videos.each { |v| assert_valid_video v }
85
+ end
86
+
87
+ def test_should_get_videos_for_categories_and_tags_with_tag_boolean_operators
88
+ response = @client.videos_by(:categories => { :either => [:news, :sports], :exclude => [:comedy] },
89
+ :tags => { :either => ['football', 'soccer', 'polo'] })
90
+ response.videos.each { |v| assert_valid_video v }
91
+ end
92
+
93
+ def test_should_get_videos_by_user
94
+ response = @client.videos_by(:user => 'liz')
95
+ response.videos.each { |v| assert_valid_video v }
96
+ end
97
+
98
+ # HTTP 403 Error
99
+ # def test_should_get_favorite_videos_by_user
100
+ # response = @client.videos_by(:favorites, :user => 'liz')
101
+ # response.videos.each { |v| assert_valid_video v }
102
+ # end
103
+
104
+ def test_should_get_videos_for_query_search_with_categories_excluded
105
+ response = @client.videos_by(:query => 'bench press', :categories => { :exclude => [:comedy, :entertainment] },
106
+ :max_results => 10)
107
+ assert_equal "<object width=\"425\" height=\"350\">\n <param name=\"movie\" value=\"http://www.youtube.com/v/wOnP_oAXUMA\"></param>\n <param name=\"wmode\" value=\"transparent\"></param>\n <embed src=\"http://www.youtube.com/v/wOnP_oAXUMA\" type=\"application/x-shockwave-flash\" \n wmode=\"transparent\" width=\"425\" height=\"350\"></embed>\n</object>\n", response.videos.first.embed_html
108
+ response.videos.each { |v| assert_valid_video v }
109
+ end
110
+
111
+ def test_should_be_able_to_pass_in_logger
112
+ @client = YouTubeG::Client.new(Logger.new(STDOUT))
113
+ assert_not_nil @client.logger
114
+ end
115
+
116
+ def test_should_create_logger_if_not_passed_in
117
+ @client = YouTubeG::Client.new
118
+ assert_not_nil @client.logger
119
+ end
120
+
121
+ def test_should_determine_if_nonembeddable_video_is_embeddable
122
+ response = @client.videos_by(:query => "avril lavigne girlfriend")
123
+
124
+ video = response.videos.first
125
+ assert !video.can_embed?
126
+ end
127
+
128
+ def test_should_determine_if_embeddable_video_is_embeddable
129
+ response = @client.videos_by(:query => "strongbad")
130
+
131
+ video = response.videos.first
132
+ assert video.can_embed?
133
+ end
134
+
135
+ def test_should_retrieve_video_by_id
136
+ video = @client.video_by("http://gdata.youtube.com/feeds/videos/EkF4JD2rO3Q")
137
+ assert_valid_video video
138
+ end
139
+
140
+ private
141
+
142
+ def assert_valid_video (video)
143
+ # pp video
144
+
145
+ # check general attributes
146
+ assert_instance_of YouTubeG::Model::Video, video
147
+ assert_instance_of Fixnum, video.duration
148
+ assert(video.duration > 0)
149
+ #assert_match(/^<div style=.*?<\/div>/m, video.html_content)
150
+ assert_instance_of String, video.html_content
151
+
152
+ # validate media content records
153
+ video.media_content.each do |media_content|
154
+ # http://www.youtube.com/v/IHVaXG1thXM
155
+ assert_valid_url media_content.url
156
+ assert(media_content.duration > 0)
157
+ assert_instance_of YouTubeG::Model::Video::Format, media_content.format
158
+ assert_instance_of String, media_content.mime_type
159
+ assert_match(/^[^\/]+\/[^\/]+$/, media_content.mime_type)
160
+ end
161
+
162
+ default_content = video.default_media_content
163
+ if default_content
164
+ assert_instance_of YouTubeG::Model::Content, default_content
165
+ assert default_content.is_default?
166
+ end
167
+
168
+ # validate keywords
169
+ video.keywords.each { |kw| assert_instance_of(String, kw) }
170
+
171
+ # http://www.youtube.com/watch?v=IHVaXG1thXM
172
+ assert_valid_url video.player_url
173
+ assert_instance_of Time, video.published_at
174
+
175
+ # validate optionally-present rating
176
+ if video.rating
177
+ assert_instance_of YouTubeG::Model::Rating, video.rating
178
+ assert_instance_of Float, video.rating.average
179
+ assert_instance_of Fixnum, video.rating.max
180
+ assert_instance_of Fixnum, video.rating.min
181
+ assert_instance_of Fixnum, video.rating.rater_count
182
+ end
183
+
184
+ # validate thumbnails
185
+ assert(video.thumbnails.size > 0)
186
+
187
+ assert_not_nil video.title
188
+ assert_instance_of String, video.title
189
+ assert(video.title.length > 0)
190
+
191
+ assert_instance_of Time, video.updated_at
192
+ # http://gdata.youtube.com/feeds/videos/IHVaXG1thXM
193
+ assert_valid_url video.video_id
194
+ assert_instance_of Fixnum, video.view_count
195
+
196
+ # validate author
197
+ assert_instance_of YouTubeG::Model::Author, video.author
198
+ assert_instance_of String, video.author.name
199
+ assert(video.author.name.length > 0)
200
+ assert_valid_url video.author.uri
201
+
202
+ # validate categories
203
+ video.categories.each do |cat|
204
+ assert_instance_of YouTubeG::Model::Category, cat
205
+ assert_instance_of String, cat.label
206
+ assert_instance_of String, cat.term
207
+ end
208
+ end
209
+
210
+ def assert_valid_url (url)
211
+ URI::parse(url)
212
+ return true
213
+ rescue
214
+ return false
215
+ end
216
+ end
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'pp'
4
+
5
+ require 'youtube_g'
6
+
7
+ class TestVideo < Test::Unit::TestCase
8
+
9
+ def test_should_extract_unique_id_from_video_id
10
+ video = YouTubeG::Model::Video.new(:video_id => "http://gdata.youtube.com/feeds/videos/ZTUVgYoeN_o")
11
+ assert_equal "ZTUVgYoeN_o", video.unique_id
12
+ end
13
+
14
+ def test_should_extract_unique_id_with_hypen_from_video_id
15
+ video = YouTubeG::Model::Video.new(:video_id => "http://gdata.youtube.com/feeds/videos/BDqs-OZWw9o")
16
+ assert_equal "BDqs-OZWw9o", video.unique_id
17
+ end
18
+ end
@@ -0,0 +1,114 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'pp'
4
+
5
+ require 'youtube_g'
6
+
7
+ class TestVideoSearch < Test::Unit::TestCase
8
+
9
+ def test_should_build_basic_query_url
10
+ request = YouTubeG::Request::VideoSearch.new(:query => "penguin")
11
+ assert_equal "http://gdata.youtube.com/feeds/api/videos?vq=penguin", request.url
12
+ end
13
+
14
+ def test_should_build_multiword_metasearch_query_url
15
+ request = YouTubeG::Request::VideoSearch.new(:query => 'christina ricci')
16
+ assert_equal "http://gdata.youtube.com/feeds/api/videos?vq=christina+ricci", request.url
17
+ end
18
+
19
+ def test_should_build_video_id_url
20
+ request = YouTubeG::Request::VideoSearch.new(:video_id => 'T7YazwP8GtY')
21
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/T7YazwP8GtY", request.url
22
+ end
23
+
24
+ def test_should_build_one_tag_querl_url
25
+ request = YouTubeG::Request::VideoSearch.new(:tags => ['panther'])
26
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/panther/", request.url
27
+ end
28
+
29
+ def test_should_build_multiple_tags_query_url
30
+ request = YouTubeG::Request::VideoSearch.new(:tags => ['tiger', 'leopard'])
31
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/tiger/leopard/", request.url
32
+ end
33
+
34
+ def test_should_build_one_category_query_url
35
+ request = YouTubeG::Request::VideoSearch.new(:categories => [:news])
36
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/News/", request.url
37
+ end
38
+
39
+ def test_should_build_multiple_categories_query_url
40
+ request = YouTubeG::Request::VideoSearch.new(:categories => [:news, :sports])
41
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/News/Sports/", request.url
42
+ end
43
+
44
+ def test_should_build_categories_and_tags_query_url
45
+ request = YouTubeG::Request::VideoSearch.new(:categories => [:news, :sports], :tags => ['soccer', 'football'])
46
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/News/Sports/soccer/football/", request.url
47
+ end
48
+
49
+ def test_should_build_categories_and_tags_url_with_max_results
50
+ request = YouTubeG::Request::VideoSearch.new(:categories => [:music], :tags => ['classic', 'rock'], :max_results => 2)
51
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/Music/classic/rock/?max-results=2", request.url
52
+ end
53
+
54
+ # -- Standard Feeds --------------------------------------------------------------------------------
55
+
56
+ def test_should_build_url_for_most_viewed
57
+ request = YouTubeG::Request::StandardSearch.new(:most_viewed)
58
+ assert_equal "http://gdata.youtube.com/feeds/api/standardfeeds/most_viewed", request.url
59
+ end
60
+
61
+ def test_should_raise_exception_for_invalid_type
62
+ assert_raise RuntimeError do
63
+ request = YouTubeG::Request::StandardSearch.new(:most_viewed_yo)
64
+ end
65
+ end
66
+
67
+ def test_should_build_url_for_top_rated_for_today
68
+ request = YouTubeG::Request::StandardSearch.new(:top_rated, :time => :today)
69
+ assert_equal "http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?time=today", request.url
70
+ end
71
+
72
+ # -- Complex Video Queries -------------------------------------------------------------------------
73
+
74
+ def test_should_build_url_for_boolean_or_case_for_categories
75
+ request = YouTubeG::Request::VideoSearch.new(:categories => { :either => [:news, :sports] })
76
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/News%7CSports/", request.url
77
+ end
78
+
79
+ def test_should_build_url_for_boolean_or_and_exclude_case_for_categories
80
+ request = YouTubeG::Request::VideoSearch.new(:categories => { :either => [:news, :sports], :exclude => [:comedy] })
81
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/News%7CSports/-Comedy/", request.url
82
+ end
83
+
84
+ def test_should_build_url_for_exclude_case_for_tags
85
+ request = YouTubeG::Request::VideoSearch.new(:categories => { :either => [:news, :sports], :exclude => [:comedy] },
86
+ :tags => { :include => ['football'], :exclude => ['soccer'] })
87
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/News%7CSports/-Comedy/football/-soccer/", request.url
88
+ end
89
+
90
+ def test_should_build_url_for_either_case_for_tags
91
+ request = YouTubeG::Request::VideoSearch.new(:categories => { :either => [:news, :sports], :exclude => [:comedy] },
92
+ :tags => { :either => ['soccer', 'football', 'donkey'] })
93
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/News%7CSports/-Comedy/soccer%7Cfootball%7Cdonkey/", request.url
94
+ end
95
+
96
+ def test_should_build_url_for_query_search_with_categories_excluded
97
+ request = YouTubeG::Request::VideoSearch.new(:query => 'bench press',
98
+ :categories => { :exclude => [:comedy, :entertainment] },
99
+ :max_results => 10)
100
+ assert_equal "http://gdata.youtube.com/feeds/api/videos/-/-Comedy/-Entertainment/?vq=bench+press&max-results=10", request.url
101
+ end
102
+
103
+ # -- User Queries ---------------------------------------------------------------------------------
104
+
105
+ def test_should_build_url_for_videos_by_user
106
+ request = YouTubeG::Request::UserSearch.new(:user => 'liz')
107
+ assert_equal "http://gdata.youtube.com/feeds/api/users/liz/uploads", request.url
108
+ end
109
+
110
+ def test_should_build_url_for_favorite_videos_by_user
111
+ request = YouTubeG::Request::UserSearch.new(:favorites, :user => 'liz')
112
+ assert_equal "http://gdata.youtube.com/feeds/api/users/liz/favorites", request.url
113
+ end
114
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: youtube-g
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.4.0
7
+ date: 2007-12-18 00:00:00 -06:00
8
+ summary: Ruby client for the YouTube GData API
9
+ require_paths:
10
+ - lib
11
+ email: shanev@gmail.com
12
+ homepage: http://rubyforge.org/projects/youtube-g/
13
+ rubyforge_project: youtube-g
14
+ description: "== FEATURES/PROBLEMS: * Aims to be in parity with Google's YouTube GData API (it is currently not complete) == SYNOPSIS: Basic queries: @client = YouTubeG::Client.new @client.videos_by(:query => \"penguin\") @client.videos_by(:tags => ['tiger', 'leopard']) @client.videos_by(:categories => [:news, :sports]) @client.videos_by(:categories => [:news, :sports], :tags => ['soccer', 'football']) @client.videos_by(:user => 'liz') Standard feeds: @client.videos_by(:most_viewed) @client.videos_by(:top_rated, :time => :today) Advanced queries (with boolean operators OR (either), AND (include), NOT (exclude)): @client.videos_by(:categories => { :either => [:news, :sports], :exclude => [:comedy] }, :tags => { :include => ['football'], :exclude => ['soccer'] }) == REQUIREMENTS: * None == INSTALL: * sudo gem install youtube-g"
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Shane Vitarana
31
+ - Walter Korman
32
+ files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ - Rakefile
37
+ - TODO.txt
38
+ - lib/youtube_g.rb
39
+ - lib/youtube_g/client.rb
40
+ - lib/youtube_g/logger.rb
41
+ - lib/youtube_g/model/author.rb
42
+ - lib/youtube_g/model/category.rb
43
+ - lib/youtube_g/model/contact.rb
44
+ - lib/youtube_g/model/content.rb
45
+ - lib/youtube_g/model/playlist.rb
46
+ - lib/youtube_g/model/rating.rb
47
+ - lib/youtube_g/model/thumbnail.rb
48
+ - lib/youtube_g/model/user.rb
49
+ - lib/youtube_g/model/video.rb
50
+ - lib/youtube_g/parser.rb
51
+ - lib/youtube_g/record.rb
52
+ - lib/youtube_g/request/video_search.rb
53
+ - lib/youtube_g/response/video_search.rb
54
+ - test/test_client.rb
55
+ - test/test_video.rb
56
+ - test/test_video_search.rb
57
+ test_files:
58
+ - test/test_client.rb
59
+ - test/test_video.rb
60
+ - test/test_video_search.rb
61
+ rdoc_options:
62
+ - --main
63
+ - README.txt
64
+ extra_rdoc_files:
65
+ - History.txt
66
+ - Manifest.txt
67
+ - README.txt
68
+ - TODO.txt
69
+ executables: []
70
+
71
+ extensions: []
72
+
73
+ requirements: []
74
+
75
+ dependencies:
76
+ - !ruby/object:Gem::Dependency
77
+ name: hoe
78
+ version_requirement:
79
+ version_requirements: !ruby/object:Gem::Version::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 1.3.0
84
+ version: