howcast 0.7.4 → 0.7.15

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.
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