klipbook 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +6 -5
- data/Gemfile.lock +62 -25
- data/Guardfile +1 -2
- data/README.md +66 -18
- data/Rakefile +5 -1
- data/bin/klipbook +85 -1
- data/example.png +0 -0
- data/features/collate.feature +51 -0
- data/features/fixtures/clippings-for-three-books.txt +105 -0
- data/features/list.feature +31 -0
- data/features/step_definitions/collate_steps.rb +61 -0
- data/features/step_definitions/list_steps.rb +15 -0
- data/features/support/env.rb +5 -1
- data/klipbook.gemspec +49 -32
- data/lib/klipbook/book.rb +18 -0
- data/lib/klipbook/clipping.rb +4 -10
- data/lib/klipbook/collator.rb +17 -0
- data/lib/klipbook/config.rb +22 -0
- data/lib/klipbook/fetcher.rb +29 -0
- data/lib/klipbook/invalid_source_error.rb +12 -0
- data/lib/klipbook/output/book_helpers.rb +12 -0
- data/lib/klipbook/{book_summary.erb → output/html_book_summary.erb} +65 -11
- data/lib/klipbook/output/html_summary_writer.rb +42 -0
- data/lib/klipbook/printer.rb +18 -0
- data/lib/klipbook/sources/amazon_site/book_scraper.rb +67 -0
- data/lib/klipbook/sources/amazon_site/scraper.rb +78 -0
- data/lib/klipbook/sources/kindle_device/entry.rb +11 -0
- data/lib/klipbook/sources/kindle_device/entry_parser.rb +85 -0
- data/lib/klipbook/sources/kindle_device/file.rb +57 -0
- data/lib/klipbook/sources/kindle_device/file_parser.rb +33 -0
- data/lib/klipbook/version.rb +1 -1
- data/lib/klipbook.rb +18 -5
- data/spec/lib/klipbook/book_spec.rb +33 -0
- data/spec/lib/klipbook/collator_spec.rb +40 -0
- data/spec/lib/klipbook/fetcher_spec.rb +81 -0
- data/spec/lib/klipbook/output/html_summary_writer_spec.rb +90 -0
- data/spec/lib/klipbook/printer_spec.rb +45 -0
- data/spec/lib/klipbook/sources/kindle_device/entry_parser_spec.rb +275 -0
- data/spec/lib/klipbook/sources/kindle_device/file_parser_spec.rb +68 -0
- data/spec/lib/klipbook/sources/kindle_device/file_spec.rb +163 -0
- metadata +158 -58
- data/features/list_books.feature +0 -23
- data/features/print_book_summary.feature +0 -10
- data/features/step_definitions/klipbook_steps.rb +0 -87
- data/lib/klipbook/book_summary.rb +0 -35
- data/lib/klipbook/cli.rb +0 -49
- data/lib/klipbook/clippings_file.rb +0 -50
- data/lib/klipbook/clippings_parser.rb +0 -98
- data/lib/klipbook/runner.rb +0 -29
- data/spec/lib/klipbook/book_summary_spec.rb +0 -30
- data/spec/lib/klipbook/clipping_spec.rb +0 -17
- data/spec/lib/klipbook/clippings_file_spec.rb +0 -60
- data/spec/lib/klipbook/clippings_parser_spec.rb +0 -367
- data/spec/lib/klipbook/runner_spec.rb +0 -87
@@ -0,0 +1,85 @@
|
|
1
|
+
module Klipbook::Sources
|
2
|
+
module KindleDevice
|
3
|
+
class EntryParser
|
4
|
+
|
5
|
+
def build_entry(entry_text)
|
6
|
+
return nil if invalid_entry?(entry_text)
|
7
|
+
|
8
|
+
lines = split_text_into_lines(entry_text)
|
9
|
+
title_line = lines[0].strip
|
10
|
+
metadata = lines[1].strip
|
11
|
+
text_lines = lines[3..-1]
|
12
|
+
|
13
|
+
type = extract_type(metadata)
|
14
|
+
|
15
|
+
Klipbook::Sources::KindleDevice::Entry.new do |h|
|
16
|
+
h.title = extract_title(title_line)
|
17
|
+
h.author = extract_author(title_line)
|
18
|
+
h.location = extract_location(metadata)
|
19
|
+
h.page = extract_page(metadata)
|
20
|
+
h.added_on = extract_added_date(metadata)
|
21
|
+
h.text = extract_content(text_lines)
|
22
|
+
h.type = extract_type(metadata)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def invalid_entry?(entry_text)
|
29
|
+
entry_text.blank? || incomplete_entry?(entry_text)
|
30
|
+
end
|
31
|
+
|
32
|
+
def incomplete_entry?(entry_text)
|
33
|
+
split_text_into_lines(entry_text).length < 2
|
34
|
+
end
|
35
|
+
|
36
|
+
def split_text_into_lines(entry_text)
|
37
|
+
entry_text.lstrip.lines.to_a
|
38
|
+
end
|
39
|
+
|
40
|
+
def extract_title(title_line)
|
41
|
+
if title_line =~ /\(.+\)\Z/
|
42
|
+
title_line.scan(/(.*)\s+\(.+\)\Z/).first.first
|
43
|
+
else
|
44
|
+
title_line
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def extract_author(title_line)
|
49
|
+
match = title_line.scan /\(([^\(]+)\)\Z/
|
50
|
+
match.empty? ? nil : match.first.first
|
51
|
+
end
|
52
|
+
|
53
|
+
def extract_type(metadata)
|
54
|
+
type = metadata.scan(/^-( Your)? (\w+)/).first[1]
|
55
|
+
type.downcase.to_sym
|
56
|
+
end
|
57
|
+
|
58
|
+
def extract_location(metadata)
|
59
|
+
match = metadata.scan(/Loc(ation|\.) ([0-9]+-?)/)
|
60
|
+
|
61
|
+
return 0 if match.empty?
|
62
|
+
|
63
|
+
location = match.first[1]
|
64
|
+
location.to_i
|
65
|
+
end
|
66
|
+
|
67
|
+
def extract_page(metadata)
|
68
|
+
match = metadata.scan(/Page (\d+)/)
|
69
|
+
|
70
|
+
return nil if match.empty?
|
71
|
+
|
72
|
+
location = match.first.first
|
73
|
+
location.to_i
|
74
|
+
end
|
75
|
+
|
76
|
+
def extract_content(text_lines)
|
77
|
+
text_lines.join('').rstrip
|
78
|
+
end
|
79
|
+
|
80
|
+
def extract_added_date(metadata)
|
81
|
+
DateTime.parse(metadata.scan(/Added on (.+)$/i).first.first)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Klipbook::Sources
|
2
|
+
module KindleDevice
|
3
|
+
class File
|
4
|
+
def initialize(file_text, max_books, file_parser=FileParser.new)
|
5
|
+
@file_text = file_text
|
6
|
+
@file_parser = file_parser
|
7
|
+
@max_books = max_books
|
8
|
+
end
|
9
|
+
|
10
|
+
def books
|
11
|
+
@books ||= build_books.take(@max_books)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_books
|
17
|
+
sorted_entries = extract_sorted_entries_from_file_text
|
18
|
+
build_sorted_book_list(sorted_entries)
|
19
|
+
end
|
20
|
+
|
21
|
+
def extract_sorted_entries_from_file_text
|
22
|
+
entries = @file_parser.extract_entries(@file_text)
|
23
|
+
entries.sort { |entry_a, entry_b| entry_a.title <=> entry_b.title }
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_sorted_book_list(sorted_entries)
|
27
|
+
books_from_entries(sorted_entries).sort do |book_a, book_b|
|
28
|
+
-(book_a.last_update <=> book_b.last_update)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def books_from_entries(entries)
|
33
|
+
entries.select { |entry| entry.type != :bookmark }
|
34
|
+
.group_by(&:title)
|
35
|
+
.map { |title, book_entries| book_from_entries(book_entries) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def book_from_entries(entries)
|
39
|
+
entries.sort! { |ea, eb| ea.location <=> eb.location }
|
40
|
+
|
41
|
+
Klipbook::Book.new do |b|
|
42
|
+
b.title = entries.first.title
|
43
|
+
b.author = entries.first.author
|
44
|
+
b.last_update = entries.map(&:added_on).max
|
45
|
+
b.clippings = entries.map do |e|
|
46
|
+
Klipbook::Clipping.new do |c|
|
47
|
+
c.location = e.location
|
48
|
+
c.page = e.page
|
49
|
+
c.text = e.text
|
50
|
+
c.type = e.type
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Klipbook::Sources
|
4
|
+
module KindleDevice
|
5
|
+
class FileParser
|
6
|
+
def initialize(entry_parser=EntryParser.new)
|
7
|
+
@entry_parser = entry_parser
|
8
|
+
end
|
9
|
+
|
10
|
+
def extract_entries(file_text)
|
11
|
+
entries_text = split_into_raw_entries_text(file_text)
|
12
|
+
|
13
|
+
build_entries(entries_text)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def build_entries(entries_text)
|
19
|
+
entries_text.map do |entry_text|
|
20
|
+
@entry_parser.build_entry(entry_text)
|
21
|
+
end.compact
|
22
|
+
end
|
23
|
+
|
24
|
+
def strip_control_characters(file_text)
|
25
|
+
file_text.gsub("\r", '').gsub("\xef\xbb\xbf", '')
|
26
|
+
end
|
27
|
+
|
28
|
+
def split_into_raw_entries_text(file_text)
|
29
|
+
strip_control_characters(file_text).split('==========')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/klipbook/version.rb
CHANGED
data/lib/klipbook.rb
CHANGED
@@ -1,10 +1,23 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'klipbook/blank'
|
3
3
|
require 'klipbook/version'
|
4
|
-
|
4
|
+
|
5
|
+
require 'klipbook/sources/kindle_device/file_parser'
|
6
|
+
require 'klipbook/sources/kindle_device/entry_parser'
|
7
|
+
require 'klipbook/sources/kindle_device/entry'
|
8
|
+
require 'klipbook/sources/kindle_device/file'
|
9
|
+
|
10
|
+
require 'klipbook/sources/amazon_site/scraper'
|
11
|
+
require 'klipbook/sources/amazon_site/book_scraper'
|
12
|
+
|
13
|
+
require 'klipbook/invalid_source_error'
|
14
|
+
require 'klipbook/config'
|
15
|
+
require 'klipbook/book'
|
5
16
|
require 'klipbook/clipping'
|
6
|
-
require 'klipbook/clippings_parser'
|
7
|
-
require 'klipbook/book_summary'
|
8
|
-
require 'klipbook/runner'
|
9
|
-
require 'klipbook/cli'
|
10
17
|
|
18
|
+
require 'klipbook/fetcher'
|
19
|
+
require 'klipbook/collator'
|
20
|
+
require 'klipbook/printer'
|
21
|
+
|
22
|
+
require 'klipbook/output/html_summary_writer'
|
23
|
+
require 'klipbook/output/book_helpers'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Klipbook::Book do
|
4
|
+
describe '#title_and_author' do
|
5
|
+
|
6
|
+
subject { book.title_and_author }
|
7
|
+
|
8
|
+
context 'with no author' do
|
9
|
+
let(:book) do
|
10
|
+
Klipbook::Book.new do |b|
|
11
|
+
b.title = 'Book title'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'only contains the title' do
|
16
|
+
subject.should == 'Book title'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'with an author' do
|
21
|
+
let(:book) do
|
22
|
+
Klipbook::Book.new do |b|
|
23
|
+
b.title = 'Book title'
|
24
|
+
b.author = 'Rob Ripjaw'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'contains the title and author' do
|
29
|
+
subject.should == 'Book title by Rob Ripjaw'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Klipbook::Collator do
|
4
|
+
|
5
|
+
let (:it) { Klipbook::Collator.new(books, summary_writer) }
|
6
|
+
|
7
|
+
let (:summary_writer) do
|
8
|
+
Object.new.tap do |fake_writer|
|
9
|
+
stub(fake_writer).write
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let (:message_stream) do
|
14
|
+
Object.new.tap do |fake_stream|
|
15
|
+
stub(fake_stream).puts
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
let (:book_one) { Klipbook::Book.new }
|
20
|
+
let (:book_two) { Klipbook::Book.new }
|
21
|
+
let (:books) { [ book_one, book_two ] }
|
22
|
+
|
23
|
+
let(:output_dir) { 'fake output dir' }
|
24
|
+
|
25
|
+
describe '#collate_books' do
|
26
|
+
|
27
|
+
subject { it.collate_books(output_dir, true, message_stream) }
|
28
|
+
|
29
|
+
it 'prints a message displaying the output directory' do
|
30
|
+
subject
|
31
|
+
message_stream.should have_received.puts('Using output directory: fake output dir')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'passes each book to the summary writer' do
|
35
|
+
subject
|
36
|
+
summary_writer.should have_received.write(book_one, output_dir, true)
|
37
|
+
summary_writer.should have_received.write(book_two, output_dir, true)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Klipbook::Fetcher do
|
4
|
+
|
5
|
+
let(:fake_source) do
|
6
|
+
Object.new.tap do |fakey|
|
7
|
+
stub(fakey).books { books }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'when created with an invalid source' do
|
12
|
+
it 'raises an error' do
|
13
|
+
expect { Klipbook::Fetcher.new('sdf' , 2) }.to raise_error(InvalidSourceError)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'when created with a site source' do
|
18
|
+
subject { fetcher }
|
19
|
+
|
20
|
+
let(:fetcher) { Klipbook::Fetcher.new('site:username@example.com:password', 2) }
|
21
|
+
|
22
|
+
before(:each) do
|
23
|
+
stub(Klipbook::Sources::AmazonSite::Scraper).new { fake_source }
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'creates a site scraper with the provided credentials' do
|
27
|
+
subject
|
28
|
+
Klipbook::Sources::AmazonSite::Scraper.should have_received.new('username@example.com', 'password', 2)
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#fetch_books' do
|
32
|
+
subject { fetcher.fetch_books }
|
33
|
+
|
34
|
+
let(:books) { [] }
|
35
|
+
|
36
|
+
it 'returns the books returned by the site source' do
|
37
|
+
subject.should == books
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'when created with a file source' do
|
43
|
+
subject { fetcher }
|
44
|
+
|
45
|
+
let(:fetcher) { Klipbook::Fetcher.new('file:filename', 2) }
|
46
|
+
|
47
|
+
let(:fake_file) do
|
48
|
+
Object.new.tap do |file|
|
49
|
+
stub(file).read { file_contents }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
let(:file_contents) { 'fake contents ' }
|
54
|
+
|
55
|
+
before(:each) do
|
56
|
+
stub(Klipbook::Sources::KindleDevice::File).new { fake_source }
|
57
|
+
stub(File).open { fake_file }
|
58
|
+
end
|
59
|
+
|
60
|
+
it "reads the file's contents from disk" do
|
61
|
+
subject
|
62
|
+
File.should have_received.open('filename', 'r')
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'uses the file contents to create a file source' do
|
66
|
+
subject
|
67
|
+
|
68
|
+
Klipbook::Sources::KindleDevice::File.should have_received.new('fake contents', 2)
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '#fetch_books' do
|
72
|
+
subject { fetcher.fetch_books }
|
73
|
+
|
74
|
+
let(:books) { [] }
|
75
|
+
|
76
|
+
it 'returns the books returned by the file source' do
|
77
|
+
subject.should == books
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# This is more of an integration test but what the heck
|
4
|
+
# it can live in here for now
|
5
|
+
|
6
|
+
describe Klipbook::Output::HtmlSummaryWriter do
|
7
|
+
|
8
|
+
before(:all) do
|
9
|
+
@output_dir = Dir.mktmpdir
|
10
|
+
end
|
11
|
+
|
12
|
+
after(:all) do
|
13
|
+
FileUtils.rm_f(@output_dir)
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:book) do
|
17
|
+
Klipbook::Book.new do |b|
|
18
|
+
b.title = 'Fake book title'
|
19
|
+
b.author = 'Fake Author'
|
20
|
+
b.clippings = []
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:message_stream) do
|
25
|
+
Object.new.tap do |fake_stream|
|
26
|
+
stub(fake_stream).puts
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#write' do
|
31
|
+
|
32
|
+
subject { Klipbook::Output::HtmlSummaryWriter.new(message_stream).write(book, @output_dir, force) }
|
33
|
+
|
34
|
+
let(:force) { false }
|
35
|
+
|
36
|
+
let(:expected_filename) { "Fake book title by Fake Author.html" }
|
37
|
+
let(:expected_filepath) { "#{@output_dir}/Fake book title by Fake Author.html" }
|
38
|
+
|
39
|
+
context 'with no existing summary file' do
|
40
|
+
before(:each) do
|
41
|
+
FileUtils.rm_f(File.join(@output_dir, '*'))
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'writes a file named after the book into the output directory' do
|
45
|
+
subject
|
46
|
+
File.exists?(expected_filepath).should be_true
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'writes a html summary to the file' do
|
50
|
+
subject
|
51
|
+
File.read(expected_filepath).should include("<h1>Fake book title</h1>")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'with an existing summary file' do
|
56
|
+
before(:each) do
|
57
|
+
FileUtils.rm_f(expected_filepath)
|
58
|
+
FileUtils.touch(expected_filepath)
|
59
|
+
end
|
60
|
+
|
61
|
+
context "and 'force' set to false" do
|
62
|
+
let(:force) { false }
|
63
|
+
|
64
|
+
it "won't write to the file" do
|
65
|
+
subject
|
66
|
+
File.size(expected_filepath).should == 0
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'prints a message informing that the file is being skipped' do
|
70
|
+
subject
|
71
|
+
message_stream.should have_received.puts("\e[33mSkipping \e[0m#{expected_filename}")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "and 'force' set to true" do
|
76
|
+
let(:force) { true }
|
77
|
+
|
78
|
+
it 'overwrites the file' do
|
79
|
+
subject
|
80
|
+
File.size(expected_filepath).should > 0
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'prints a message informing that the file is being written' do
|
84
|
+
subject
|
85
|
+
message_stream.should have_received.puts("\e[32mWriting \e[0m#{expected_filename}")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Klipbook::Printer do
|
4
|
+
|
5
|
+
let (:output) do
|
6
|
+
Object.new.tap do |fake_output|
|
7
|
+
stub(fake_output).puts
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#print' do
|
12
|
+
|
13
|
+
subject { Klipbook::Printer.new(books).print(output) }
|
14
|
+
|
15
|
+
context 'when created with no books' do
|
16
|
+
|
17
|
+
let (:books) { [] }
|
18
|
+
|
19
|
+
it 'displays a message saying that the clipping file contains no books' do
|
20
|
+
subject
|
21
|
+
output.should have_received.puts('No books available')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when created with multiple books' do
|
26
|
+
let (:books) do
|
27
|
+
[
|
28
|
+
Klipbook::Book.new { |b| b.title = 'My first fake title' },
|
29
|
+
Klipbook::Book.new { |b| b.title = 'Another fake book'; b.author = 'Rock Riphard' }
|
30
|
+
]
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'displays a message describing the book list' do
|
34
|
+
subject
|
35
|
+
output.should have_received.puts('Book list:')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'displays an indexed list of book titles including the author when available' do
|
39
|
+
subject
|
40
|
+
output.should have_received.puts('[1] My first fake title')
|
41
|
+
output.should have_received.puts('[2] Another fake book by Rock Riphard')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|