watchmob 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/watchmob ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
3
+ require 'json'
4
+ require 'watchmob'
5
+
6
+ if ARGV.size >= 1
7
+ json = JSON.parse(File.read(ARGV[0]))
8
+ else
9
+ STDERR.puts "Use: #$0 input.json <output.html>"
10
+ exit 1
11
+ end
12
+
13
+ begin
14
+ Watchmob::Log.output = STDERR
15
+ watchlist = Watchmob::Watchlist.new(json)
16
+ watchlist.sort_by! :battery_capacity
17
+
18
+ if ARGV.size >= 2
19
+ output = File.open(ARGV[1], "w")
20
+ else
21
+ output = STDOUT
22
+ end
23
+
24
+ output.write(watchlist.to_html)
25
+ rescue Watchmob::WatchmobError => e
26
+ Watchmob::Log.error(e)
27
+ exit 2
28
+ ensure
29
+ output.close if output
30
+ end
data/lib/watchmob.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'watchmob/log'
2
+ require 'watchmob/error'
3
+ require 'watchmob/watchlist'
4
+ require 'watchmob/page'
5
+ require 'watchmob/smartphone'
6
+ require 'watchmob/aukro_page'
7
+ require 'watchmob/heureka_page'
8
+ require 'watchmob/heureka_search_page'
9
+ require 'watchmob/gsmarena_page'
10
+ require 'watchmob/gsmarena_search_page'
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+ module Watchmob
3
+ class AukroPage < Page
4
+ def finished?
5
+ not document.at_css("#itemFinishBox2").nil?
6
+ end
7
+
8
+ def price
9
+ if finished?
10
+ e = document.at_css("#itemFinishBox2 .left:first-child strong")
11
+ else
12
+ e = document.at_css(".price")
13
+ end
14
+ e.inner_text.gsub(/\s+/, "").gsub(",", ".").to_f
15
+ end
16
+
17
+ def deadline
18
+ document.at_css(".timeInfo").inner_text.match(
19
+ /\(([[:alpha:]]+),\s+(\d+)\s+([[:alpha:]]+),\s+(\d+):(\d+):(\d+)/
20
+ ) do |md|
21
+ text, weekday_name, month_day, month_name, hours, minutes, seconds = *md.to_a
22
+ DateTime.new(
23
+ Time.now.year,
24
+ month_name_to_number(month_name),
25
+ month_day.to_i,
26
+ hours.to_i,
27
+ minutes.to_i,
28
+ seconds.to_i,
29
+ '+1'
30
+ )
31
+ end
32
+ end
33
+
34
+ def month_name_to_number(month_name)
35
+ case month_name
36
+ when "ledna" then 1
37
+ when "února" then 2
38
+ when "března" then 3
39
+ when "dubna" then 4
40
+ when "května" then 5
41
+ when "června" then 6
42
+ when "července" then 7
43
+ when "srpna" then 8
44
+ when "září" then 9
45
+ when "října" then 10
46
+ when "listopadu" then 11
47
+ when "prosince" then 12
48
+ else
49
+ require 'pry'
50
+ pry binding
51
+ raise AukroDateError, "Unknown month name #{month_name.inspect}"
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+
@@ -0,0 +1,6 @@
1
+ module Watchmob
2
+ WatchmobError = Class.new(RuntimeError)
3
+ NotFoundError = Class.new(WatchmobError)
4
+ AukroDateError = Class.new(WatchmobError)
5
+ PropertyError = Class.new(WatchmobError)
6
+ end
@@ -0,0 +1,86 @@
1
+ require 'andand'
2
+ require 'watchmob/page'
3
+
4
+ module Watchmob
5
+ class GSMArenaPage < Page
6
+ def self.find(phrase)
7
+ search = GSMArenaSearchPage.search(phrase)
8
+ uri = search.first_result_uri
9
+ new(uri)
10
+ end
11
+
12
+ def title
13
+ "GSMArena"
14
+ end
15
+
16
+ def name
17
+ document.at_css("h1").andand.inner_text
18
+ end
19
+
20
+ def mass
21
+ property "Weight" do |text|
22
+ text.match /(\d+)\s*g/ do |md|
23
+ return md[1].to_f
24
+ end
25
+ end
26
+ end
27
+
28
+ def battery_capacity
29
+ property "Battery" do |text|
30
+ text.match /(\d+)\s*mAh/i do |md|
31
+ return md[1].to_f
32
+ end
33
+ end
34
+ end
35
+
36
+ def stand_by
37
+ property "Stand-by" do |text|
38
+ average_time text
39
+ end
40
+ end
41
+
42
+ def talk_time
43
+ property "Talk time" do |text|
44
+ average_time text
45
+ end
46
+ end
47
+
48
+ def average_time(text)
49
+ minutes = text.scan(/(\d+)\s*h\s*(\d+\s*min)?/).map do |hours, mins=0|
50
+ hours.to_f * 60 + mins.to_f
51
+ end
52
+
53
+ unless minutes.empty?
54
+ sum = minutes.inject :+
55
+ return sum.to_f / minutes.size
56
+ end
57
+
58
+ nil
59
+ end
60
+
61
+ def autofocus
62
+ property "Primary" do |text|
63
+ !!(text =~ /autofocus/)
64
+ end
65
+ end
66
+
67
+ def property(name)
68
+ found = nil
69
+ document.css("#specs-list tr").each do |tr|
70
+ cells = [tr.at_css("th"), tr.at_css(".ttl")]
71
+ if cells.any? { |c| c.andand.inner_text.to_s.strip == name }
72
+ value = tr.at_css(".nfo").inner_text
73
+
74
+ unless value.nil?
75
+ raise PropertyError, "Property named #{name} has been matched " +
76
+ "more than once on GSM arena page #{uri}" if found
77
+ found = yield value
78
+ end
79
+ end
80
+ end
81
+
82
+ found
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,33 @@
1
+ require 'watchmob/page'
2
+
3
+ module Watchmob
4
+ class GSMArenaSearchPage < Page
5
+ def self.search(phrase)
6
+ encoded_phrase = URI.encode_www_form_component(phrase)
7
+ new("http://www.gsmarena.com/results.php3?sQuickSearch=no&sName=#{encoded_phrase}", phrase)
8
+ end
9
+
10
+ def initialize(uri, phrase = nil)
11
+ super(uri)
12
+ @phrase = phrase
13
+ end
14
+
15
+ attr_reader :phrase
16
+
17
+ def first_result_uri
18
+ if document.at_css("#specs-list")
19
+ uri
20
+ else
21
+ path = document.css("#mid-col .makers li a").
22
+ map { |a| a.attr("href") }.
23
+ first
24
+
25
+ if path
26
+ URI.parse("http://www.gsmarena.com/#{path}")
27
+ else
28
+ raise NotFoundError, "There were no phones for #{phrase.inspect} on GSM arena"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,69 @@
1
+ require 'andand'
2
+ require 'watchmob/page'
3
+
4
+ module Watchmob
5
+ class HeurekaPage < Page
6
+ def self.find(name)
7
+ search = HeurekaSearchPage.search(name)
8
+ uri = search.first_result_uri
9
+ uri.path += "/specifikace/"
10
+ new(uri)
11
+ end
12
+
13
+ def title
14
+ "Heureka"
15
+ end
16
+
17
+ def name
18
+ document.at_css(".main-info h2").andand.inner_text
19
+ end
20
+
21
+ def mass
22
+ param_value(1607).andand.to_f
23
+ end
24
+
25
+ def battery_capacity
26
+ capacity = param_value(1615).andand.to_f
27
+ if !capacity.nil? and capacity <= 10
28
+ # this is a common mistake in Heureka specs (1.3 mAh instead of 1300 mAh)
29
+ capacity * 1000
30
+ else
31
+ capacity
32
+ end
33
+ end
34
+
35
+ def stand_by
36
+ param_value_to_min(1616).andand.to_f
37
+ end
38
+
39
+ def talk_time
40
+ param_value_to_min(1617).andand.to_f
41
+ end
42
+
43
+ def autofocus
44
+ param_value(3688).andand.eql? "ano"
45
+ end
46
+
47
+ def param_value_to_min(param)
48
+ value = param_value(param).andand.to_f
49
+ case unit = param_unit(param)
50
+ when /hodin/
51
+ value * 60
52
+ when /min/
53
+ value
54
+ when nil
55
+ nil
56
+ else
57
+ raise RuntimeError, "Unknown unit #{unit}"
58
+ end
59
+ end
60
+
61
+ def param_value(param)
62
+ document.at_css("#param-value-#{param}").andand.inner_text
63
+ end
64
+
65
+ def param_unit(param)
66
+ document.at_css("#param-unit-#{param}").andand.inner_text
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,31 @@
1
+ require 'uri'
2
+ require 'watchmob/page'
3
+
4
+ module Watchmob
5
+ class HeurekaSearchPage < Page
6
+ def self.search(phrase)
7
+ encoded_phrase = URI.encode_www_form_component(phrase)
8
+ new("http://mobilni-telefony.heureka.cz/f:q:#{encoded_phrase}", phrase)
9
+ end
10
+
11
+ def initialize(uri, phrase = nil)
12
+ super(uri)
13
+ @phrase = phrase
14
+ end
15
+
16
+ attr_reader :phrase
17
+
18
+ def first_result_uri
19
+ if document.at_css("#product-container")
20
+ found_uri = document.css("#product-container .p").
21
+ map { |search_result| URI.parse(search_result.at_css("h2 a").attr("href")) }.
22
+ first
23
+
24
+ return found_uri unless found_uri.nil?
25
+ end
26
+
27
+ raise NotFoundError, "There were no smartphones for #{phrase.inspect} on Heureka"
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,36 @@
1
+ module Watchmob
2
+ module Log
3
+ extend self
4
+
5
+ def error(msg)
6
+ show msg
7
+ end
8
+
9
+ def info(msg)
10
+ show msg
11
+ end
12
+
13
+ def waiting(msg)
14
+ show "#{msg}..."
15
+ begin
16
+ yield
17
+ rescue Exception => e
18
+ show " #{msg} -> #{e.class.to_s}"
19
+ raise
20
+ else
21
+ show " #{msg} -> OK"
22
+ end
23
+ end
24
+
25
+ attr_accessor :output
26
+
27
+ @output = STDERR
28
+ @print_mutex = Mutex.new
29
+
30
+ def show(msg)
31
+ @print_mutex.synchronize do
32
+ output.puts(msg)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ require 'nokogiri'
2
+ require 'uri'
3
+ require 'open-uri'
4
+
5
+ module Watchmob
6
+ class Page
7
+ def initialize(uri)
8
+ @uri = uri
9
+ Log.waiting("get #@uri") do
10
+ @document = Nokogiri::HTML.parse(open(uri))
11
+ end
12
+ end
13
+
14
+ attr_reader :uri
15
+ attr_reader :document
16
+ end
17
+ end
@@ -0,0 +1,112 @@
1
+ require 'uri'
2
+ require 'watchmob/gsmarena_page'
3
+ require 'watchmob/heureka_page'
4
+
5
+ module Watchmob
6
+ class Smartphone
7
+
8
+ SPEC_PAGES = [HeurekaPage, GSMArenaPage]
9
+
10
+ def self.find(params)
11
+ unless params["aukro"] and params["phone"]
12
+ raise ArgumentError, "I want parameters 'aukro' and 'phone', got #{params.inspect}"
13
+ end
14
+
15
+ aukro_page = AukroPage.new(URI.parse(params["aukro"]))
16
+ spec_pages = SPEC_PAGES.map { |spec_page|
17
+ begin
18
+ spec_page.find(params["phone"])
19
+ rescue NotFoundError
20
+ nil
21
+ end
22
+ }.select { |s| !s.nil? }
23
+
24
+ unless spec_pages.empty?
25
+ new(aukro_page, spec_pages)
26
+ else
27
+ raise NotFoundError, "Smartphone #{params["phone"]} <#{params["aukro"]}> was not found anywhere"
28
+ end
29
+ end
30
+
31
+ def initialize(aukro_page, spec_pages)
32
+ @aukro_page = aukro_page
33
+ @spec_pages = spec_pages
34
+ end
35
+
36
+ attr_reader :aukro_page
37
+ attr_reader :spec_pages
38
+
39
+ def name
40
+ @spec_pages.first.name
41
+ end
42
+
43
+ def aukro_finished?
44
+ @aukro_page.finished?
45
+ end
46
+
47
+ def aukro_price
48
+ @aukro_page.price
49
+ end
50
+
51
+ def aukro_deadline
52
+ @aukro_page.deadline
53
+ end
54
+
55
+ def aukro_uri
56
+ @aukro_page.uri
57
+ end
58
+
59
+ def mass
60
+ average :mass
61
+ end
62
+
63
+ def battery_capacity
64
+ average :battery_capacity
65
+ end
66
+
67
+ def stand_by
68
+ average :stand_by
69
+ end
70
+
71
+ def talk_time
72
+ average :talk_time
73
+ end
74
+
75
+ def autofocus
76
+ boolean :autofocus
77
+ end
78
+
79
+ private
80
+
81
+ def average(property)
82
+ values = property_values(property)
83
+ unless values.empty?
84
+ sum = values.inject(0) { |a,b| a + b }
85
+ sum / values.length.to_f
86
+ else
87
+ nil
88
+ end
89
+ end
90
+
91
+ def boolean(property)
92
+ values = property_values(property)
93
+ unless values.empty?
94
+ if values.all? { |v| !!v }
95
+ true
96
+ elsif values.all? { |v| !v }
97
+ false
98
+ else
99
+ raise PropertyError, "Property #{property} of #{name} has values #{values.inspect}"
100
+ + ", expected all true or all false"
101
+ end
102
+ else
103
+ nil
104
+ end
105
+ end
106
+
107
+ def property_values(property)
108
+ @spec_pages.map(&property).select { |v| !v.nil? }
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,15 @@
1
+ module Watchmob
2
+ module ViewHelpers
3
+ def format_datetime(time)
4
+ time.strftime("%Y-%m-%d %H:%M:%S")
5
+ end
6
+
7
+ def format_time_interval(seconds)
8
+ if seconds > 2*24*60*60
9
+ "#{(seconds / (24*60*60)).round} days"
10
+ else
11
+ "#{(seconds / (60*60)).round} hours"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,63 @@
1
+ !!! 5
2
+ %html
3
+ %head
4
+ %meta{:charset => "utf-8"}
5
+ %meta{:"http-equiv" => "Content-Type", :content => "text/html; charset=utf-8"}
6
+ %title Aukro smartphones comparsion
7
+ %style
8
+ :sass
9
+ table#phones
10
+ border-collapse: collapse
11
+ tbody tr:hover
12
+ background-color: #ddd
13
+ tbody tr.finished
14
+ font-style: italic
15
+ th
16
+ font-weight: bold
17
+ text-align: left
18
+ padding-left: 1em
19
+ td
20
+ text-align: right
21
+ padding-right: 1em
22
+ &.name
23
+ text-align: left
24
+ font-weight: bold
25
+
26
+
27
+ %body
28
+ %h1 Aukro smartphones comparsion
29
+
30
+ %table#phones
31
+ %thead
32
+ %tr
33
+ %th.name Name
34
+ %th.price Price
35
+ %th.mass Mass
36
+ %th.battery Battery
37
+ %th.talk-time Talk time
38
+ %th.stand-by Stand by
39
+ %th.autofocus Autofocus
40
+ %th.remaining-time Time
41
+ %th.deadline Deadline
42
+ %th.spec Spec
43
+
44
+ %tbody
45
+ - phones.each do |phone|
46
+ %tr{:class => phone.aukro_finished? ? "finished" : nil}
47
+ %td.name
48
+ %a{:href => phone.aukro_uri.to_s} #{phone.name}
49
+ %td #{phone.aukro_price} CZK
50
+ %td #{phone.mass} g
51
+ %td #{phone.battery_capacity} mAh
52
+ %td #{phone.talk_time} min
53
+ %td #{phone.stand_by / 60} h
54
+ %td #{phone.autofocus.nil? ? "" : (phone.autofocus ? "yes" : "no")}
55
+ %td #{format_time_interval(phone.aukro_deadline.to_time - Time.now)}
56
+ %td #{format_datetime(phone.aukro_deadline.to_time)}
57
+ %td
58
+ - phone.spec_pages.each do |spec|
59
+ %a{:href => spec.uri} #{spec.title}
60
+
61
+ #generated-at
62
+ Generated at #{format_datetime(Time.now)}
63
+
@@ -0,0 +1,47 @@
1
+ require 'timeout'
2
+ require 'sass'
3
+ require 'haml'
4
+
5
+ require 'watchmob/view_helpers'
6
+
7
+ module Watchmob
8
+ class Watchlist
9
+ include ViewHelpers
10
+
11
+ HAML_TEMPLATE = File.expand_path("../watchlist.haml", __FILE__)
12
+ PHONE_TIMEOUT = 15.0
13
+
14
+ def initialize(json)
15
+ @phones = []
16
+
17
+ phone_threads = json.map do |phone_json|
18
+ params = if phone_json.is_a? Array
19
+ {"aukro" => phone_json[0], "phone" => phone_json[1]}
20
+ else
21
+ phone_json
22
+ end
23
+
24
+ Thread.new do
25
+ Thread.current.abort_on_exception = true
26
+ @phones << Smartphone.find(params)
27
+ end
28
+ end
29
+
30
+ Timeout.timeout(PHONE_TIMEOUT) do
31
+ phone_threads.each(&:join)
32
+ end
33
+ end
34
+
35
+ attr_reader :phones
36
+
37
+ def sort_by!(property)
38
+ @phones.sort_by! { |phone| phone.aukro_page.uri.to_s }
39
+ @phones.sort_by! { |phone| phone.__send__(property) or 0 }
40
+ end
41
+
42
+ def to_html
43
+ engine = Haml::Engine.new(File.read(HAML_TEMPLATE), :filename => HAML_TEMPLATE)
44
+ engine.render(self)
45
+ end
46
+ end
47
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: watchmob
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jan Špaček
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: andand
16
+ requirement: &68961290 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *68961290
25
+ - !ruby/object:Gem::Dependency
26
+ name: haml
27
+ requirement: &68960590 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '3.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *68960590
36
+ - !ruby/object:Gem::Dependency
37
+ name: json
38
+ requirement: &68960200 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '1.6'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *68960200
47
+ - !ruby/object:Gem::Dependency
48
+ name: nokogiri
49
+ requirement: &68959790 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *68959790
58
+ - !ruby/object:Gem::Dependency
59
+ name: sass
60
+ requirement: &68958990 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: '3.1'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *68958990
69
+ description: ! " Reads a simple JSON watchlist, loads auction information and\n phone
70
+ specifications from multiple web resources and displays\n the smartphones in clear
71
+ HTML file.\n"
72
+ email: patek.mail@gmail.com
73
+ executables:
74
+ - watchmob
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ./lib/watchmob/aukro_page.rb
79
+ - ./lib/watchmob/heureka_page.rb
80
+ - ./lib/watchmob/smartphone.rb
81
+ - ./lib/watchmob/page.rb
82
+ - ./lib/watchmob/gsmarena_search_page.rb
83
+ - ./lib/watchmob/watchlist.haml
84
+ - ./lib/watchmob/log.rb
85
+ - ./lib/watchmob/view_helpers.rb
86
+ - ./lib/watchmob/error.rb
87
+ - ./lib/watchmob/gsmarena_page.rb
88
+ - ./lib/watchmob/heureka_search_page.rb
89
+ - ./lib/watchmob/watchlist.rb
90
+ - ./lib/watchmob.rb
91
+ - !binary |-
92
+ YmluL3dhdGNobW9i
93
+ homepage: http://github.com/honzasp/watchmob
94
+ licenses:
95
+ - MIT
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 1.8.17
115
+ signing_key:
116
+ specification_version: 3
117
+ summary: Watch smartphones on Aukro.cz and compare them
118
+ test_files: []