# yahoo-video -- provides OO access to the Yahoo! Video web site content # Copyright (C) 2006 Walter Korman # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA require 'open-uri' require 'rexml/document' module YahooVideo # An exception thrown by the YahooVideo module should something untoward # occur. class YahooVideoException < RuntimeError def initialize (message) super(message) end end # A generic record that initializes instance variables from the supplied hash # mapping symbol names to their respective values. class Record def initialize (params) if params params.each do |key, value| name = key.to_s instance_variable_set("@#{name}", value) if respond_to?(name) end end end end # the maximum number of results that can be requested from the search # service with a single request. MAX_RESULTS_PER_PAGE = 50 # Describes a search request's parameters for submission to the Yahoo! Video # search service via Client#search. Parameters are passed to the # constructor as hash key/value pairs. class SearchRequest < Record # Optional: a boolean value representing whether adult-only results are # permitted in the result set, defaulting to false. attr_reader :adult_ok # Required: the application id to associate with the request, as obtained # from http://developer.yahoo.com/faq/index.html#appid. attr_reader :app_id # the name of the callback function to wrap around JSON data if requested # in the output format. since we require xml for parsing and aren't # making a request via javascript, this doesn't pertain to us. # attr_reader :callback # Optional: a list of the output formats by which the search should be # constrained. One of any (default), +avi+, +flash+, +mpeg+, # +msmedia+, +quicktime+, +realmedia+. attr_reader :format # Optional: the format for the output. since we require xml for parsing # this doesn't pertain to us and we always request only xml which also # happens to be the default. # attr_reader :output # Required: the query string representing the query to search for. # Supports +, to include terms, - to exclude terms, and # quotes around "exact phrase" matching. attr_reader :query # Optional: the number of results to return, defaulting to 10 and at # most 50. attr_reader :results # Optional: a list of the site domains by which the search should be # constrained, e.g., www.yahoo.com. attr_reader :site # Optional: The starting result index to return (1-based), defaulting # to 1. The finishing position (start + results - 1) cannot # exceed 1000. attr_reader :start # Optional: the kind of search to execute, one of all (default), # any, or phrase. # # * all: returns results with all query terms. # * any: returns results with one or more of the query terms. # * phrase: returns results containing the query terms as a phrase. attr_reader :type end # Describes a SearchResult file thumbnail image. Parameters are passed to # the constructor as hash key/value pairs. class Thumbnail < Record # the url of the video thumbnail image. attr_reader :url # the width of the video thumbnail image in pixels. attr_reader :width # the height of the video thumbnail image in pixels. attr_reader :height end # Describes a single search result as returned by the Yahoo! Video search # service. Parameters are passed to the constructor as hash key/value # pairs. class SearchResult < Record # the number of channels in the audio. usually 1 (mono) or 2 (stereo). attr_reader :channels # the url for linking to the video file. attr_reader :click_url # the copyright owner. attr_reader :copyright # the duration of the video file in seconds. attr_reader :duration # the format of the video file -- one of +avi+, +flash+, +mpeg+, # +msmedia+, +quicktime+, or +realmedia+. attr_reader :file_format # the size of the video file in bytes. attr_reader :file_size # the height of the keyframe Yahoo! extracted from the video in pixels. # the video has this height if rendered at 100%. attr_reader :height # the creator of the video file. attr_reader :publisher # the url of the web page hosting the content. attr_reader :referer_url # Provides any restrictions for this media object. Restrictions include # +noframe+ and +noinline+. # # * +noframe+ means that you should not display it with a framed page on # your site. # * +noinline+ means that you should not inline the object in the frame up # top (it won't work because the site has some protection based on the # "referrer" field). attr_reader :restrictions # whether the video file is streaming (true) or not (false). attr_reader :streaming # summary text description associated with the video file. attr_reader :summary # the Thumbnail record describing the url of the thumbnail file and its # height and width in pixels. attr_reader :thumbnail # the title of the video file. attr_reader :title # the url for the video file or stream. attr_reader :url # the width of the keyframe Yahoo! extracted from the video in pixels. # the video has this width if rendered at 100%. attr_reader :width end # Describes a response to a Yahoo! Video search executed via Client#search. # Parameters are passed to the constructor as hash key/value pairs. class SearchResponse < Record # the position of the first result in the overall search. attr_reader :first_result_position # the list of zero or more SearchResult records comprising the results of # the executed search query. attr_reader :results # the number of query matches in the database. attr_reader :total_results_available # the number of query matches returned. this may be lower than the number # of results requested if there were fewer total results available. attr_reader :total_results_returned end class Client # the default hostname queried to retrieve yahoo video content. DEFAULT_API_URL = 'http://search.yahooapis.com/VideoSearchService/V1/videoSearch' # the default user agent submitted with http requests of yahoo video. DEFAULT_AGENT = 'yahoo-video for Ruby (http://www.rubyforge.org/projects/yahoo-video/)' # constructs a Client with the supplied parameters as: # # * +app_id+: the Yahoo! Video application id to submit with API requests. # * +api_url+: the root url via which to make requests of the Yahoo! Video # API. # * +agent+: the user agent to pass along with http requests made of the # Yahoo! Video API. def initialize (app_id, api_url = DEFAULT_API_URL, agent = DEFAULT_AGENT) @app_id = app_id @api_url = api_url @agent = agent end # submits a search request of the Yahoo! Video service with the supplied # SearchRequest parameters, returning a SearchResponse record detailing # the results. def search (request) # submit the api request url = _video_search_url(request) response = _request(url) # build an xml document with which to analyze the results doc = REXML::Document.new response root = doc.root # parse out all of the search result records results = [] root.elements.each('Result') do |re| # parse out the thumbnail record te = re.elements["Thumbnail"] thumbnail = Thumbnail.new(:url => te.elements["Url"].text, :width => _text_or_nil(te, "Height", 'to_i'), :height => _text_or_nil(te, "Height", 'to_i')) # tack on the new search result record results << SearchResult.new(:title => re.elements["Title"].text, :summary => re.elements["Summary"].text, :click_url => re.elements["ClickUrl"].text, :url => re.elements["Url"].text, :referer_url => re.elements["RefererUrl"].text, :file_size => re.elements["FileSize"].text.to_i, :file_format => re.elements["FileFormat"].text, :height => re.elements["Height"].text.to_i, :width => re.elements["Width"].text.to_i, :duration => re.elements["Duration"].text.to_i, :streaming => YahooVideo::_string_to_boolean(re.elements["Streaming"].text), :channels => re.elements["Channels"].text.to_i, :thumbnail => thumbnail) end # construct and return our final response record SearchResponse.new(:first_result_position => root.attributes['firstResultPosition'].to_i, :results => results, :total_results_available => root.attributes['totalResultsAvailable'].to_i, :total_results_returned => root.attributes['totalResultsReturned'].to_i) end private def _text_or_nil (element, key, format = 'to_s') value = element.elements[key] (value) ? value.text.send(format) : nil end # returns a url string with the given request's parameters encoded # appropriately for making a REST call of the Yahoo! Video API service. def _video_search_url (request) url = "#{@api_url}?appid=#{@app_id}" url << "&query=#{URI.encode(request.query)}" url << "&type=#{request.type}" if (request.type) url << "&results=#{request.results}" if (request.results) url << "&start=#{request.start}" if (request.start) url << "&#{_list_to_query_params('format', request.format)}" if (request.format) url << "&adult_ok=1" if (request.adult_ok) url << "&#{_list_to_query_params('site', request.site)}" if (request.site) url end # returns a string representing one or more key/value pairs built up # from the supplied parameters, suitable for inclusion as part of a # url's query parameters. def _list_to_query_params (key, values) return "#{key}=#{values}" if values.is_a?(String) str = '' values.each_with_index do |value, index| str << '&' if (index > 0) str << "#{key}=#{value}" end end # makes an http request of the given url and returns the html response # as a string. def _request (url) begin content = '' open(url, "User-Agent" => @agent) { |f| content = f.read } content rescue raise YahooVideoException.new("failed to request '#{url}': " + $!) end end end private # Returns the Ruby boolean object as TrueClass or FalseClass based on # the supplied string value. TrueClass is returned if the value is # non-nil and "true" (case-insensitive), else FalseClass is returned. def self._string_to_boolean (bool_str) (bool_str && bool_str.downcase == "true") end end