dailyrep 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/README.md +17 -0
- data/Rakefile +8 -0
- data/bin/dailyrep +42 -0
- data/config/.gitkeep +0 -0
- data/config/config_template.json +57 -0
- data/config/index_template.html +189 -0
- data/dailyrep.gemspec +27 -0
- data/lib/dailyrep/AppContainer.rb +25 -0
- data/lib/dailyrep/Changable.rb +20 -0
- data/lib/dailyrep/Dbops.rb +28 -0
- data/lib/dailyrep/IBrowser.rb +77 -0
- data/lib/dailyrep/IndexWriter.rb +55 -0
- data/lib/dailyrep/Loadconfig.rb +135 -0
- data/lib/dailyrep/Notificator.rb +23 -0
- data/lib/dailyrep/Trackable.rb +14 -0
- data/lib/dailyrep/entities/Doujob.rb +83 -0
- data/lib/dailyrep/entities/Kinozal.rb +87 -0
- data/lib/dailyrep/entities/Micex.rb +66 -0
- data/lib/dailyrep/entities/Minfin.rb +69 -0
- data/lib/dailyrep/entities/Yanoil.rb +67 -0
- data/lib/dailyrep/version.rb +3 -0
- data/test/dailyrep/Doujob_test.rb +22 -0
- data/test/dailyrep/IBrowser_test.rb +46 -0
- data/test/dailyrep/Kinozal_test.rb +23 -0
- data/test/dailyrep/Micex_test.rb +27 -0
- data/test/dailyrep/Minfin_test.rb +23 -0
- data/test/dailyrep/Yanoil_test.rb +21 -0
- data/test/test_helper.rb +37 -0
- metadata +176 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'json'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
require_relative 'Notificator'
|
7
|
+
require_relative 'IndexWriter'
|
8
|
+
require_relative 'Dbops'
|
9
|
+
require_relative 'Trackable'
|
10
|
+
|
11
|
+
module DailyRep
|
12
|
+
class IBrowser
|
13
|
+
include Dbops
|
14
|
+
include Trackable
|
15
|
+
attr_reader :entity
|
16
|
+
|
17
|
+
@@notificators = Notificator.get_clients
|
18
|
+
@@index_writer = IndexWriter.new
|
19
|
+
@@app_cycle_methods = ['process', 'write_to_db', 'web_reload', 'notify']
|
20
|
+
|
21
|
+
def initialize *inputs
|
22
|
+
@entity = self.class.to_s.downcase.split('::').last
|
23
|
+
end
|
24
|
+
|
25
|
+
def push_note note
|
26
|
+
@@notificators.each { |client|
|
27
|
+
client.push_note(note)
|
28
|
+
}
|
29
|
+
# @@client.push_note(nil, @@note_title, note)
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_html entity, values, push=0
|
33
|
+
@@index_writer.set_html entity, values, push
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_html_with_diff entity, scope, diff, reset=false
|
37
|
+
@@index_writer.set_html_with_diff entity, scope, diff, reset
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.write_html
|
41
|
+
@@index_writer.write_html
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def get_source_http source, val_scope
|
46
|
+
page = Nokogiri::HTML(open(source.to_s))
|
47
|
+
page.css(val_scope)
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_source_json source
|
51
|
+
uri = URI(source)
|
52
|
+
out_json = Net::HTTP.get(uri)
|
53
|
+
JSON.parse(out_json)
|
54
|
+
end
|
55
|
+
|
56
|
+
def method_missing(meth, *args, &block)
|
57
|
+
if @@app_cycle_methods.include?(meth.to_s)
|
58
|
+
begin
|
59
|
+
self.log_msg meth.to_s
|
60
|
+
block.call
|
61
|
+
rescue Exception => e
|
62
|
+
log_error e
|
63
|
+
end
|
64
|
+
else
|
65
|
+
super
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def run
|
70
|
+
self.process
|
71
|
+
self.write_to_db if Configer.is_phase_enabled?('write_to_db')
|
72
|
+
self.notify if Configer.is_phase_enabled?('notify')
|
73
|
+
self.web_reload if Configer.is_phase_enabled?('web_reload')
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module DailyRep
|
2
|
+
class IndexWriter
|
3
|
+
|
4
|
+
def initialize(index_path=Configer.index_path)
|
5
|
+
@out_html_path = index_path.to_s
|
6
|
+
@out_html = Nokogiri::HTML(File.open(@out_html_path))
|
7
|
+
end
|
8
|
+
|
9
|
+
def write_html
|
10
|
+
@out_html.css("#refresh")[0].content = Time.now
|
11
|
+
File.open(@out_html_path , 'w') { |file| file.write(@out_html.to_html) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def set_html entity, params, notif=0
|
15
|
+
entity = entity.downcase
|
16
|
+
params.each do |key, val|
|
17
|
+
@out_html.css("##{entity}_#{key}")[0].content = val
|
18
|
+
end
|
19
|
+
@out_html.css("##{entity}_refresh")[0].content = Time.now
|
20
|
+
@out_html.css("##{entity}_notif")[0].content = Time.now if notif == 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_html_with_diff entity, scope, diff, reset=false
|
24
|
+
entity = entity.downcase
|
25
|
+
target_div = @out_html.css(scope + " .list-group .list-group-item")
|
26
|
+
new_root_node = Nokogiri::XML::Node.new('div', @out_html)
|
27
|
+
new_root_node['class'] = 'list-group'
|
28
|
+
|
29
|
+
|
30
|
+
diff.each do |diff_el|
|
31
|
+
target_div.each { |node|
|
32
|
+
if diff_el[0] == '+' && node['href'].include?(diff_el[2].first[0]) then
|
33
|
+
target_div.delete(node)
|
34
|
+
end
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
new_root_node.add_child(target_div.unlink) if not reset
|
39
|
+
|
40
|
+
diff.each do |diff_el|
|
41
|
+
if diff_el[0] == '-' then
|
42
|
+
nodeel = Nokogiri::XML::Node.new('a', @out_html)
|
43
|
+
nodeel['href'] = diff_el[2].first[0]
|
44
|
+
nodeel['class'] = 'list-group-item'
|
45
|
+
nodeel.content = diff_el[2].first[1]
|
46
|
+
new_root_node.add_child(nodeel)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
@out_html.at_css(scope + " .list-group").replace(new_root_node)
|
51
|
+
|
52
|
+
@out_html.css("##{entity}_refresh")[0].content = Time.now
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
|
2
|
+
class String
|
3
|
+
def to_bool
|
4
|
+
return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
|
5
|
+
return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
|
6
|
+
raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
ActiveRecord::Base.establish_connection(
|
11
|
+
:adapter => 'sqlite3',
|
12
|
+
:database => DATABASE,
|
13
|
+
:pool => 5,
|
14
|
+
:timeout => 5000
|
15
|
+
)
|
16
|
+
|
17
|
+
def generate_database
|
18
|
+
ActiveRecord::Schema.define(version: 201502161646) do
|
19
|
+
create_table "Histories", force: false do |t|
|
20
|
+
t.text "entity"
|
21
|
+
t.text "parameter"
|
22
|
+
t.text "val"
|
23
|
+
t.datetime "created_at"
|
24
|
+
t.datetime "notified"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
tables = ActiveRecord::Base.connection.execute(
|
30
|
+
"SELECT name FROM sqlite_master WHERE type='table' and name = 'Histories'; "
|
31
|
+
)
|
32
|
+
|
33
|
+
if tables.empty?
|
34
|
+
generate_database
|
35
|
+
else
|
36
|
+
generate_database unless tables[0]["name"] == 'Histories'
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
class History < ActiveRecord::Base
|
41
|
+
end
|
42
|
+
|
43
|
+
module DailyRep
|
44
|
+
|
45
|
+
START_TIME = Time.now
|
46
|
+
|
47
|
+
#constants
|
48
|
+
P_GO_FILE_PATH = 'GoFilePath'
|
49
|
+
P_PUSH_CLIENTS = 'Push clients'
|
50
|
+
P_PUSH_NOTE_TITLE = 'Push title'
|
51
|
+
P_RAISE_ERRORS = 'Raise errors'
|
52
|
+
|
53
|
+
PA_ENTITIES = 'Entities'
|
54
|
+
PA_APP_CYCLE = 'app_cycle'
|
55
|
+
P_INDEX_PATH = 'Index path'
|
56
|
+
P_NOTIF_DELTA = 'notification delta'
|
57
|
+
P_ENABLED = 'enabled'
|
58
|
+
P_TIME_FRAME = 'time frame'
|
59
|
+
P_SOURCE = 'source'
|
60
|
+
P_SEARCH_STRS = 'searchStr'
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
class Configer
|
67
|
+
def self.set_index_file file
|
68
|
+
@@config_json[P_INDEX_PATH] = file
|
69
|
+
end
|
70
|
+
|
71
|
+
File.open(CONFIG_FILE) {|file| @@config_json = JSON.parse(file.read) }
|
72
|
+
|
73
|
+
def self.index_path
|
74
|
+
@@config_json[P_INDEX_PATH]
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.push_note_title
|
78
|
+
@@config_json[P_PUSH_NOTE_TITLE]
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.raise_error?
|
82
|
+
@@config_json[P_RAISE_ERRORS].to_bool
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.exctract_entity_params entity, parameter_name
|
86
|
+
@@config_json[PA_ENTITIES][entity][parameter_name]
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.is_phase_enabled? phase
|
90
|
+
@@config_json[PA_APP_CYCLE][phase].to_bool
|
91
|
+
end
|
92
|
+
#+
|
93
|
+
def self.is_entity_enabled? entity
|
94
|
+
time_arr = @@config_json[PA_ENTITIES][entity][P_TIME_FRAME].split('..').map { |el|
|
95
|
+
el.to_i
|
96
|
+
}
|
97
|
+
(time_arr[0]..time_arr[1]).include?(Time.now.hour) &&
|
98
|
+
@@config_json[PA_ENTITIES][entity][P_ENABLED].to_bool
|
99
|
+
end
|
100
|
+
#+
|
101
|
+
def self.enabled_entities
|
102
|
+
@@config_json[PA_ENTITIES].keys.map { |entity|
|
103
|
+
entity.downcase.capitalize if is_entity_enabled?(entity)
|
104
|
+
}
|
105
|
+
end
|
106
|
+
#+
|
107
|
+
def self.get_client_tokens
|
108
|
+
@@config_json[P_PUSH_CLIENTS].values
|
109
|
+
end
|
110
|
+
|
111
|
+
# def self.include_search_strs? entity
|
112
|
+
# result = false
|
113
|
+
# result = true if @@config_json[PA_ENTITIES][entity][P_SEARCH_STRS]
|
114
|
+
# return result
|
115
|
+
# end
|
116
|
+
|
117
|
+
def self.get_search_strs entity
|
118
|
+
@@config_json[PA_ENTITIES][entity][P_SEARCH_STRS]
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.method_missing(meth, *args, &block)
|
122
|
+
method_inputs = meth.to_s.split('_')
|
123
|
+
out_param = ''
|
124
|
+
@@config_json[PA_ENTITIES].keys.each { |key|
|
125
|
+
if method_inputs[0] == key.downcase
|
126
|
+
out_param = self.exctract_entity_params method_inputs[0], method_inputs[1]
|
127
|
+
end
|
128
|
+
}
|
129
|
+
super if not @@config_json[PA_ENTITIES].keys.include?(method_inputs[0])
|
130
|
+
return out_param
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'washbullet'
|
2
|
+
module DailyRep
|
3
|
+
|
4
|
+
class Notificator
|
5
|
+
def self.get_clients
|
6
|
+
result = []
|
7
|
+
result = Configer.get_client_tokens.map { |token|
|
8
|
+
new token
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(push_token, note_title=Configer.push_note_title)
|
13
|
+
@client = Washbullet::Client.new(push_token)
|
14
|
+
@note_title = note_title
|
15
|
+
end
|
16
|
+
|
17
|
+
def push_note note
|
18
|
+
@client.push_note(nil, @note_title, note)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module DailyRep
|
2
|
+
module Trackable
|
3
|
+
def log_msg entity, add_param='', phase
|
4
|
+
p "#{entity} (#{add_param}), stage: #{phase} at #{Time.now}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def log_error e
|
8
|
+
# p " ERROR!: #{e.message}"
|
9
|
+
# p " ERROR_MSG: #{e.backtrace.inspect}"
|
10
|
+
raise
|
11
|
+
# if Configer.raise_error?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require_relative '../Dbops'
|
2
|
+
|
3
|
+
module DailyRep
|
4
|
+
module Entities
|
5
|
+
|
6
|
+
|
7
|
+
class Doujob < IBrowser
|
8
|
+
attr_reader :diff
|
9
|
+
include Dbops
|
10
|
+
|
11
|
+
def self.create
|
12
|
+
result = []
|
13
|
+
result = Configer.get_search_strs('doujob').map { |str|
|
14
|
+
new str
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize search, source=Configer.doujob_source
|
19
|
+
super
|
20
|
+
@search=search.gsub(' ','+')
|
21
|
+
@dou_search_path = source + @search
|
22
|
+
@vacancy_scope = 'div#vacancyListId li'
|
23
|
+
@parameter = 'title-href'
|
24
|
+
@diff =[]
|
25
|
+
end
|
26
|
+
|
27
|
+
def log_msg phase
|
28
|
+
super @entity, @search, phase
|
29
|
+
end
|
30
|
+
|
31
|
+
#overwriting
|
32
|
+
def process
|
33
|
+
super do
|
34
|
+
out_set = get_source_http @dou_search_path, @vacancy_scope
|
35
|
+
@out_vac = out_set.map do |a|
|
36
|
+
[a.css('.title .vt').attribute("href").text, "#{a.css('.company').text} : #{a.css('.title .vt').text}"]
|
37
|
+
end
|
38
|
+
out_hash = @out_vac.map { |el|
|
39
|
+
{el[0] => el[1]}
|
40
|
+
}
|
41
|
+
hist_hash = read_hist_hash
|
42
|
+
@diff = HashDiff.diff(out_hash, hist_hash)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
#overwriting
|
47
|
+
def notify
|
48
|
+
super do
|
49
|
+
@diff.each { |lv1|
|
50
|
+
push_note("New vacancy at DOU: #{lv1[2]} - #{lv1[3]} ") if lv1[0] == "-"
|
51
|
+
push_note("DOU vacancy has gone: #{lv1[2]} - #{lv1[3]} ") if lv1[0] == "+"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
#overwriting
|
56
|
+
def write_to_db
|
57
|
+
super do
|
58
|
+
@out_vac.each { |vac|
|
59
|
+
write_hist @entity, @parameter, "#{vac[0]}=>#{vac[1]}"
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
#overwriting
|
64
|
+
def web_reload
|
65
|
+
super do
|
66
|
+
set_html_with_diff @entity, '#doujobs', @diff
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def read_hist_hash
|
72
|
+
maxtime = History.where(entity: @entity, parameter: @parameter).where("created_at > ?", Time.now - 6.hours).maximum("created_at")
|
73
|
+
maxtime||= Time.now
|
74
|
+
maxtime = maxtime.to_datetime
|
75
|
+
History.select("val").where(entity: @entity, parameter: @parameter).where("created_at > ?", maxtime - 1.minute).map { |row|
|
76
|
+
el_ar = row.val.split('=>')
|
77
|
+
{el_ar[0] => el_ar[1]}
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
module DailyRep
|
4
|
+
module Entities
|
5
|
+
class Kinozal < IBrowser
|
6
|
+
attr_reader :out_hash_clean, :to_notification
|
7
|
+
|
8
|
+
def self.create
|
9
|
+
result = []
|
10
|
+
result = Configer.get_search_strs('kinozal').map { |str|
|
11
|
+
new str
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize movie, source=Configer.kinozal_source, filters=Configer.kinozal_filters
|
16
|
+
super
|
17
|
+
@movie = movie
|
18
|
+
@search_path = source+@movie.gsub(' ','+') + filters
|
19
|
+
@scope = 'td.nam a'
|
20
|
+
@dirty_words = ['тизер', 'трейлер']
|
21
|
+
@out_hash_clean = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def log_msg phase
|
25
|
+
super @entity, @movie, phase
|
26
|
+
end
|
27
|
+
|
28
|
+
def process
|
29
|
+
super do
|
30
|
+
out_set = get_source_http @search_path, @scope
|
31
|
+
out_hash_dirty = {}
|
32
|
+
out_set.each { |el|
|
33
|
+
out_hash_dirty.merge!({ el['href'] => el.text.split(' / ')})
|
34
|
+
}
|
35
|
+
out_hash_dirty.each { |key, val|
|
36
|
+
exclude = 0
|
37
|
+
@dirty_words.each { |word|
|
38
|
+
if val[0].mb_chars.downcase.include?(word) then
|
39
|
+
exclude = 1
|
40
|
+
break
|
41
|
+
end
|
42
|
+
}
|
43
|
+
@out_hash_clean.merge!({key => val}) if exclude == 0
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_to_db
|
49
|
+
super do
|
50
|
+
@to_notification = {}
|
51
|
+
@out_hash_clean.each { |key, val|
|
52
|
+
id = key.split('id=')[1]
|
53
|
+
if !check_row_exists(@entity, @movie, id) then
|
54
|
+
write_hist(@entity, @movie, id, 1)
|
55
|
+
@to_notification.merge!({key => [val[0], val.last]})
|
56
|
+
end
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def notify
|
62
|
+
super do
|
63
|
+
@to_notification.each { |key, val|
|
64
|
+
note = "Kinozal
|
65
|
+
Movie appears: #{val[0]}
|
66
|
+
Quality: #{val.last}
|
67
|
+
Link: #{key}"
|
68
|
+
push_note note
|
69
|
+
}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def web_reload
|
75
|
+
super do
|
76
|
+
i = 0
|
77
|
+
diff = @to_notification.map { |key, val|
|
78
|
+
i = i+1
|
79
|
+
['-', i , {'http://kinozal.tv/'+ key => val.join(' : ')} ]
|
80
|
+
}
|
81
|
+
set_html_with_diff @entity, '#kinozal', diff, true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|