hikkmemo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 460d695a343ba81ba8e772c50d3203db69fcd808
4
+ data.tar.gz: 9b9dcc7a0751f8b461f9d55edf192a3663343080
5
+ SHA512:
6
+ metadata.gz: ea968b2495164105c24025a9b4125801d88e0eefa89bc1ca8124c66b8b71d0bf20893ea9ede1e781e927989f7dab9748a8ed8ad1d7af0cc68dd6c274fc60c238
7
+ data.tar.gz: f4c42be90b90db1dcb40722b15cf6e63dbd5cd398d7d0bcfc07ade8769a6d295b8b5c2994fc564a93a1b5dde316c34591e85a1565179aa9734c906b698e604c5
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (C) 2013 Ulthar Sothoth
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'hikkmemo'
3
+ s.version = '1.0.0'
4
+ s.license = 'MIT'
5
+ s.author = 'Ulthar Sothoth'
6
+ s.email = 'ulthar.ix@gmail.com'
7
+ s.homepage = 'http://github.com/ulthar/hikkmemo'
8
+ s.summary = 'Imageboard memoizer.'
9
+ s.description = 'Extensible/customizable/programmable thread/post/image memoizer for imageboards.'
10
+
11
+ s.add_runtime_dependency 'nokogiri', '~> 1.5'
12
+ s.add_runtime_dependency 'sqlite3', '~> 1.3'
13
+ s.add_runtime_dependency 'sequel', '~> 3.46'
14
+ s.add_runtime_dependency 'rainbow', '~> 1.1'
15
+ s.add_runtime_dependency 'unicode_utils', '~> 1.4'
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.require_path = 'lib'
19
+ end
@@ -0,0 +1,12 @@
1
+ require 'hikkmemo/session'
2
+ require 'hikkmemo/readers'
3
+
4
+ module Hikkmemo
5
+ module_function
6
+ def run(*args, &block)
7
+ Thread.abort_on_exception = true
8
+ s = Session.new(*args)
9
+ s.instance_eval(&block)
10
+ s.interact
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ require 'nokogiri'
2
+
3
+ module Hikkmemo
4
+ class Reader
5
+ attr_reader :url
6
+
7
+ def initialize(url, &block)
8
+ @url = url
9
+ self.instance_eval(&block)
10
+ end
11
+
12
+ def doc(url)
13
+ begin
14
+ Nokogiri::HTML(open(url))
15
+ rescue
16
+ puts "Failed accessing #{url}, retrying..."
17
+ sleep 2
18
+ retry
19
+ end
20
+ end
21
+
22
+ def fringe
23
+ @threads.(doc(@url)).map do |t|
24
+ [@thread_id.(t), @post_id.(@posts.(t).last)]
25
+ end
26
+ end
27
+
28
+ def thread_posts(thread_id, after: nil)
29
+ posts = @posts.(doc(@thread_url.(thread_id)))
30
+ after ? posts.drop_while {|p| @post_id.(p) <= after } : posts
31
+ end
32
+
33
+ def post_data(node, thread_id)
34
+ { :id => @post_id.(node),
35
+ :thread => thread_id,
36
+ :date => @post_date.(node),
37
+ :author => @post_author.(node),
38
+ :subject => @post_subject.(node),
39
+ :message => @post_message.(node),
40
+ :image => @post_image.(node),
41
+ :embed => @post_embed.(node)
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,149 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'date'
3
+ require 'hikkmemo/reader'
4
+ require 'hikkmemo/util'
5
+ require "unicode_utils/titlecase"
6
+
7
+ module Hikkmemo
8
+ module Readers
9
+ module_function
10
+
11
+ def nullchan(section)
12
+ Reader.new('http://0chan.hk' + section) do
13
+ @threads = ->(d) { d.css('div[id^="thread"]') }
14
+ @posts = ->(d) { d.css('div.postnode') }
15
+ @thread_url = ->(i) { "http://0chan.hk#{section}res/#{i}.html" }
16
+ @thread_id = ->(t) { t['id'].tr('^0-9', '').to_i }
17
+ @post_id = ->(p) { p.css('span[class^="dnb"]')[0]['class'].tr('^0-9', '').to_i }
18
+ @post_subject = ->(p) { p.css('span.filetitle').text }
19
+ @post_author = ->(p) {
20
+ trip = p.css('span.postertrip')[0]
21
+ p.css('span.postername').text + (trip && trip.text || '')
22
+ }
23
+ @post_date = ->(p) {
24
+ date_str = p.css('label').children.last.text.strip
25
+ DateTime.strptime(Util.delocalize_ru_date(date_str), '%a %Y %b %e %H:%M:%S')
26
+ }
27
+ @post_message = ->(p) {
28
+ msg = p.css('div.postmessage')
29
+ msg.children.each {|c| c.replace(c.text + "\n") if ['br', 'span'].include?(c.name) }
30
+ msg.text.strip
31
+ }
32
+ @post_image = ->(p) {
33
+ img = p.css('img')[0]
34
+ img && "http://0chan.hk#{section}src/#{File.basename(img['src']).to_i}#{File.extname(img['src'])}"
35
+ }
36
+ @post_embed = ->(p) {
37
+ vid = p.css('embed')[0]
38
+ vid && vid['src']
39
+ }
40
+ end
41
+ end
42
+
43
+ def dvach_hk(section)
44
+ Reader.new('http://2ch.hk' + section) do
45
+ @threads = ->(d) { d.css('div.thread') }
46
+ @posts = ->(d) { d.css('div.oppost') + d.css('table.post') }
47
+ @thread_url = ->(i) { "http://2ch.hk#{section}res/#{i}.html" }
48
+ @thread_id = ->(t) { t['id'][7..-1].to_i }
49
+ @post_id = ->(p) { p['id'][5..-1].to_i }
50
+ @post_subject = ->(p) { p.css('span.subject').text }
51
+ @post_author = ->(p) {
52
+ trip = p.css('span.postertrip')[0]
53
+ p.css('span.name').text + (trip && trip.text || '')
54
+ }
55
+ @post_date = ->(p) {
56
+ date_str = p.css('span.posttime').text.strip
57
+ DateTime.strptime(Util.delocalize_ru_date(date_str), '%a %e %b %Y %H:%M:%S')
58
+ }
59
+ @post_message = ->(p) {
60
+ msg = p.css('blockquote.postMessage p')
61
+ msg.children.each {|c| c.replace(c.text + "\n") if ['br'].include?(c.name) }
62
+ msg.text.strip
63
+ }
64
+ @post_image = ->(p) {
65
+ img = p.css('span[id^="exlink"] a')[0]
66
+ img && "http://2ch.hk#{img['href']}"
67
+ }
68
+ @post_embed = ->(p) {
69
+ vid = p.css('embed')[0]
70
+ vid && vid['src']
71
+ }
72
+ end
73
+ end
74
+
75
+ def dobrochan(section)
76
+ Reader.new('http://dobrochan.ru' + section) do
77
+ @threads = ->(d) { d.css('div.thread') }
78
+ @posts = ->(d) { d.css('div.oppost') + d.css('table.post') }
79
+ @thread_url = ->(i) { "http://dobrochan.ru#{section}res/#{i}.xhtml" }
80
+ @thread_id = ->(t) { t['id'][7..-1].to_i }
81
+ @post_id = ->(p) { p['id'][5..-1].to_i }
82
+ @post_subject = ->(p) { p.css('span.replytitle').text }
83
+ @post_author = ->(p) {
84
+ trip = p.css('span.postertrip')[0]
85
+ p.css('span.postername').text + (trip && trip.text || '')
86
+ }
87
+ @post_date = ->(p) {
88
+ date_str = p.css('label').children.last.text.strip
89
+ DateTime.strptime(date_str, '%e %B %Y (%a) %H:%M')
90
+ }
91
+ @post_message = ->(p) {
92
+ msg = p.css('div.message')
93
+ msg.children.each {|c| c.replace("\n" + c.text.delete("\n")) if c.name == 'blockquote' }
94
+ msg.text.strip
95
+ }
96
+ @post_image = ->(p) {
97
+ imgs = p.css('a[href^="/src"]').to_a.map{|a| a['href'] }.uniq
98
+ imgs.size > 0 && imgs.map {|img| "http://dobrochan.ru#{img}" }.join(',')
99
+ }
100
+ @post_embed = ->(_) { nil }
101
+ end
102
+ end
103
+
104
+ def iichan(section)
105
+ Reader.new('http://iichan.hk' + section) do
106
+ @threads = ->(d) { d.css('div[id^="thread"]') }
107
+ @posts = ->(d) {
108
+ d.css('span.filetitle').attr('class', 'replytitle')
109
+ d.css('span.commentpostername').attr('class', 'postername')
110
+ d = d.at_css('div[id^="thread"]') if d.name == 'document'
111
+ p = d.at_css('table')
112
+ r = p && (d.children.index(p) - 1) || -1
113
+ td = d.document.create_element 'td', {
114
+ :class => 'reply',
115
+ :id => "reply#{d['id'][7..-1].to_i}"
116
+ }
117
+ td.children = d.children[0..r].unlink
118
+ if d.children.empty? then d.add_child(td) else d.children.before(td) end
119
+ d.css('td.reply')
120
+ }
121
+ @thread_url = ->(i) { "http://iichan.hk#{section}res/#{i}.html" }
122
+ @thread_id = ->(t) { t['id'][7..-1].to_i }
123
+ @post_id = ->(p) { p['id'][5..-1].to_i }
124
+ @post_subject = ->(p) { p.css('span.replytitle').text }
125
+ @post_author = ->(p) {
126
+ trip = p.css('span.postertrip')[0]
127
+ p.css('span.postername').text + (trip && trip.text || '')
128
+ }
129
+ @post_date = ->(p) {
130
+ d = p.css('label')[0].children.last.text.strip.split(' ').drop(1)
131
+ d[1] = 'Май' if d[1] == 'мая'
132
+ d[1] = UnicodeUtils.titlecase(d[1][0..2])
133
+ date_str = Util.delocalize_ru_date(d.join(' '))
134
+ DateTime.strptime(date_str, '%e %b %Y %H:%M:%S')
135
+ }
136
+ @post_message = ->(p) {
137
+ msg = p.at_css('blockquote')
138
+ msg.traverse {|c| c.replace(c.text + "\n") if ['p', 'br'].include?(c.name) }
139
+ msg.text.strip
140
+ }
141
+ @post_image = ->(p) {
142
+ img = p.at_css('span.filesize a[target="_blank"]')
143
+ img && "http://iichan.hk#{img['href']}"
144
+ }
145
+ @post_embed = ->(_) { nil }
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,25 @@
1
+ module Hikkmemo
2
+ class RingBuffer
3
+ def initialize(capacity)
4
+ @buff = [nil] * capacity
5
+ @i = 0
6
+ end
7
+
8
+ def push(elem)
9
+ @buff[@i] = elem
10
+ @i = @i == @buff.size - 1 ? 0 : @i + 1
11
+ self
12
+ end
13
+
14
+ def pop
15
+ @i = (@i == 0 ? @buff.size : @i) - 1
16
+ @buff[@i]
17
+ end
18
+
19
+ def last_n(n)
20
+ n = [n, @buff.size].min
21
+ i = @i == 0 ? @buff.size : @i
22
+ i < n ? @buff[-(n-i)..-1] + @buff[0..i-1] : @buff[i-n..i-1]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,213 @@
1
+ require 'fileutils'
2
+ require 'nokogiri'
3
+ require 'open-uri'
4
+ require 'readline'
5
+ require 'rainbow'
6
+ require 'sequel'
7
+ require 'hikkmemo/ring_buffer'
8
+ require 'hikkmemo/worker'
9
+ require 'hikkmemo/util'
10
+
11
+ module Hikkmemo
12
+ class Session
13
+ attr_reader :path, :workers, :history
14
+
15
+ def initialize(path, opts = {})
16
+ opts[:log_to] ||= :files
17
+ opts[:boards] ||= {}
18
+ opts[:history_size] ||= 10
19
+ @prompt = opts[:prompt] || 'hikkmemo/%b>'
20
+ @prompt_color = opts[:prompt_color] || :default
21
+ @theme = opts[:theme] || :solid
22
+ @colors = opts[:colors] || :default
23
+ @log_msg = opts[:log_msg] || '%t %k (%b) %m'
24
+ @msg_sz = opts[:msg_sz] || 100
25
+
26
+ @path = File.expand_path(path).chomp('/')
27
+ @history = RingBuffer.new(opts[:history_size])
28
+ @aux_cnt = 0
29
+ @workers = {}
30
+ @cmds = {}
31
+ @board = opts[:boards].keys[0].to_s
32
+
33
+ cl = ->(k,b,m) { print "\r#{log_msg(k,b,m)}\n#{prompt}"}
34
+ fl = ->(k,b,m) { File.open("#{@path}/#{b}.log", 'a') {|f| f.puts "#{time} #{k} #{m}" } }
35
+ @logger = {
36
+ :console => cl,
37
+ :files => fl,
38
+ :console_and_files => ->(k,b,m) { cl.(k,b,m); fl.(k,b,m) }
39
+ }[opts[:log_to]]
40
+
41
+ opts[:boards].each {|b,r| serve(b.to_s, r) }
42
+ end
43
+
44
+ def serve(board, reader)
45
+ FileUtils.mkdir_p [@path, "#{@path}/#{board}/"]
46
+ File.open("#{@path}/#{board}.db", 'a') {}
47
+ db = Sequel.sqlite("#{@path}/#{board}.db")
48
+
49
+ db.create_table? :posts do
50
+ Integer :id, :primary_key => true
51
+ foreign_key :thread, :threads
52
+ DateTime :date
53
+ String :author
54
+ String :subject
55
+ String :message
56
+ String :image
57
+ String :embed
58
+ end
59
+
60
+ db.create_table? :threads do
61
+ Integer :id, :primary_key => true
62
+ Integer :last_post
63
+ DateTime :date
64
+ end
65
+
66
+ worker = @workers[board] = Worker.new(db, reader)
67
+ worker.on_add_thread {|t| log('+', board, "[#{t[:id]}]") }
68
+ worker.on_add_post do |p|
69
+ log('+', board, "#{p[:id]}[#{p[:thread]}] - '#{p[:message][0..@msg_sz].tr("\n",'')}...'")
70
+ (img = p[:image]) && img.split(',').each {|img| download_image(board, img) }
71
+ end
72
+ worker.run
73
+ end
74
+
75
+ def interact
76
+ loop do
77
+ inp = Readline.readline(prompt, true)
78
+ next unless inp
79
+ cmd = inp.split
80
+ case cmd[0]
81
+ when 'exit' then break
82
+ when 'help'
83
+ puts '=========================================================================='
84
+ puts '# b = board'
85
+ puts '# p = post id'
86
+ puts '# t = thread id'
87
+ puts '# ?x = optional x'
88
+ puts '=========================================================================='
89
+ puts 'history n -- last n events'
90
+ puts 'context b -- set board in context (for cmds like "post" and "thread")'
91
+ puts 'post p ?b -- print post'
92
+ puts 'posts n ?b -- last n posts'
93
+ puts 'tposts n t ?b -- last n posts of thread'
94
+ puts 'thread t ?b -- print all posts of thread'
95
+ when 'history' then @history.last_n(cmd[1].to_i).each {|m| puts m }
96
+ when 'context' then cmd_context cmd[1]
97
+ when 'post' then cmd_post cmd[1].to_i, cmd[2] || @board
98
+ when 'posts' then cmd_posts cmd[1].to_i, cmd[2] || @board
99
+ when 'tposts' then cmd_tposts cmd[1].to_i, cmd[2].to_i, cmd[3] || @board
100
+ when 'thread' then cmd_thread cmd[1].to_i, cmd[2] || @board
101
+ else
102
+ fn = @cmds[cmd[0]]
103
+ fn && fn.(cmd.drop(1))
104
+ end
105
+ puts ''
106
+ end
107
+ end
108
+
109
+ def notice(msg)
110
+ print "\r#{time} ~ #{msg}\n#{prompt}".color(:cyan)
111
+ end
112
+
113
+ def hook(board = nil, &block)
114
+ if board
115
+ with_board_worker (board.to_s) {|w| w.on_add_post(&block) }
116
+ else
117
+ @workers.each do |b,w|
118
+ w.on_add_post {|p| block.call(p,b) }
119
+ end
120
+ end
121
+ end
122
+
123
+ def cmd(name, &block)
124
+ @cmds[name] = block
125
+ end
126
+
127
+ private
128
+
129
+ def log(kind, board, msg)
130
+ @history.push log_msg(kind, board, msg, colored: false)
131
+ @logger.(kind, board, msg)
132
+ end
133
+
134
+ def time
135
+ Time.now.strftime '%H:%M:%S'
136
+ end
137
+
138
+ def log_msg(kind, board, msg, colored: true)
139
+ text = @log_msg.gsub /%t|%k|%b|%m/, {
140
+ '%t' => time, '%k' => kind,
141
+ '%b' => board, '%m' => msg
142
+ }
143
+ colored ? colorize(text) : text
144
+ end
145
+
146
+ def colorize(text)
147
+ case @theme
148
+ when :solid then text.color(@colors)
149
+ when :zebra then text.color(@colors[@aux_cnt = (@aux_cnt + 1) % @colors.size])
150
+ else text
151
+ end
152
+ end
153
+
154
+ def prompt
155
+ @prompt.gsub('%b', @board).color(@prompt_color)
156
+ end
157
+
158
+ def download_image(board, src)
159
+ path = "#{@path}/#{board}/#{File.basename(src)}"
160
+ begin
161
+ bytes = open(src).read
162
+ File.open(path, 'wb') {|f| f << bytes }
163
+ rescue
164
+ log('!', board, src)
165
+ else
166
+ log('@', board, path)
167
+ end
168
+ end
169
+
170
+ def print_post(post)
171
+ puts "\n(#{post[:author]}) - #{post[:date]} - #{post[:id]}[#{post[:thread]}]".underline
172
+ puts post[:message]
173
+ end
174
+
175
+ def with_board_worker(board, &block)
176
+ worker = @workers[board]
177
+ worker ? block.call(worker) : puts('unknown board')
178
+ end
179
+
180
+ def cmd_context(board)
181
+ with_board_worker (board) { @board = board }
182
+ end
183
+
184
+ def cmd_post(id, board)
185
+ with_board_worker board do |wr|
186
+ post = wr.db[:posts][:id => id]
187
+ post ? print_post(post) : puts('post not found')
188
+ end
189
+ end
190
+
191
+ def cmd_posts(n, board)
192
+ with_board_worker board do |wr|
193
+ wr.db[:posts].order(Sequel.desc(:date)).limit(n)
194
+ .all.reverse.each {|p| print_post(p) }
195
+ end
196
+ end
197
+
198
+ def cmd_tposts(n, tid, board)
199
+ with_board_worker board do |wr|
200
+ wr.db[:posts].where(:thread => tid)
201
+ .order(Sequel.desc(:date)).limit(n)
202
+ .all.reverse.each {|p| print_post(p) }
203
+ end
204
+ end
205
+
206
+ def cmd_thread(tid, board)
207
+ with_board_worker board do |wr|
208
+ if_nil wr.db[:threads][:id => tid] { puts 'thread not found'; return }
209
+ wr.db[:posts].where(:thread => tid).each {|p| print_post(p) }
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,14 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Hikkmemo
3
+ module Util
4
+ module_function
5
+ def delocalize_ru_date(string)
6
+ string.gsub /Пнд|Втр|Срд|Чтв|Птн|Суб|Вск|Янв|Фев|Мар|Апр|Май|Июн|Июл|Авг|Сен|Окт|Ноя|Дек/, {
7
+ 'Пнд' => 'Mon', 'Втр' => 'Tue', 'Срд' => 'Wed',
8
+ 'Чтв' => 'Thu', 'Птн' => 'Fri', 'Суб' => 'Sat', 'Вск' => 'Sun',
9
+ 'Янв' => 'Jan', 'Фев' => 'Feb', 'Мар' => 'Mar', 'Апр' => 'Apr', 'Май' => 'May', 'Июн' => 'Jun',
10
+ 'Июл' => 'Jul', 'Авг' => 'Aug', 'Сен' => 'Sep', 'Окт' => 'Oct', 'Ноя' => 'Nov', 'Дек' => 'Dec'
11
+ }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,49 @@
1
+ module Hikkmemo
2
+ class Worker
3
+ attr_reader :db
4
+ attr_accessor :timeout, :thread
5
+
6
+ def initialize(db, reader)
7
+ @db, @reader = db, reader
8
+ @on_add_thread = []
9
+ @on_add_post = []
10
+ @timeout = 30
11
+ end
12
+
13
+ def add_post(post_node, thread_id)
14
+ data = @reader.post_data(post_node, thread_id)
15
+ unless @db[:posts][:id => data[:id]]
16
+ @db[:posts].insert(data)
17
+ @on_add_post.each {|p| p.(data) }
18
+ end
19
+ end
20
+
21
+ def on_add_post (&p) @on_add_post += [p] end
22
+ def on_add_thread(&p) @on_add_thread += [p] end
23
+
24
+ def run
25
+ @thread ||= Thread.new do
26
+ loop do
27
+ @reader.fringe.each do |tid,pid|
28
+ thread = @db[:threads][:id => tid]
29
+ if thread
30
+ if thread[:last_post] != pid
31
+ @reader.thread_posts(tid, after: thread[:last_post])
32
+ .each {|p| add_post(p, tid) }
33
+ @db[:threads][:id => tid] = { :last_post => pid }
34
+ end
35
+ else
36
+ posts = @reader.thread_posts(tid)
37
+ date = @reader.post_data(posts[0], tid)[:date]
38
+ data = { :id => tid, :last_post => pid, :date => date }
39
+ @db[:threads].insert(data)
40
+ @on_add_thread.each {|p| p.(data) }
41
+ posts.each {|p| add_post(p, tid) }
42
+ end
43
+ end
44
+ sleep @timeout
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hikkmemo
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ulthar Sothoth
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-04-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sequel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '3.46'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '3.46'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rainbow
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: unicode_utils
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '1.4'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '1.4'
83
+ description: Extensible/customizable/programmable thread/post/image memoizer for imageboards.
84
+ email: ulthar.ix@gmail.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - LICENSE
90
+ - hikkmemo.gemspec
91
+ - lib/hikkmemo.rb
92
+ - lib/hikkmemo/reader.rb
93
+ - lib/hikkmemo/readers.rb
94
+ - lib/hikkmemo/ring_buffer.rb
95
+ - lib/hikkmemo/session.rb
96
+ - lib/hikkmemo/util.rb
97
+ - lib/hikkmemo/worker.rb
98
+ homepage: http://github.com/ulthar/hikkmemo
99
+ licenses:
100
+ - MIT
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - '>='
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 2.0.0
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Imageboard memoizer.
122
+ test_files: []