robi 0.0.1
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 +7 -0
- data/bin/robi +13 -0
- data/lib/optparser.rb +37 -0
- data/lib/robi/compiler.rb +179 -0
- data/lib/robi/fetcher.rb +46 -0
- data/lib/robi/post.rb +49 -0
- data/lib/robi.rb +41 -0
- data/lib/static/stylesheet.css +23 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 67c9217164cb562bda4b90c2330e8ed7f8a3dc70
|
|
4
|
+
data.tar.gz: eddff5c9f49640c852d3992f22464d27a95a98d3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2e441b7ea0deb38e1b07e5352081cfeebbe5a71b9b9b32612d2ed7725854ce5a9ff0b7c80e6656edb2692365d8a080fa43e5ad716b61d889632486b228d68d53
|
|
7
|
+
data.tar.gz: c05727623b778fa1a16f1e78ae0b585bde44cc473c0a3fab29762852f98c407c3d66f310e67d63af2d72b6e1cc32b9a528e724ad52864e3b72e97f45f4139d66
|
data/bin/robi
ADDED
data/lib/optparser.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
|
|
3
|
+
module OptParser
|
|
4
|
+
TYPES = %i(hot new rising controversial top gilded).freeze
|
|
5
|
+
|
|
6
|
+
def self.parse
|
|
7
|
+
opt = {}
|
|
8
|
+
|
|
9
|
+
OptionParser.new { |options|
|
|
10
|
+
options.banner = 'Usage: robi SUBREDDIT [-t TYPE] [-c COUNT]'
|
|
11
|
+
|
|
12
|
+
opt[:subreddit] = ARGV.first
|
|
13
|
+
opt[:type] = :hot
|
|
14
|
+
opt[:count] = 25
|
|
15
|
+
|
|
16
|
+
options.on('-t', '--type TYPE', 'top, new, etc. (default hot)') { |type|
|
|
17
|
+
opt[:type] = type.downcase.to_sym
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
options.on('-c', '--count COUNT', 'Max number of posts (default 25)') { |count|
|
|
21
|
+
opt[:count] = count.to_i
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
options.on('-h', '--help', 'Display this screen') {
|
|
25
|
+
puts options
|
|
26
|
+
exit
|
|
27
|
+
}
|
|
28
|
+
}.parse!
|
|
29
|
+
|
|
30
|
+
raise 'No subreddit specified' if opt[:subreddit].nil?
|
|
31
|
+
raise "Subreddit name must be raw ('funny' or 'pics')" if opt[:subreddit].include?('/')
|
|
32
|
+
raise "Type must be one of {#{TYPES.join(', ')}}" unless TYPES.include?(opt[:type])
|
|
33
|
+
raise 'Count must be at least 1' if opt[:count] < 1
|
|
34
|
+
|
|
35
|
+
opt
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
require 'nokogiri'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
class Robi
|
|
5
|
+
class Compiler
|
|
6
|
+
def initialize(dest_dir, stylesheet_source)
|
|
7
|
+
@dest_dir = dest_dir
|
|
8
|
+
|
|
9
|
+
@html_file = 'index.html'
|
|
10
|
+
@stylesheet_source = stylesheet_source
|
|
11
|
+
@stylesheet_file = File.basename(@stylesheet_source)
|
|
12
|
+
@table_of_contents_file = 'tableofcontents.ncx'
|
|
13
|
+
@metadata_file = 'metadata.opf'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def compile(title, posts)
|
|
17
|
+
puts ' Building HTML...'
|
|
18
|
+
html = build_html(title, posts)
|
|
19
|
+
|
|
20
|
+
puts ' Building metadata...'
|
|
21
|
+
metadata = build_metadata(title)
|
|
22
|
+
|
|
23
|
+
puts ' Building Table of Contents...'
|
|
24
|
+
table_of_contents = build_table_of_contents(title, posts)
|
|
25
|
+
|
|
26
|
+
puts ' Assembling files...'
|
|
27
|
+
assemble(metadata, html, table_of_contents)
|
|
28
|
+
|
|
29
|
+
"#{@dest_dir}/#{@metadata_file}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_html(title, posts)
|
|
33
|
+
Nokogiri::HTML::Builder.new(encoding: 'utf-8') { |doc|
|
|
34
|
+
doc.html {
|
|
35
|
+
doc.head {
|
|
36
|
+
doc.title { doc.text title }
|
|
37
|
+
doc.link(
|
|
38
|
+
rel: 'stylesheet',
|
|
39
|
+
href: @stylesheet_file,
|
|
40
|
+
type: 'text/css'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
doc.body {
|
|
44
|
+
doc.div(id: 'titlepage') {
|
|
45
|
+
doc.h1 { doc.text title }
|
|
46
|
+
doc.h2 {
|
|
47
|
+
unit = simple_plural('post', posts.size)
|
|
48
|
+
doc.text "#{posts.size} #{unit}"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
doc.div(id: 'tableofcontents') {
|
|
52
|
+
doc.h1 { doc.text 'Posts' }
|
|
53
|
+
doc.ol {
|
|
54
|
+
posts.each do |post|
|
|
55
|
+
doc.li {
|
|
56
|
+
doc.a(href: "##{post.uid}") {
|
|
57
|
+
doc.text post.title
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
doc.div(id: 'postwrapper') {
|
|
64
|
+
posts.each do |post|
|
|
65
|
+
doc.div(class: 'post', id: post.uid) {
|
|
66
|
+
doc.h1 { doc.text post.title }
|
|
67
|
+
doc.h2 { doc.text "by #{post.author}" }
|
|
68
|
+
doc.div {
|
|
69
|
+
body = Nokogiri::HTML::DocumentFragment.parse(post.body)
|
|
70
|
+
doc << body.content
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}.to_html
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_metadata(title)
|
|
81
|
+
Nokogiri::XML::Builder.new { |xml|
|
|
82
|
+
xml.root {
|
|
83
|
+
xml.package(
|
|
84
|
+
:'unique-identifier' => title,
|
|
85
|
+
:'xmlns:opf' => 'http://www.idpf.org/2007/opf',
|
|
86
|
+
:'xmlns:asd' => 'http://www.idpf.org/asdfaf'
|
|
87
|
+
) {
|
|
88
|
+
xml.metadata {
|
|
89
|
+
xml.send(
|
|
90
|
+
:'dc-metadata',
|
|
91
|
+
:'xmlns:dc' => 'http://purl.org/metadata/dublin_core',
|
|
92
|
+
:'xmlns:oebpackage' => 'http://openebook.org/namespaces/oeb-package/1.0/'
|
|
93
|
+
) {
|
|
94
|
+
xml.send(:'dc:Title', title)
|
|
95
|
+
xml.send(:'dc:Language', 'en')
|
|
96
|
+
xml.send(:'dc:Creator', 'Reddit')
|
|
97
|
+
xml.send(:'x-metadata')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
xml.manifest {
|
|
101
|
+
# xml.item(
|
|
102
|
+
# :id => 'content',
|
|
103
|
+
# :'media-type' => 'text/x-oeb1-document',
|
|
104
|
+
# :href => "#{HTML}#tableofcontents"
|
|
105
|
+
# )
|
|
106
|
+
xml.item(
|
|
107
|
+
:id => 'ncx',
|
|
108
|
+
:'media-type' => 'application/x-dtbncx+xml',
|
|
109
|
+
:href => @table_of_contents_file
|
|
110
|
+
)
|
|
111
|
+
xml.item(
|
|
112
|
+
:id => 'text',
|
|
113
|
+
:'media-type' => 'text/x-oeb1-document',
|
|
114
|
+
:href => "#{@html_file}"
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
xml.spine(toc: 'ncx') {
|
|
118
|
+
# xml.itemref(idref: 'content')
|
|
119
|
+
xml.itemref(idref: 'text')
|
|
120
|
+
}
|
|
121
|
+
xml.guide {
|
|
122
|
+
xml.reference(type: 'toc', title: 'Table of Contents', href: @table_of_contents_file)
|
|
123
|
+
xml.reference(type: 'text', title: title, href: @html_file)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}.to_xml
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_table_of_contents(title, posts)
|
|
131
|
+
Nokogiri::XML::Builder.new { |xml|
|
|
132
|
+
xml.doc.create_internal_subset(
|
|
133
|
+
'ncx',
|
|
134
|
+
'-//NISO//DTD ncx 2005-1//EN',
|
|
135
|
+
'http://www.daisy.org/z3986/2005/ncx-2005-1.dtd'
|
|
136
|
+
)
|
|
137
|
+
xml.ncx(xmlns: 'http://www.daisy.org/z3986/2005/ncx/', version: '2005-1') {
|
|
138
|
+
xml.docTitle {
|
|
139
|
+
xml.text_ title
|
|
140
|
+
}
|
|
141
|
+
xml.navMap {
|
|
142
|
+
# xml.navPoint(id: 'toc', playOrder: 1) {
|
|
143
|
+
# xml.navLabel {
|
|
144
|
+
# xml.text_ 'Table of Contents'
|
|
145
|
+
# }
|
|
146
|
+
# xml.content(src: @html_file)
|
|
147
|
+
# }
|
|
148
|
+
posts.zip(1..Float::INFINITY).each do |post, index|
|
|
149
|
+
xml.navPoint(id: post.uid, playOrder: index) {
|
|
150
|
+
xml.navLabel {
|
|
151
|
+
xml.text_ post.title
|
|
152
|
+
}
|
|
153
|
+
xml.content(src: "#{@html_file}##{post.uid}")
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}.to_xml
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def assemble(metadata, html, table_of_contents)
|
|
162
|
+
Dir.mkdir(@dest_dir) unless Dir.exist?(@dest_dir)
|
|
163
|
+
|
|
164
|
+
File.open("#{@dest_dir}/#{@html_file}", 'w') { |file| file.write(html) }
|
|
165
|
+
File.open("#{@dest_dir}/#{@metadata_file}", 'w') { |file| file.write(metadata) }
|
|
166
|
+
File.open("#{@dest_dir}/#{@table_of_contents_file}", 'w') { |file| file.write(table_of_contents) }
|
|
167
|
+
|
|
168
|
+
FileUtils.cp(@stylesheet_source, "#{@dest_dir}/#{@stylesheet_file}")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def simple_plural(word, count)
|
|
172
|
+
if count.abs > 1
|
|
173
|
+
"#{word}s"
|
|
174
|
+
else
|
|
175
|
+
word
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
data/lib/robi/fetcher.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'pp'
|
|
4
|
+
|
|
5
|
+
require 'robi/post'
|
|
6
|
+
|
|
7
|
+
class Robi
|
|
8
|
+
class Fetcher
|
|
9
|
+
HTTP_SUCCESS = %w(200).map(&:freeze).freeze
|
|
10
|
+
HTTP_REDIRECT = %w(301 302).map(&:freeze).freeze
|
|
11
|
+
|
|
12
|
+
def initialize(subreddit, type)
|
|
13
|
+
@uri = URI("https://reddit.com/r/#{subreddit}/#{type}/.json")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fetch(count)
|
|
17
|
+
puts " Connecting to #{@uri}..."
|
|
18
|
+
response = Net::HTTP.get_response(@uri)
|
|
19
|
+
|
|
20
|
+
while HTTP_REDIRECT.include?(response.code)
|
|
21
|
+
@uri = URI(response.header['location'])
|
|
22
|
+
puts " Redirecting to #{@uri}..."
|
|
23
|
+
response = Net::HTTP.get_response(@uri)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
unless HTTP_SUCCESS.include?(response.code)
|
|
27
|
+
raise "Received response code #{response.code}: #{response.msg}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
json = response.body
|
|
31
|
+
|
|
32
|
+
puts ' Extracting content...'
|
|
33
|
+
obj = JSON.parse(json)
|
|
34
|
+
extract(count, obj)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def extract(count, obj)
|
|
38
|
+
obj['data']['children']
|
|
39
|
+
.map { |post| post['data'] }
|
|
40
|
+
.reject { |post| post['stickied'] }
|
|
41
|
+
.reject { |post| post['selftext'].empty? }
|
|
42
|
+
.first(count)
|
|
43
|
+
.map { |post| Post.from_json_hash(post) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/robi/post.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
class Robi
|
|
3
|
+
class Post
|
|
4
|
+
attr_reader(
|
|
5
|
+
*%i(
|
|
6
|
+
uid
|
|
7
|
+
title
|
|
8
|
+
author
|
|
9
|
+
date body
|
|
10
|
+
comment_count
|
|
11
|
+
points
|
|
12
|
+
)
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def initialize(
|
|
16
|
+
uid,
|
|
17
|
+
title,
|
|
18
|
+
author,
|
|
19
|
+
timestamp,
|
|
20
|
+
body,
|
|
21
|
+
comment_count,
|
|
22
|
+
points
|
|
23
|
+
)
|
|
24
|
+
@uid = uid.to_s
|
|
25
|
+
@title = title.to_s
|
|
26
|
+
@author = author.to_s
|
|
27
|
+
@date = Time.at(timestamp.to_i)
|
|
28
|
+
@body = body.to_s
|
|
29
|
+
@comment_count = comment_count.to_i
|
|
30
|
+
@points = points
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.from_json_hash(hash)
|
|
34
|
+
new(
|
|
35
|
+
*hash.values_at(
|
|
36
|
+
*%w(
|
|
37
|
+
name
|
|
38
|
+
title
|
|
39
|
+
author
|
|
40
|
+
created
|
|
41
|
+
selftext_html
|
|
42
|
+
num_comments
|
|
43
|
+
score
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/robi.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
|
|
3
|
+
require 'robi/fetcher'
|
|
4
|
+
require 'robi/compiler'
|
|
5
|
+
|
|
6
|
+
class Robi
|
|
7
|
+
def initialize(subreddit, type)
|
|
8
|
+
@subreddit = subreddit
|
|
9
|
+
@type = type
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def bundle(count)
|
|
13
|
+
id = [@subreddit, @type, Time.now.strftime('%Y-%m-%d-%H%M')]
|
|
14
|
+
|
|
15
|
+
title_string = id.join(' ')
|
|
16
|
+
title_slug = id.join('_')
|
|
17
|
+
dest_dir = "./#{title_slug}"
|
|
18
|
+
|
|
19
|
+
if Dir.exist?(dest_dir) && (Dir.entries(dest_dir) - %w(. ..)).any?
|
|
20
|
+
raise "#{dest_dir} not empty"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
puts 'Locating kindlegen'
|
|
24
|
+
kindlegen_found = system('which kindlegen')
|
|
25
|
+
abort unless kindlegen_found
|
|
26
|
+
|
|
27
|
+
puts "\nFetching posts"
|
|
28
|
+
posts = Fetcher.new(@subreddit, @type).fetch(count)
|
|
29
|
+
|
|
30
|
+
puts "\nCompiling to eBook source"
|
|
31
|
+
stylesheet = File.expand_path('stylesheet.css', "#{File.dirname(__FILE__)}/static")
|
|
32
|
+
metadata_file = Compiler.new(dest_dir, stylesheet)
|
|
33
|
+
.compile(title_string, posts)
|
|
34
|
+
|
|
35
|
+
puts "\nInvoking kindlegen"
|
|
36
|
+
outfile = "#{File.dirname(metadata_file)}/output.mobi"
|
|
37
|
+
system("kindlegen #{metadata_file} -o output.mobi")
|
|
38
|
+
raise 'kindlegen failed to output eBook' unless File.exist?(outfile)
|
|
39
|
+
FileUtils.mv(outfile, "./#{title_slug}.mobi")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
h1 { text-align: center; }
|
|
2
|
+
h2 { text-align: center; }
|
|
3
|
+
h3 { text-align: center; }
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
#titlepage {
|
|
7
|
+
page-break-after: always;
|
|
8
|
+
padding-top: 40%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
#tableofcontents {
|
|
13
|
+
page-break-after: always;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#tableofcontents ol {
|
|
17
|
+
margin: 0 10%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
.post {
|
|
22
|
+
page-break-after: always;
|
|
23
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: robi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrew Tolvstad
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2016-12-02 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.6'
|
|
20
|
+
- - ">="
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 1.6.8
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - "~>"
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '1.6'
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.6.8
|
|
33
|
+
description: Compile Reddit to Kindle eBooks
|
|
34
|
+
email: tolvstaa@oregonstate.edu
|
|
35
|
+
executables:
|
|
36
|
+
- robi
|
|
37
|
+
extensions: []
|
|
38
|
+
extra_rdoc_files: []
|
|
39
|
+
files:
|
|
40
|
+
- bin/robi
|
|
41
|
+
- lib/optparser.rb
|
|
42
|
+
- lib/robi.rb
|
|
43
|
+
- lib/robi/compiler.rb
|
|
44
|
+
- lib/robi/fetcher.rb
|
|
45
|
+
- lib/robi/post.rb
|
|
46
|
+
- lib/static/stylesheet.css
|
|
47
|
+
homepage: https://github.com/Inityx/robi
|
|
48
|
+
licenses:
|
|
49
|
+
- Apache-2.0
|
|
50
|
+
metadata: {}
|
|
51
|
+
post_install_message:
|
|
52
|
+
rdoc_options: []
|
|
53
|
+
require_paths:
|
|
54
|
+
- lib
|
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '0'
|
|
65
|
+
requirements: []
|
|
66
|
+
rubyforge_project:
|
|
67
|
+
rubygems_version: 2.5.2
|
|
68
|
+
signing_key:
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: Robi
|
|
71
|
+
test_files: []
|