howcast 0.7.4 → 0.7.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +6 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +38 -0
  6. data/{README.markdown → README.md} +44 -13
  7. data/Rakefile +6 -39
  8. data/VERSION +1 -1
  9. data/fixtures/categories.xml +2230 -0
  10. data/fixtures/category.1585.xml +24 -0
  11. data/fixtures/homepage.staff_videos.xml +315 -0
  12. data/fixtures/invalid.api_key.xml +6 -0
  13. data/fixtures/playlist.4566.xml +484 -0
  14. data/fixtures/users.someone.profile.videos.xml +440 -0
  15. data/fixtures/video.233.generated.xml +1563 -0
  16. data/fixtures/video.233.xml +830 -0
  17. data/howcast.gemspec +31 -90
  18. data/lib/howcast.rb +1 -1
  19. data/lib/howcast/client.rb +1 -1
  20. data/lib/howcast/client/base.rb +232 -188
  21. data/lib/howcast/client/category.rb +31 -13
  22. data/lib/howcast/client/homepage.rb +10 -8
  23. data/lib/howcast/client/marker.rb +2 -0
  24. data/lib/howcast/client/playlist.rb +2 -0
  25. data/lib/howcast/client/search.rb +11 -11
  26. data/lib/howcast/client/type.rb +48 -0
  27. data/lib/howcast/client/user.rb +2 -0
  28. data/lib/howcast/client/utils.rb +53 -0
  29. data/lib/howcast/client/video.rb +39 -25
  30. data/lib/howcast/ext/string.rb +8 -0
  31. data/lib/howcast/hpricot/elements.rb +22 -0
  32. data/lib/howcast/version.rb +3 -0
  33. data/script/github-test.rb +24 -0
  34. data/spec/howcast/client/base_spec.rb +2 -2
  35. data/spec/howcast/client/category_spec.rb +41 -2
  36. data/spec/howcast/client/homepage_spec.rb +8 -8
  37. data/spec/howcast/client/playlist_spec.rb +6 -4
  38. data/spec/howcast/client/search_spec.rb +7 -8
  39. data/spec/howcast/client/user_spec.rb +9 -7
  40. data/spec/howcast/client/video_spec.rb +106 -22
  41. data/spec/spec_helper.rb +4 -7
  42. data/spec/xml_fixtures_helper.rb +20 -2895
  43. metadata +94 -28
  44. data/CHANGELOG +0 -108
  45. data/Manifest +0 -19
  46. data/howcast-0.7.3.gem +0 -0
  47. 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
- uri = "categories.xml"
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
- # Exception here to parse the <parents> tag in a category, will set a category.parents variable
93
- # which is an array of parent metadata hases
94
- # [{:id => '123', :name => "root"}, {:id => "1234", :name => "parent"}]
95
- def parse_single_category_xml(xml)
96
- hash = {}
97
- Category.attr_accessors.each do |attribute|
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
- def parse_playlists(xml)
69
- playlists = []
70
- node = xml.at('playlists')
71
- node.children_of_type('playlist').each do |child|
72
- playlists << parse_single_xml(child, Playlist)
73
- end unless node.nil?
74
- playlists
75
- end
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
- 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
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
- def search_url params
80
- uri = "search.xml?"
81
- params[:q] = CGI.escape params[:q] if params[:q]
82
- uri += hash_to_params params
83
- uri += uri_suffix(params.merge(:use_ampersand => true)) unless params[:page]
84
- uri
85
- end
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
@@ -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
@@ -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
- attr_accessor :id, :title, :permalink, :thumbnail_url, :category_id,
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
- def videos(options = {})
124
+ def videos(options = {})
112
125
  uri = videos_url(options)
113
- (establish_connection(uri)/:video).inject([]){ |r, i| r << parse_single_xml(i, Video)}
114
- end
115
-
116
- private
117
- def videos_url(options={})
118
- defaults = {:page => nil, :sort => "most_recent", :filter => "howcast_studios", :category_id => nil}
119
- options = defaults.update(options)
120
- "videos/#{options[:sort]}/#{options[:filter]}#{"/#{options[:category_id]}" if options[:category_id]}#{"/#{options[:page]}" if options[:page]}.xml"
121
- end
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,8 @@
1
+ unless String.instance_methods.include? 'singularize'
2
+ class String
3
+ def singularize
4
+ self.gsub(/e?s\Z/, '')
5
+ end
6
+ end
7
+ end
8
+
@@ -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,3 @@
1
+ module Howcast
2
+ VERSION = File.read(File.expand_path(File.join(File.dirname(__FILE__), '../../VERSION'))).chomp
3
+ 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 www.howcast.com" do
22
- Howcast::Client.base_uri.to_s.should == "http://www.howcast.com"
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(equivalent_uri("http://www.howcast.com/categories/2.xml?api_key=myapikey")).and_return(category_xml)
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(equivalent_uri("http://www.howcast.com/categories.xml?api_key=myapikey")).and_return(categories_xml)
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