youtube-g 0.4.0
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.
- data/History.txt +12 -0
- data/Manifest.txt +24 -0
- data/README.txt +66 -0
- data/Rakefile +19 -0
- data/TODO.txt +11 -0
- data/lib/youtube_g.rb +18 -0
- data/lib/youtube_g/client.rb +35 -0
- data/lib/youtube_g/logger.rb +27 -0
- data/lib/youtube_g/model/author.rb +8 -0
- data/lib/youtube_g/model/category.rb +8 -0
- data/lib/youtube_g/model/contact.rb +8 -0
- data/lib/youtube_g/model/content.rb +13 -0
- data/lib/youtube_g/model/playlist.rb +7 -0
- data/lib/youtube_g/model/rating.rb +10 -0
- data/lib/youtube_g/model/thumbnail.rb +10 -0
- data/lib/youtube_g/model/user.rb +20 -0
- data/lib/youtube_g/model/video.rb +100 -0
- data/lib/youtube_g/parser.rb +163 -0
- data/lib/youtube_g/record.rb +12 -0
- data/lib/youtube_g/request/video_search.rb +162 -0
- data/lib/youtube_g/response/video_search.rb +23 -0
- data/test/test_client.rb +216 -0
- data/test/test_video.rb +18 -0
- data/test/test_video_search.rb +114 -0
- metadata +84 -0
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,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,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
|
data/test/test_client.rb
ADDED
@@ -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
|
data/test/test_video.rb
ADDED
@@ -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:
|