dayone-kindle 0.1.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: 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