watchmob 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.
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: []