howcast 0.7.4 → 0.7.15
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +38 -0
- data/{README.markdown → README.md} +44 -13
- data/Rakefile +6 -39
- data/VERSION +1 -1
- data/fixtures/categories.xml +2230 -0
- data/fixtures/category.1585.xml +24 -0
- data/fixtures/homepage.staff_videos.xml +315 -0
- data/fixtures/invalid.api_key.xml +6 -0
- data/fixtures/playlist.4566.xml +484 -0
- data/fixtures/users.someone.profile.videos.xml +440 -0
- data/fixtures/video.233.generated.xml +1563 -0
- data/fixtures/video.233.xml +830 -0
- data/howcast.gemspec +31 -90
- data/lib/howcast.rb +1 -1
- data/lib/howcast/client.rb +1 -1
- data/lib/howcast/client/base.rb +232 -188
- data/lib/howcast/client/category.rb +31 -13
- data/lib/howcast/client/homepage.rb +10 -8
- data/lib/howcast/client/marker.rb +2 -0
- data/lib/howcast/client/playlist.rb +2 -0
- data/lib/howcast/client/search.rb +11 -11
- data/lib/howcast/client/type.rb +48 -0
- data/lib/howcast/client/user.rb +2 -0
- data/lib/howcast/client/utils.rb +53 -0
- data/lib/howcast/client/video.rb +39 -25
- data/lib/howcast/ext/string.rb +8 -0
- data/lib/howcast/hpricot/elements.rb +22 -0
- data/lib/howcast/version.rb +3 -0
- data/script/github-test.rb +24 -0
- data/spec/howcast/client/base_spec.rb +2 -2
- data/spec/howcast/client/category_spec.rb +41 -2
- data/spec/howcast/client/homepage_spec.rb +8 -8
- data/spec/howcast/client/playlist_spec.rb +6 -4
- data/spec/howcast/client/search_spec.rb +7 -8
- data/spec/howcast/client/user_spec.rb +9 -7
- data/spec/howcast/client/video_spec.rb +106 -22
- data/spec/spec_helper.rb +4 -7
- data/spec/xml_fixtures_helper.rb +20 -2895
- metadata +94 -28
- data/CHANGELOG +0 -108
- data/Manifest +0 -19
- data/howcast-0.7.3.gem +0 -0
- data/spec/string_matchers_helper.rb +0 -22
@@ -24,6 +24,8 @@
|
|
24
24
|
class Howcast::Client
|
25
25
|
class Category
|
26
26
|
extend WatchAttrAccessors
|
27
|
+
include XmlMethods
|
28
|
+
|
27
29
|
attr_accessor :id, :name, :parent_id, :parents, :permalink
|
28
30
|
|
29
31
|
# Creates a new Category object which is used to encapsulate all the attributes available
|
@@ -84,21 +86,37 @@ class Howcast::Client
|
|
84
86
|
# Get the top level Howcast categories
|
85
87
|
# Howcast::Client.new.categories
|
86
88
|
def categories(options = {})
|
87
|
-
|
88
|
-
establish_connection(uri).at('categories').children_of_type('category').inject([]){ |r, i| r << parse_single_category_xml(i)}
|
89
|
+
fetch_categories.at('categories').children_of_type('category').inject([]){ |r, i| r << parse_single_category_xml(i) }
|
89
90
|
end
|
90
91
|
|
92
|
+
def category_id_for key
|
93
|
+
fetch_categories do |categories|
|
94
|
+
node = (categories/"//category/name[text-downcase()='#{key.downcase}']/../permalink")
|
95
|
+
node = (categories/"//category/permalink[text-downcase()*='#{key.downcase}']") if node.empty?
|
96
|
+
node.text.split("/").last unless node.empty?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
91
100
|
private
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
node_name = attribute.to_s.gsub("_", "-") # xml schema uses hyphens for spaces, but ruby uses underscores
|
99
|
-
hash[attribute] = !xml.at(node_name).nil? ? xml.at(node_name).inner_text.strip : ""
|
100
|
-
end
|
101
|
-
hash[:parents] = (xml.at('parents')/:category).map{ |c| {:id => c.at('id').inner_text.strip, :name => c.at('name').inner_text.strip, :permalink => c.at('permalink').inner_text.strip }} unless xml.at('parents').nil?
|
102
|
-
hash.values.all?{|v| v==""} ? nil : Category.new(hash)
|
101
|
+
def fetch_categories
|
102
|
+
@categories ||= establish_connection("categories.xml")
|
103
|
+
if block_given?
|
104
|
+
yield @categories
|
105
|
+
else
|
106
|
+
@categories
|
103
107
|
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Exception here to parse the <parents> tag in a category, will set a category.parents variable
|
111
|
+
# which is an array of parent metadata hases
|
112
|
+
# [{:id => '123', :name => "root"}, {:id => "1234", :name => "parent"}]
|
113
|
+
def parse_single_category_xml(xml)
|
114
|
+
hash = {}
|
115
|
+
Category.attr_accessors.each do |attribute|
|
116
|
+
node_name = attribute.to_s.gsub("_", "-") # xml schema uses hyphens for spaces, but ruby uses underscores
|
117
|
+
hash[attribute] = !xml.at(node_name).nil? ? xml.at(node_name).inner_text.strip : ""
|
118
|
+
end
|
119
|
+
hash[:parents] = (xml.at('parents')/:category).map{ |c| {:id => c.at('id').inner_text.strip, :name => c.at('name').inner_text.strip, :permalink => c.at('permalink').inner_text.strip }} unless xml.at('parents').nil?
|
120
|
+
hash.values.all?{|v| v==""} ? nil : Category.new(hash)
|
121
|
+
end
|
104
122
|
end
|
@@ -24,6 +24,8 @@
|
|
24
24
|
class Howcast::Client
|
25
25
|
class Homepage
|
26
26
|
extend WatchAttrAccessors
|
27
|
+
include XmlMethods
|
28
|
+
|
27
29
|
attr_accessor :videos, :playlists
|
28
30
|
|
29
31
|
# Creates a new Homepage object which is used to encapsulate all the attributes available
|
@@ -65,12 +67,12 @@ class Howcast::Client
|
|
65
67
|
end
|
66
68
|
|
67
69
|
private
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
70
|
+
def parse_playlists(xml)
|
71
|
+
playlists = []
|
72
|
+
node = xml.at('playlists')
|
73
|
+
node.children_of_type('playlist').each do |child|
|
74
|
+
playlists << parse_single_xml(child, Playlist)
|
75
|
+
end unless node.nil?
|
76
|
+
playlists
|
77
|
+
end
|
76
78
|
end
|
@@ -24,6 +24,8 @@
|
|
24
24
|
class Howcast::Client
|
25
25
|
class Marker
|
26
26
|
extend WatchAttrAccessors
|
27
|
+
include XmlMethods
|
28
|
+
|
27
29
|
attr_accessor :id, :position, :timemarker, :type, :thumbnail_url, :title, :textile_text, :text
|
28
30
|
|
29
31
|
# Creates a new Marker object which is used to encapsulate all the attributes available
|
@@ -24,6 +24,8 @@
|
|
24
24
|
class Howcast::Client
|
25
25
|
class Playlist
|
26
26
|
extend WatchAttrAccessors
|
27
|
+
include XmlMethods
|
28
|
+
|
27
29
|
attr_accessor :id, :title, :description, :playlist_thumbnail_url, :videos
|
28
30
|
|
29
31
|
# Creates a new Playlist object which is used to encapsulate all the attributes available
|
@@ -71,16 +71,16 @@ class Howcast::Client
|
|
71
71
|
end
|
72
72
|
|
73
73
|
private
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
74
|
+
def do_search params
|
75
|
+
uri = search_url params
|
76
|
+
(establish_connection(uri)/:video).inject([]){ |r, i| r << parse_single_xml(i, Video)}
|
77
|
+
end
|
78
78
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
79
|
+
def search_url params
|
80
|
+
uri = "search.xml?"
|
81
|
+
params = params.inject({}){ |h, (k, v)| h[k] = CGI::escape(v.to_s); h }
|
82
|
+
uri += hash_to_params params
|
83
|
+
uri += uri_suffix(params.merge(:use_ampersand => true)) unless params[:page]
|
84
|
+
uri
|
85
|
+
end
|
86
86
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2010 Howcast Media Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
class Howcast::Client
|
25
|
+
class Type
|
26
|
+
extend WatchAttrAccessors
|
27
|
+
include XmlMethods
|
28
|
+
|
29
|
+
attr_accessor :kind, :status, :name
|
30
|
+
|
31
|
+
# Creates a new Type object which is used to encapsulate all the attributes available
|
32
|
+
# from the Howcast Video API.
|
33
|
+
#
|
34
|
+
# === Inputs
|
35
|
+
#
|
36
|
+
# * <tt>attributes</tt> -- A hash to set the various attributes of the marker object
|
37
|
+
#
|
38
|
+
# === Examples
|
39
|
+
#
|
40
|
+
# Initialize a type
|
41
|
+
# Type.new :name => "HowcastGuide", :kind => "HowcastOriginalGuide", :status => "proprietary"
|
42
|
+
def initialize(attributes={})
|
43
|
+
attributes.each do |k, v|
|
44
|
+
self.send("#{k}=", v) if self.respond_to?(k)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/howcast/client/user.rb
CHANGED
@@ -24,6 +24,8 @@
|
|
24
24
|
class Howcast::Client
|
25
25
|
class User
|
26
26
|
extend WatchAttrAccessors
|
27
|
+
include XmlMethods
|
28
|
+
|
27
29
|
attr_accessor :login, :firstname, :lastname, :thumbnail_url, :views, :count, :videos
|
28
30
|
|
29
31
|
# Creates a new User object which is used to encapsulate all the attributes available
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# utility modules for models (eg: video, marker, etc)
|
2
|
+
|
3
|
+
module WatchAttrAccessors
|
4
|
+
def attr_accessor(*args)
|
5
|
+
super(*args)
|
6
|
+
@attr_accessors ||= []
|
7
|
+
@attr_accessors += args
|
8
|
+
end
|
9
|
+
|
10
|
+
def attr_accessors
|
11
|
+
@attr_accessors || []
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module XmlMethods
|
16
|
+
def to_doc
|
17
|
+
doc = Nokogiri::XML::Document.new
|
18
|
+
node = Nokogiri::XML::Node.new(self.class.to_s.sub(/([a-z]+::)+/i, '').downcase, doc)
|
19
|
+
root = doc.add_child node
|
20
|
+
self.class.attr_accessors.each do |attr|
|
21
|
+
node = Nokogiri::XML::Node.new("#{attr}", doc)
|
22
|
+
value = self.send(attr)
|
23
|
+
if value.instance_of? String
|
24
|
+
value = inflect(attr, value) if respond_to? :inflect
|
25
|
+
if value.instance_of? String
|
26
|
+
node.content = value
|
27
|
+
else
|
28
|
+
node.add_child value
|
29
|
+
end
|
30
|
+
root.add_child node
|
31
|
+
elsif value.respond_to? :to_doc
|
32
|
+
node = value.to_doc.root
|
33
|
+
root.add_child node
|
34
|
+
elsif value.instance_of? Array
|
35
|
+
value.each do |v|
|
36
|
+
if v.respond_to? :to_doc
|
37
|
+
node.add_child v.to_doc.root
|
38
|
+
else
|
39
|
+
child = Nokogiri::XML::Node.new("#{attr}".singularize, doc)
|
40
|
+
child.content = v.to_s
|
41
|
+
node.add_child child
|
42
|
+
end
|
43
|
+
end
|
44
|
+
root.add_child node
|
45
|
+
end
|
46
|
+
end
|
47
|
+
doc
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_xml
|
51
|
+
to_doc.to_xml
|
52
|
+
end
|
53
|
+
end
|
data/lib/howcast/client/video.rb
CHANGED
@@ -22,24 +22,14 @@
|
|
22
22
|
#++
|
23
23
|
|
24
24
|
class Howcast::Client
|
25
|
-
module WatchAttrAccessors
|
26
|
-
def attr_accessor(*args)
|
27
|
-
super(*args)
|
28
|
-
@attr_accessors ||= []
|
29
|
-
@attr_accessors += args
|
30
|
-
end
|
31
|
-
|
32
|
-
def attr_accessors
|
33
|
-
@attr_accessors || []
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
25
|
class Video
|
38
26
|
extend WatchAttrAccessors
|
39
|
-
|
27
|
+
include XmlMethods
|
28
|
+
|
29
|
+
attr_accessor :id, :title, :permalink, :thumbnail_url, :category_id,
|
40
30
|
:views, :username, :duration, :created_at, :rating, :description, :width, :height,
|
41
31
|
:badges, :easy_steps, :embed, :category_hierarchy, :ingredients, :markers, :related_videos,
|
42
|
-
:filename
|
32
|
+
:filename, :mature_content, :ads_allowed, :playlist_memberships, :type
|
43
33
|
|
44
34
|
# Creates a new Video object which is used to encapsulate all the attributes available
|
45
35
|
# from the Howcast Video API
|
@@ -57,11 +47,34 @@ class Howcast::Client
|
|
57
47
|
self.send("#{k}=", v) if self.respond_to?(k)
|
58
48
|
end
|
59
49
|
end
|
60
|
-
|
50
|
+
|
61
51
|
# Return true if the video contains easy step by step directions, else false
|
62
52
|
def easy_steps?
|
63
53
|
easy_steps == "true"
|
64
54
|
end
|
55
|
+
|
56
|
+
def mature_content?
|
57
|
+
mature_content == "true"
|
58
|
+
end
|
59
|
+
|
60
|
+
def ads_allowed?
|
61
|
+
ads_allowed == "true"
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_param
|
65
|
+
id
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
def inflect attr, value
|
70
|
+
if %w{ ads_allowed mature_content easy_steps }.include? "#{attr}"
|
71
|
+
(value.empty? ? false : value).to_s
|
72
|
+
elsif "#{attr}" == "embed"
|
73
|
+
Nokogiri::XML(value).root
|
74
|
+
else
|
75
|
+
value.to_s
|
76
|
+
end
|
77
|
+
end
|
65
78
|
end
|
66
79
|
|
67
80
|
# Provides access to the Howcast video API.
|
@@ -108,15 +121,16 @@ class Howcast::Client
|
|
108
121
|
# Howcast::Client.new.videos
|
109
122
|
# Get the third page of top favorites which are featured
|
110
123
|
# Howcast::Client.new.videos(:page => 3, :sort => "top_favorites", :filter => "top_rated")
|
111
|
-
|
124
|
+
def videos(options = {})
|
112
125
|
uri = videos_url(options)
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
126
|
+
doc = establish_connection(uri)
|
127
|
+
(doc/:video).inject([]){ |r, i| r << parse_single_xml(i, Video) }
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
def videos_url(options={})
|
132
|
+
defaults = {:page => nil, :sort => "most_recent", :filter => "howcast_studios", :category_id => nil}
|
133
|
+
options = defaults.update(options)
|
134
|
+
"videos/#{options[:sort]}/#{options[:filter]}#{"/#{options[:category_id]}" if options[:category_id]}#{"/#{options[:page]}" if options[:page]}.xml"
|
135
|
+
end
|
122
136
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Hpricot
|
2
|
+
module Traverse
|
3
|
+
oper_procs =
|
4
|
+
{'=' => proc{ |a,b| a == b },
|
5
|
+
'!=' => proc{ |a,b| a != b },
|
6
|
+
'~=' => proc{ |a,b| a.split(/\s+/).include?(b) },
|
7
|
+
'|=' => proc{ |a,b| a =~ /^#{Regexp::quote b}(-|$)/ },
|
8
|
+
'^=' => proc{ |a,b| a.index(b) == 0 },
|
9
|
+
'$=' => proc{ |a,b| a =~ /#{Regexp::quote b}$/ },
|
10
|
+
'*=' => proc{ |a,b| idx = a.index(b) }}
|
11
|
+
|
12
|
+
pred_n = 'text-downcase()'
|
13
|
+
pred_f = proc{ |ele, *_| ele.inner_text.strip.downcase }
|
14
|
+
|
15
|
+
oper_procs.each do |oper_n, oper_f|
|
16
|
+
filter "#{pred_n}#{oper_n}" do |*a|
|
17
|
+
qual = pred_f[self, *a]
|
18
|
+
oper_f[qual, a[-2]] if qual
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
if ARGV.size < 1
|
5
|
+
puts "Usage: github-test.rb my-project.gemspec"
|
6
|
+
exit
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'rubygems/specification'
|
11
|
+
|
12
|
+
data = File.read(ARGV[0])
|
13
|
+
spec = nil
|
14
|
+
|
15
|
+
if data !~ %r{!ruby/object:Gem::Specification}
|
16
|
+
Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
|
17
|
+
else
|
18
|
+
spec = YAML.load(data)
|
19
|
+
end
|
20
|
+
|
21
|
+
spec.validate
|
22
|
+
|
23
|
+
puts spec
|
24
|
+
puts "OK"
|
@@ -18,8 +18,8 @@ describe Howcast::Client, "base_uri" do
|
|
18
18
|
Howcast::Client.instance_variable_set :@base_uri, nil
|
19
19
|
end
|
20
20
|
|
21
|
-
it "should use a default base_uri of
|
22
|
-
Howcast::Client.base_uri.to_s.should == "http://
|
21
|
+
it "should use a default base_uri of api.howcast.com" do
|
22
|
+
Howcast::Client.base_uri.to_s.should == "http://api.howcast.com"
|
23
23
|
end
|
24
24
|
|
25
25
|
it "should allow easily setting the base_url" do
|
@@ -20,7 +20,7 @@ describe Howcast::Client, "category" do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
it "should establish a connection with the correct category url" do
|
23
|
-
@hc.should_receive(:open).with(
|
23
|
+
@hc.should_receive(:open).with(URI.parse "http://api.howcast.com/categories/2.xml?api_key=myapikey").and_return(category_xml)
|
24
24
|
@hc.category(2)
|
25
25
|
end
|
26
26
|
|
@@ -54,7 +54,7 @@ describe Howcast::Client, "categories" do
|
|
54
54
|
end
|
55
55
|
|
56
56
|
it "should establish a connection with categories.xml" do
|
57
|
-
@hc.should_receive(:open).with(
|
57
|
+
@hc.should_receive(:open).with(URI.parse "http://api.howcast.com/categories.xml?api_key=myapikey").and_return(categories_xml)
|
58
58
|
@hc.categories
|
59
59
|
end
|
60
60
|
|
@@ -66,3 +66,42 @@ describe Howcast::Client, "categories" do
|
|
66
66
|
categories.last.name.should == 'Travel'
|
67
67
|
end
|
68
68
|
end
|
69
|
+
|
70
|
+
describe Howcast::Client, "category_id_for" do
|
71
|
+
before :each do
|
72
|
+
@hc = Howcast::Client.new(:key => "myapikey")
|
73
|
+
@hc.stub!(:open).and_return(categories_xml)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should be able to fetch the category id of a top-level category" do
|
77
|
+
@hc.category_id_for("Arts & Media").should == "1-Arts-and-Media"
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should be able to fetch the category id of a top-level category in a case-insensitive manner" do
|
81
|
+
@hc.category_id_for("arts & media").should == "1-Arts-and-Media"
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should be able to fetch the category id of a subcategory" do
|
85
|
+
@hc.category_id_for("Animation Techniques").should == "6-Animation-Techniques"
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should be able to fetch the category id of a subcategory in a case-insensitive manner" do
|
89
|
+
@hc.category_id_for("animation techniques").should == "6-Animation-Techniques"
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should be able to fetch the category id given a permalink id" do
|
93
|
+
@hc.category_id_for("375-Asian-Cooking").should == "375-Asian-Cooking"
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should be able to fetch the category id given a permalink id in a case-insensitive manner" do
|
97
|
+
@hc.category_id_for("375-asian-cooking").should == "375-Asian-Cooking"
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should return nothing if the category name does not exist" do
|
101
|
+
@hc.category_id_for("This Category Does Not Exist").should be nil
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should return nothing if the permalink id does not exist" do
|
105
|
+
@hc.category_id_for("99999-This-Category-Does-Not-Exist").should be nil
|
106
|
+
end
|
107
|
+
end
|