yt_util 0.0.1 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4b3dfe4b4c3b37181be955ee39eb039d8385844d
4
- data.tar.gz: 030dc138c1663339fc1b029689b7486d34fa07fa
3
+ metadata.gz: e7b1ed94c177fb7d05f71eae24e87db5467d01c1
4
+ data.tar.gz: 487ea160d323c3b8ef432d91523d2afbc1d44a0b
5
5
  SHA512:
6
- metadata.gz: a8fe781a3e22f5c1758e3171733ee85183204810563c6cf1602779add34265cc05d25b602101bd0efa60e9170d896e591f0547797f8942c823e0abd4e884a1b0
7
- data.tar.gz: 478691e74b25f47ff4a9bfcec18703665c5432b9d071dde56d3e96473b0ab6a06cd48a394c9fb49b7535b5592b4633f159c9fc7b155945a5c0408d29d61f8a81
6
+ metadata.gz: d529e7cdf11e6b2f4b5e844eddceec0886213894fa87aa70c642042a325a58ac20684c536cb696a5e2ca4111e17c194294a4ead6692f2d940dd16899b5aecf0c
7
+ data.tar.gz: cfa6c69365757262e89dd62ee777c9a7dcef6eede3093393eec9ada9a3b0dd34270d2a9fdadcdc5d656f48af51b22b7db7cc3ec3fa3ac8523eb8593b19808669
@@ -5,69 +5,98 @@ module YtUtil
5
5
  # OR range etc https://support.google.com/websearch/answer/136861?hl=en
6
6
  # punctuation: https://support.google.com/websearch/answer/2466433
7
7
 
8
- def self.query(search, filters = "") # Returns an Array of Hash results
9
- results = Mechanize.new.
10
- tap { |i| i.follow_meta_refresh = true }.
11
- get("https://www.youtube.com/results?search_query=#{Addressable::URI.parse(search).normalize + filters}")
12
- parse_query(results.parser)
8
+ def self.raw_query(search, filters = "")
9
+ raise "Invalid object type" unless search.is_a? String and filters.is_a? String
10
+ request_query(search,filters)
13
11
  end
14
12
 
15
- def self.video_statistics(video_code) # Video page hit.
16
- result = Nokogiri::HTML(open("https://www.youtube.com/watch?v=#{video_code}"))
17
- parse_video_page(result)
13
+ def self.query(search = nil, filters = "", &qry)
14
+ result = qry.try(:call) || raw_query(search,filters)
15
+ parse_query(result)
18
16
  end
19
17
 
20
- def self.user_statistics(username)
21
- results = Mechanize.new.
22
- tap { |i| i.follow_meta_refresh = true }.
23
- get("https://www.youtube.com/user/#{username}/about")
24
- parse_user(results.parser)
18
+ def self.raw_video_stats(video_code)
19
+ raise "Invalid object type" unless video_code.is_a? String
20
+ raise "Invalid video code" unless video_code.length == 11
21
+ request_video_stats(video_code)
22
+ end
23
+
24
+ def self.video_stats(video_code = nil, &qry)
25
+ result = qry.try(:call) || raw_video_stats(video_code)
26
+ parse_video_page(video_code, result)
27
+ end
28
+
29
+ def self.raw_user_stats(username)
30
+ raise "Invalid object type" unless username.is_a? String
31
+ request_user_stats(username)
32
+ end
33
+
34
+ def self.user_stats(username = nil, &qry)
35
+ result = qry.try(:call) || raw_user_stats(username)
36
+ parse_user(result)
25
37
  end
26
38
 
27
39
  private
40
+ def self.request(web_request)
41
+ try { Mechanize.new.tap { |i| i.follow_meta_refresh = true }.get(web_request).parser } ||
42
+ Nokogiri::HTML(open(web_request))
43
+ end
44
+
45
+ def self.request_query(search, filters = "")
46
+ web_request = "https://www.youtube.com/results?search_query=#{Addressable::URI.parse(search).normalize + filters}"
47
+ request(web_request)
48
+ end
49
+
50
+ def self.request_video_stats(video_code)
51
+ web_request = "https://www.youtube.com/watch?v=#{video_code}"
52
+ request(web_request)
53
+ end
54
+
55
+ def self.request_user_stats(username)
56
+ web_request = "https://www.youtube.com/user/#{username}/about"
57
+ request(web_request)
58
+ end
59
+
28
60
  def self.parse_query(query_result)
29
61
  query_result.css("ol.item-section > li")[1..-1].map do |result|
30
62
  {
31
- title: result.css("div:nth-child(1)").css("div:nth-child(2)").css("h3").text,
32
- video: result.css("div:nth-child(1)").css("div:nth-child(2)").css("h3 > a").first[:href].dup.tap{|i|i.replace i[(i.index("=").to_i+1)..-1]},
33
- views: Integer(String(/(\d+)/.match(result.css("div:nth-child(1)").css("div:nth-child(2)").css("li:nth-child(3)").text.gsub(",", "")))),
34
- new: !!result.css("div:nth-child(1)").css("div:nth-child(2)").css("div:nth-child(4)").css("ul:nth-child(1)").text["New"],
35
- hd: !!result.css("div:nth-child(1)").css("div:nth-child(2)").css("div:nth-child(4)").css("ul:nth-child(1)").text["HD"],
36
- description: result.css("div:nth-child(1)").css("div:nth-child(2)").css("div:nth-child(3)").text,
37
- length: result.css("div:nth-child(1)").css("div:nth-child(1)").css("a:nth-child(1)").css("span:nth-child(2)").text
63
+ title: try {result.css("div:nth-child(1)").css("div:nth-child(2)").css("h3").text},
64
+ video: try {result.css("div:nth-child(1)").css("div:nth-child(2)").css("h3 > a").first[:href].dup.tap{|i|i.replace i[(i.index("=").to_i+1)..-1]}},
65
+ views: try {result.css('li').select {|i| i.text =~ /^[\d,]{1,} views/ }.first.text.split.first.gsub(",","_").to_i},
66
+ new: try {!!result.css("div:nth-child(1)").css("div:nth-child(2)").css("div:nth-child(4)").css("ul:nth-child(1)").text["New"]},
67
+ hd: try {!!result.css("div:nth-child(1)").css("div:nth-child(2)").css("div:nth-child(4)").css("ul:nth-child(1)").text["HD"]},
68
+ description: try {result.css("div:nth-child(1)").css("div:nth-child(2)").css(".yt-lockup-description").text},
69
+ length: try {result.css("div:nth-child(1)").css("div:nth-child(1)").css("a:nth-child(1)").css("span:nth-child(2)").text}
38
70
  }
39
71
  end
40
72
  end
41
73
 
42
- def self.parse_video_page(query_result)
74
+ def self.parse_video_page(video_code, query_result)
43
75
  {
44
- video: video_code,
45
- user_name: query_result.css('a.yt-user-name').text,
46
- description: query_result.css('p#eow-description').text,
47
- category: query_result.css('li.watch-meta-item:nth-child(1)').css('ul:nth-child(2)').css('li:nth-child(1)').css('a:nth-child(1)').first[:href].try(:[], 1..-1),
48
- # Views are rounded to nearest thousand.
49
- views: String(/(\d+)/.match(query_result.css('div.watch-view-count').text.strip.gsub(',', ''))).to_i*1000,
50
- # Likes are rounded to nearest thousand.
51
- likes: String(/(\d+)/.match(query_result.css('button#watch-like').text.strip.gsub(',', ''))).to_i*1000,
52
- # Dislikes are rounded to nearest thousand.
53
- dislikes: String(/(\d+)/.match(query_result.css('button#watch-dislike').text.strip.gsub(',', ''))).to_i*1000,
54
- published: query_result.css('strong.watch-time-text').text[13..-1],
55
- license: query_result.css('li.watch-meta-item:nth-child(2)').text.gsub("\n", '').strip.tap {|i| i.replace i[i.index(' ').to_i..-1].strip}
76
+ video: video_code || try {query_result.css('meta').select {|i| i.attributes["property"].try(:value) =~ /og:url/}.first["content"].match(/\?v=(.+)/)[1]},
77
+ user_name: try {query_result.css('li').css('a').select{|i| i.text =~ / by [a-z0-9]{1,}/i}.map {|i| i.text.match(/by ([a-z0-9]{1,})/i)[1]}.first},
78
+ description: try {query_result.css('p#eow-description').text},
79
+ category: try {query_result.css('.watch-meta-item').css('a').text},
80
+ views: try {String(/(\d+)/.match(query_result.css('div.watch-view-count').text.strip.gsub(',', ''))).to_i},
81
+ likes: try {String(/(\d+)/.match(query_result.css('button#watch-like').text.strip.gsub(',', ''))).to_i},
82
+ dislikes: try {String(/(\d+)/.match(query_result.css('button#watch-dislike').text.strip.gsub(',', ''))).to_i},
83
+ published: try {query_result.css('strong.watch-time-text').text.match(/ on ([a-z0-9, ]{11,})/i)[1]},
84
+ license: try {query_result.css('li.watch-meta-item:nth-child(2)').text.gsub("\n", '').strip.tap {|i| i.replace i[i.index(' ').to_i..-1].strip}}
56
85
  }
57
86
  end
58
87
 
59
88
  def self.parse_user(query_result)
60
- views_n_subs = query_result.css('.about-stats').
89
+ views_n_subs = try {query_result.css('.about-stats').
61
90
  css('li').take(2).map{|i| i = i.text.strip; {
62
91
  i.match(/[a-z]+/)[0] => i.match(/[\d,]+/)[0]}
63
- }.inject(:update)
92
+ }.inject(:update)}
64
93
 
65
94
  {
66
- description: query_result.css('.about-description').css('p').text,
67
- link: query_result.css('a[title="Google+"]')[0]["href"],
68
- views: views_n_subs["views"],
69
- subscribers: views_n_subs["subscribers"],
70
- joined: query_result.css('.about-stats').css('.joined-date').text.strip
95
+ description: try {query_result.css('.about-description').css('p').text},
96
+ link: try {query_result.css('a[title="Google+"]')[0]["href"]},
97
+ views: try {views_n_subs["views"]},
98
+ subscribers: try {views_n_subs["subscribers"]},
99
+ joined: try {query_result.css('.about-stats').css('.joined-date').text.strip}
71
100
  }
72
101
  end
73
102
 
@@ -0,0 +1,102 @@
1
+ unless defined? try
2
+ class Object
3
+ # Invokes the public method whose name goes as first argument just like
4
+ # +public_send+ does, except that if the receiver does not respond to it the
5
+ # call returns +nil+ rather than raising an exception.
6
+ #
7
+ # This method is defined to be able to write
8
+ #
9
+ # @person.try(:name)
10
+ #
11
+ # instead of
12
+ #
13
+ # @person.name if @person
14
+ #
15
+ # +try+ calls can be chained:
16
+ #
17
+ # @person.try(:spouse).try(:name)
18
+ #
19
+ # instead of
20
+ #
21
+ # @person.spouse.name if @person && @person.spouse
22
+ #
23
+ # +try+ will also return +nil+ if the receiver does not respond to the method:
24
+ #
25
+ # @person.try(:non_existing_method) # => nil
26
+ #
27
+ # instead of
28
+ #
29
+ # @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil
30
+ #
31
+ # +try+ returns +nil+ when called on +nil+ regardless of whether it responds
32
+ # to the method:
33
+ #
34
+ # nil.try(:to_i) # => nil, rather than 0
35
+ #
36
+ # Arguments and blocks are forwarded to the method if invoked:
37
+ #
38
+ # @posts.try(:each_slice, 2) do |a, b|
39
+ # ...
40
+ # end
41
+ #
42
+ # The number of arguments in the signature must match. If the object responds
43
+ # to the method the call is attempted and +ArgumentError+ is still raised
44
+ # in case of argument mismatch.
45
+ #
46
+ # If +try+ is called without arguments it yields the receiver to a given
47
+ # block unless it is +nil+:
48
+ #
49
+ # @person.try do |p|
50
+ # ...
51
+ # end
52
+ #
53
+ # You can also call try with a block without accepting an argument, and the block
54
+ # will be instance_eval'ed instead:
55
+ #
56
+ # @person.try { upcase.truncate(50) }
57
+ #
58
+ # Please also note that +try+ is defined on +Object+. Therefore, it won't work
59
+ # with instances of classes that do not have +Object+ among their ancestors,
60
+ # like direct subclasses of +BasicObject+. For example, using +try+ with
61
+ # +SimpleDelegator+ will delegate +try+ to the target instead of calling it on
62
+ # the delegator itself.
63
+ def try(*a, &b)
64
+ try!(*a, &b) if a.empty? || respond_to?(a.first)
65
+ end
66
+
67
+ # Same as #try, but will raise a NoMethodError exception if the receiver is not +nil+ and
68
+ # does not implement the tried method.
69
+
70
+ def try!(*a, &b)
71
+ if a.empty? && block_given?
72
+ if b.arity.zero?
73
+ instance_eval(&b)
74
+ else
75
+ yield self
76
+ end
77
+ else
78
+ public_send(*a, &b)
79
+ end
80
+ end
81
+ end
82
+
83
+ class NilClass
84
+ # Calling +try+ on +nil+ always returns +nil+.
85
+ # It becomes especially helpful when navigating through associations that may return +nil+.
86
+ #
87
+ # nil.try(:name) # => nil
88
+ #
89
+ # Without +try+
90
+ # @person && @person.children.any? && @person.children.first.name
91
+ #
92
+ # With +try+
93
+ # @person.try(:children).try(:first).try(:name)
94
+ def try(*args)
95
+ nil
96
+ end
97
+
98
+ def try!(*args)
99
+ nil
100
+ end
101
+ end
102
+ end
@@ -1,3 +1,3 @@
1
1
  module YtUtil
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/yt_util.rb CHANGED
@@ -5,6 +5,7 @@ require "mechanize"
5
5
  require "yt_util/version"
6
6
  require "yt_util/scrape"
7
7
  require "yt_util/url"
8
+ require "yt_util/try"
8
9
 
9
10
  module YtUtil
10
11
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yt_util
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel P. Clark
@@ -96,6 +96,7 @@ files:
96
96
  - Rakefile
97
97
  - lib/yt_util.rb
98
98
  - lib/yt_util/scrape.rb
99
+ - lib/yt_util/try.rb
99
100
  - lib/yt_util/url.rb
100
101
  - lib/yt_util/version.rb
101
102
  - spec/spec_helper.rb