dayone-kindle 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e91e2417dc8198ecae3088a1fe6b8730aa93fc5e
4
+ data.tar.gz: f00a1bc2e8ca3f915239c29264ff94df2313269a
5
+ SHA512:
6
+ metadata.gz: 5a0387c105b5d61d2bbf118fbb14ad5f1a5af970abe09c87e9d3c1cf07c6662b6d199625003f7bef4543964fc3b7822d79ed9c43a61d9808bedd3688e3aa05e1
7
+ data.tar.gz: aa0e67f393e5e31a32d7111dad575ff3ee4e577c7ba5a9c399842c8c9d4f4c89c1219e11a980db436f8f8a489aca01a5a12369f0d8998305b3b2659e8f470fd3
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1.1
5
+ - 2.2.2
6
+ - ruby-head
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'rake'
7
+ gem 'minitest-reporters'
8
+ end
@@ -0,0 +1,23 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ ansi (1.5.0)
5
+ builder (3.2.2)
6
+ minitest (5.6.1)
7
+ minitest-reporters (1.0.16)
8
+ ansi
9
+ builder
10
+ minitest (>= 5.0)
11
+ ruby-progressbar
12
+ rake (10.4.2)
13
+ ruby-progressbar (1.7.5)
14
+
15
+ PLATFORMS
16
+ ruby
17
+
18
+ DEPENDENCIES
19
+ minitest-reporters
20
+ rake
21
+
22
+ BUNDLED WITH
23
+ 1.10.4
@@ -0,0 +1,5 @@
1
+ # DayOne Kindle highlights importer [![Build Status](https://travis-ci.org/TimPetricola/dayone-kindle.svg)](https://travis-ci.org/TimPetricola/dayone-kindle)
2
+
3
+ ```
4
+ dayone-kindle --help
5
+ ```
@@ -0,0 +1,10 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new(:test) do |t|
4
+ t.libs.push 'test'
5
+ t.pattern = 'test/**/*_test.rb'
6
+ t.warning = false
7
+ t.verbose = true
8
+ end
9
+
10
+ task default: [:test]
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if RUBY_VERSION.to_f < 2.0
4
+ $stderr.puts 'dayone-kindle require Ruby 2.0+.'
5
+ exit 1
6
+ end
7
+
8
+ Encoding.default_external = Encoding::UTF_8
9
+ Encoding.default_internal = Encoding::UTF_8
10
+
11
+ require 'optparse'
12
+
13
+ # resolve bin path, ignoring symlinks
14
+ require 'pathname'
15
+ bin_file = Pathname.new(__FILE__).realpath
16
+
17
+ # add self to libpath
18
+ $:.unshift File.expand_path('../../lib', bin_file)
19
+
20
+ require 'dayone-kindle'
21
+
22
+ DayOneKindle::CLI.run
@@ -0,0 +1,48 @@
1
+ require 'time'
2
+ require 'fileutils'
3
+
4
+ # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb
5
+ class Object
6
+ def try(*a, &b)
7
+ try!(*a, &b) if a.empty? || respond_to?(a.first)
8
+ end
9
+
10
+ def try!(*a, &b)
11
+ if a.empty? && block_given?
12
+ if b.arity == 0
13
+ instance_eval(&b)
14
+ else
15
+ yield self
16
+ end
17
+ else
18
+ public_send(*a, &b)
19
+ end
20
+ end
21
+ end
22
+
23
+ # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/array/conversions.rb
24
+ class Array
25
+ def to_sentence(words_connector: ', ', two_words_connector: ' and ', last_word_connector: ', and ')
26
+ case length
27
+ when 0
28
+ ''
29
+ when 1
30
+ "#{self[0]}"
31
+ when 2
32
+ "#{self[0]}#{two_words_connector}#{self[1]}"
33
+ else
34
+ "#{self[0...-1].join(words_connector)}#{last_word_connector}#{self[-1]}"
35
+ end
36
+ end
37
+ end
38
+
39
+ LIB = File.dirname(File.expand_path(__FILE__))
40
+
41
+ require LIB + '/dayone-kindle/day_one'
42
+ require LIB + '/dayone-kindle/highlight'
43
+ require LIB + '/dayone-kindle/data_store'
44
+ require LIB + '/dayone-kindle/clipping_parser'
45
+ require LIB + '/dayone-kindle/clippings_parser'
46
+ require LIB + '/dayone-kindle/device'
47
+ require LIB + '/dayone-kindle/cli'
48
+
@@ -0,0 +1,81 @@
1
+ module DayOneKindle
2
+ module CLI
3
+ def self.dialog(value)
4
+ script = <<-END
5
+ tell app "System Events"
6
+ display dialog "#{value}"
7
+ end tell
8
+ END
9
+
10
+ system('osascript ' + script.split(/\n/).map { |line| "-e '#{line}'" }.join(' ') + '> /dev/null 2>&1')
11
+ end
12
+
13
+ def self.options
14
+ return @options if @options
15
+
16
+ options = {
17
+ ask_confirmation: true,
18
+ tags: [],
19
+ dry_run: false,
20
+ archive: true
21
+ }
22
+
23
+ optparse = OptionParser.new do |opts|
24
+ opts.banner = 'Usage: dayone-kindle [options]'
25
+
26
+ opts.on '--dry', 'Outputs highlights instead of importing them (use for testing)' do
27
+ options[:dry_run] = true
28
+ end
29
+
30
+ opts.on '-t', '--tags reading,quote', Array, 'Tags to be saved with highlights' do |t|
31
+ options[:tags] = t
32
+ end
33
+
34
+ opts.on '--auto-confirm', 'Do not ask for confirmation before import' do
35
+ options[:ask_confirmation] = false
36
+ end
37
+
38
+ opts.on '--no-archive', 'Do not archive imported highlights on device' do
39
+ options[:archive] = false
40
+ end
41
+ end
42
+
43
+ optparse.parse!
44
+
45
+ @options = options
46
+ end
47
+
48
+ def self.run
49
+ tags = options[:tags]
50
+
51
+ DayOneKindle::Device.find_at('/Volumes').each do |kindle|
52
+ next if kindle.highlights.empty?
53
+
54
+ if options[:ask_confirmation]
55
+ label = "#{kindle.name} detected. Highlights will be imported to Day One."
56
+ next unless dialog(label)
57
+ end
58
+
59
+ store = DayOneKindle::DataStore.new(kindle.highlights, tags)
60
+ puts "#{store.entries.count} highlights to import"
61
+
62
+ puts "Tags: #{tags.empty? ? 'no tags' : tags.join(', ')}"
63
+
64
+ if options[:dry_run]
65
+ puts 'Dry run, no highlight imported'
66
+ else
67
+ entries = store.save!
68
+ puts "#{entries.count} highlights imported with tags"
69
+
70
+ if options[:archive]
71
+ path = kindle.archive_highlights!
72
+ puts "Highlights archived at #{path}"
73
+ end
74
+
75
+ kindle.clear_highlights!
76
+ puts 'Highlights cleared from device'
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,81 @@
1
+ module DayOneKindle
2
+ class ClippingParser
3
+ attr_reader :raw
4
+
5
+ REGEX = %r{^
6
+ (?<book>.+)\s\((?<authors>.+)\)\n
7
+ \-\s(?<metadata>.+)\n\n
8
+ (?<quote>.+)?
9
+ $}xi
10
+
11
+ def initialize(raw)
12
+ @raw = raw
13
+ end
14
+
15
+ def highlight
16
+ return unless metadata.match(/Highlight/i)
17
+ return unless quote
18
+
19
+ Highlight.new(
20
+ book: book,
21
+ highlight: quote,
22
+ time: time,
23
+ page: page,
24
+ location: location,
25
+ authors: authors
26
+ )
27
+ end
28
+
29
+ private
30
+
31
+ def book
32
+ @book ||= begin
33
+ b = matches.try(:[], :book)
34
+ b && !b.empty? && b.strip
35
+ end
36
+ end
37
+
38
+ def quote
39
+ @quote ||= begin
40
+ q = matches.try(:[], :quote)
41
+ q && !q.empty? && q && q.gsub(/ \u00a0/, "\n\n").strip
42
+ end
43
+ end
44
+
45
+ def time
46
+ @time ||= begin
47
+ string = metadata.match(/added on (?<time>.+)/i).try(:[], :time)
48
+ string && !string.empty? && Time.parse(string.strip)
49
+ end
50
+ end
51
+
52
+ def page
53
+ @page ||= begin
54
+ page = metadata.match(/page (?<page>\d+)/i).try(:[], :page)
55
+ page && !page.empty? && page.to_i
56
+ end
57
+ end
58
+
59
+ def location
60
+ @location ||= begin
61
+ location = metadata.match(/location (?<location>[\d-]+)/i).try(:[], :location)
62
+ location && !location.empty? && location
63
+ end
64
+ end
65
+
66
+ def authors
67
+ @authors ||= begin
68
+ string = matches.try(:[], :authors)
69
+ string && !string.empty? && string.split(';')
70
+ end
71
+ end
72
+
73
+ def metadata
74
+ @metadata ||= matches[:metadata]
75
+ end
76
+
77
+ def matches
78
+ @matches ||= raw.match(REGEX)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,15 @@
1
+ module DayOneKindle
2
+ class ClippingsParser
3
+ attr_reader :raw
4
+
5
+ def initialize(raw)
6
+ @raw = raw
7
+ end
8
+
9
+ def highlights
10
+ raw.split("\n==========\n").map do |r|
11
+ ClippingParser.new(r).highlight
12
+ end.compact
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ module DayOneKindle
2
+ class DataStore
3
+ attr_reader :entries, :tags
4
+
5
+ def initialize(entries, tags = [])
6
+ @entries = entries
7
+ @tags = tags
8
+ end
9
+
10
+ def save!
11
+ entries.map { |entry| save_entry!(entry) }
12
+ end
13
+
14
+ private
15
+
16
+ def save_entry!(entry)
17
+ DayOne::Entry.new(
18
+ entry.to_markdown,
19
+ created_at: entry.time,
20
+ tags: tags
21
+ ).save!
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,67 @@
1
+ module DayOneKindle
2
+ module DayOne
3
+ def self.journal_location
4
+ @journal_location ||= begin
5
+ prefs_file = File.join(ENV['HOME'], 'Library/Group Containers/5U8NS4GX82.dayoneapp/Data/Preferences/dayone.plist')
6
+ raise 'DayOne preference file not found' unless File.exists?(prefs_file)
7
+
8
+ prefs = File.read(prefs_file)
9
+ match = prefs.match(/<key>JournalPackageURL<\/key>\n\t<string>([^<>]+)<\/string>/)
10
+ raise 'DayOne journal not found' unless match && match[1]
11
+ match[1]
12
+ end
13
+ end
14
+
15
+ class Entry
16
+ attr_accessor :content, :created_at, :tags
17
+
18
+ def initialize(content, options = {})
19
+ @content = content
20
+ @tags = options[:tags] || []
21
+ @created_at = options[:created_at] || Time.now
22
+ end
23
+
24
+ def uuid
25
+ @uuid ||= `uuidgen`.gsub('-', '').strip
26
+ end
27
+
28
+ def file
29
+ @file ||= File.join(DayOne::journal_location, 'entries', "#{uuid}.doentry")
30
+ end
31
+
32
+ def to_xml
33
+ tags_xml = ''
34
+
35
+ unless tags.empty?
36
+ tags_xml = <<XML
37
+ <key>Tags</key>
38
+ <array>
39
+ #{tags.map { |tag| " <string>#{tag}</string>" }.join("\n ") }
40
+ </array>
41
+ XML
42
+ end
43
+
44
+ <<-XML
45
+ <?xml version="1.0" encoding="UTF-8"?>
46
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
47
+ <plist version="1.0">
48
+ <dict>
49
+ <key>Creation Date</key>
50
+ <date>#{created_at.utc.iso8601}</date>
51
+ <key>Entry Text</key>
52
+ <string>#{content}</string>
53
+ #{tags_xml.strip}
54
+ <key>UUID</key>
55
+ <string>#{uuid}</string>
56
+ </dict>
57
+ </plist>
58
+ XML
59
+ end
60
+
61
+ def save!
62
+ File.open(file, 'w') { |f| f.write(to_xml) }
63
+ uuid
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,52 @@
1
+ module DayOneKindle
2
+ class Device
3
+ CLIPPINGS_PATH = '/documents/My Clippings.txt'.freeze
4
+ VERSION_PATH = '/system/version.txt'.freeze
5
+ BACKUP_PATH = '/clippings-backups'.freeze
6
+ BACKUP_NAME = '%Y%m%d-%H%M%S.txt'.freeze
7
+
8
+ attr_reader :path, :name, :clippings_path, :clippings_backups_path
9
+
10
+ def initialize(path, name)
11
+ @path = path
12
+ @name = name
13
+ @clippings_path = File.join(path, CLIPPINGS_PATH)
14
+ @clippings_backups_path = File.join(path, BACKUP_PATH)
15
+ end
16
+
17
+ def highlights
18
+ ClippingsParser.new(raw_clippings).highlights
19
+ end
20
+
21
+ def archive_highlights!
22
+ Dir.mkdir(clippings_backups_path) unless Dir.exist?(clippings_backups_path)
23
+ backup_name = Time.now.strftime(BACKUP_NAME)
24
+ backup_file_path = File.join(clippings_backups_path, backup_name)
25
+ FileUtils.cp(clippings_path, backup_file_path)
26
+ backup_file_path
27
+ end
28
+
29
+ def clear_highlights!
30
+ File.truncate(clippings_path, 0)
31
+ end
32
+
33
+ def self.find_at(mount_path)
34
+ volumes = Dir.entries(mount_path) - ['.', '..']
35
+
36
+ volumes.map do |name|
37
+ path = File.join(mount_path, name)
38
+ version_path = File.join(path, VERSION_PATH)
39
+
40
+ if File.exist?(version_path) && IO.read(version_path) =~ /^Kindle/
41
+ new(path, name)
42
+ end
43
+ end.compact
44
+ end
45
+
46
+ private
47
+
48
+ def raw_clippings
49
+ File.read(clippings_path).gsub("\r", '')
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,51 @@
1
+ module DayOneKindle
2
+ class Highlight
3
+ attr_reader :book, :highlight, :time, :page, :location, :authors
4
+
5
+ def initialize(options)
6
+ # Faking required named arguments for ruby 2.0 compatibility
7
+ %i(book highlight time).each do |param|
8
+ raise ArgumentError, "missing keyword :#{param}" unless options[param]
9
+ instance_variable_set(:"@#{param}", options[param])
10
+ end
11
+
12
+ @page = options[:page]
13
+ @location = options[:location]
14
+ @authors = options[:authors] || []
15
+ end
16
+
17
+ def to_markdown
18
+ [
19
+ markdown_header,
20
+ '',
21
+ markdown_authors,
22
+ '',
23
+ markdown_highlight,
24
+ '',
25
+ markdown_meta
26
+ ].compact.join("\n")
27
+ end
28
+
29
+ private
30
+
31
+ def markdown_header
32
+ "# Quote from #{book || 'a book'}"
33
+ end
34
+
35
+ def markdown_authors
36
+ return if authors.empty?
37
+ "By #{authors.map { |a| "*#{a}*" }.to_sentence}."
38
+ end
39
+
40
+ def markdown_meta
41
+ meta = []
42
+ meta << "Page #{page}" if page
43
+ meta << "Location #{location}" if location
44
+ meta.join(' - ') unless meta.empty?
45
+ end
46
+
47
+ def markdown_highlight
48
+ '> ' + highlight.gsub(/\n/, "\n>")
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ module DayOneKindle
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,19 @@
1
+ require 'test_helper'
2
+
3
+ class ClippingParserTest < Minitest::Test
4
+ def test_parsing
5
+ clipping = <<EOF
6
+ Shibumi (Trevanian)
7
+ - Your Highlight on Page 121 | Location 1851-1853 | Added on Friday, November 28, 2014 3:31:42 PM
8
+
9
+ Do not fall into the error of the artisan who boasts of twenty years experience in his craft while in fact he has had only one year of experience—twenty times.
10
+ EOF
11
+ highlight = DayOneKindle::ClippingParser.new(clipping).highlight
12
+
13
+ assert_equal 'Shibumi', highlight.book
14
+ assert_equal ['Trevanian'], highlight.authors
15
+ assert_equal 121, highlight.page
16
+ assert_equal '1851-1853', highlight.location
17
+ assert_equal Time.new(2014, 11, 28, 15, 31, 42), highlight.time
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ require 'test_helper'
2
+
3
+ class HighlightTest < Minitest::Test
4
+ def test_to_markdown
5
+ highlight = DayOneKindle::Highlight.new(
6
+ book: 'The Pragmatic Programmer: From Journeyman to Master',
7
+ authors: ['Andrew Hunt', 'David Thomas'],
8
+ highlight: 'Embrace the fact that debugging is just problem solving, and attack it as such.',
9
+ time: Time.new(2014, 3, 5, 15, 4, 22),
10
+ page: 121,
11
+ location: '1850-1850'
12
+ )
13
+
14
+ expected_markdown = <<EOF
15
+ # Quote from The Pragmatic Programmer: From Journeyman to Master
16
+
17
+ By *Andrew Hunt* and *David Thomas*.
18
+
19
+ > Embrace the fact that debugging is just problem solving, and attack it as such.
20
+
21
+ Page 121 - Location 1850-1850
22
+ EOF
23
+ expected_markdown.strip!
24
+
25
+ assert_equal expected_markdown, highlight.to_markdown
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ require 'minitest/autorun'
4
+
5
+ require 'minitest/reporters'
6
+ Minitest::Reporters.use!(Minitest::Reporters::DefaultReporter.new(color: true))
7
+
8
+ require './lib/dayone-kindle'
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dayone-kindle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tim Petricola
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.3.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.3.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 10.4.2
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 10.4.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.19.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.19.1
55
+ description:
56
+ email: tim.petricola@gmail.com
57
+ executables:
58
+ - dayone-kindle
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".travis.yml"
63
+ - Gemfile
64
+ - Gemfile.lock
65
+ - README.md
66
+ - Rakefile
67
+ - bin/dayone-kindle
68
+ - lib/dayone-kindle.rb
69
+ - lib/dayone-kindle/cli.rb
70
+ - lib/dayone-kindle/clipping_parser.rb
71
+ - lib/dayone-kindle/clippings_parser.rb
72
+ - lib/dayone-kindle/data_store.rb
73
+ - lib/dayone-kindle/day_one.rb
74
+ - lib/dayone-kindle/device.rb
75
+ - lib/dayone-kindle/highlight.rb
76
+ - lib/dayone-kindle/version.rb
77
+ - test/clipping_parser_test.rb
78
+ - test/highlight_test.rb
79
+ - test/test_helper.rb
80
+ homepage: https://github.com/TimPetricola/dayone-kindle
81
+ licenses:
82
+ - MIT
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 2.4.5
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Import highlights from Kindle to Day One
104
+ test_files:
105
+ - test/clipping_parser_test.rb
106
+ - test/highlight_test.rb
107
+ - test/test_helper.rb