hawx-alexandria 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2013 Joshua Hawxwell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Alexandria
2
+
3
+ Alexandria is a command-line ebook manager.
4
+
5
+ ## Installing
6
+
7
+
8
+ ## Adding a book
9
+
10
+ When a book is added to alexandria it is automatically converted to each
11
+ supported format (at the moment `.epub` and `.mobi`). This is done with the
12
+ `ebook-convert` tool provided by [Calibre][]. You will need to install
13
+ [Calibre][] then set the environment variable `EBOOK_CONVERT` to be the path to
14
+ the `ebook-convert` file.
15
+
16
+ You can the add books like,
17
+
18
+ ``` bash
19
+ $ alexandria add /path/to/book.epub
20
+ ...
21
+ $ alexandria add /path/to/another-book.mobi
22
+ ...
23
+ ```
24
+
25
+
26
+ ## Listing books
27
+
28
+ You can list all books with
29
+
30
+ ``` bash
31
+ $ alexandria list
32
+ ```
33
+
34
+
35
+ [Calibre]: http://calibre-ebook.com
data/bin/alexandria ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'clive'
4
+ require 'pp'
5
+ require 'data_mapper'
6
+ require 'highline/import'
7
+
8
+ LIB_ROOT = File.expand_path(ENV['ALEXANDRIA_LIBRARY'] || '~/alexandria')
9
+
10
+ FileUtils.mkdir_p LIB_ROOT
11
+ DataMapper.setup(:default, "sqlite://#{LIB_ROOT}/library.db")
12
+
13
+ require_relative '../lib/alexandria'
14
+
15
+ class Alexandria::CLI < Clive
16
+
17
+ desc 'Adds a book to library, converting it to all formats'
18
+ command :add, arg: '<path>...' do
19
+ bool :quiet, default: true
20
+ bool :force, default: false
21
+
22
+ action do
23
+ options = {
24
+ quiet: get(:quiet),
25
+ force: get(:force)
26
+ }
27
+
28
+ path.each {|book_path|
29
+ ::Alexandria::Library.add book_path, options
30
+ }
31
+ end
32
+ end
33
+
34
+ desc 'Lists all books in the library'
35
+ command :list do
36
+ desc 'Show only for author'
37
+ opt :author, args: '<name>'
38
+
39
+ desc 'Show only with title'
40
+ opt :title, args: '<name>'
41
+
42
+ desc 'List by author'
43
+ bool :by_author
44
+
45
+ desc 'Show full output'
46
+ bool :v, :verbose
47
+
48
+ action do
49
+ criteria = {
50
+ title: get(:title),
51
+ author: {
52
+ name: get(:author)
53
+ }
54
+ }
55
+
56
+ if get(:by_author)
57
+ ::Alexandria::Library.authors(criteria[:author]).each do |author|
58
+ puts author.name.bold
59
+
60
+ author.books.each do |book|
61
+ puts " #{book.title}"
62
+ puts " #{book.path}\n".grey if get(:verbose)
63
+ end
64
+ end
65
+
66
+ else
67
+ ::Alexandria::Library.books(criteria).each do |book|
68
+ puts "#{book.title.bold} by #{book.author.name}"
69
+ puts " #{book.path}\n".grey if get(:verbose)
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ desc 'Syncs books from a connected device to the library'
76
+ command :sync do
77
+ bool :dry_run
78
+
79
+ action do
80
+ device = ::Alexandria::Device.find
81
+
82
+ unless device
83
+ puts "Nothing to sync.".red
84
+ exit
85
+ end
86
+
87
+ device.each_book do |book|
88
+ unless Book.any?(author: book.author, title: book.title)
89
+ if get(:dry_run)
90
+ puts "Missing: #{book.title} by #{book.author}"
91
+ else
92
+ ::Alexandria::Library.add(book)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ Alexandria::CLI.run
@@ -0,0 +1,26 @@
1
+ module Alexandria
2
+ module Book
3
+ @@registered = {}
4
+
5
+ def self.register(extensions, klass)
6
+ extensions.each do |extension|
7
+ @@registered[extension] = klass
8
+ end
9
+ end
10
+
11
+ def self.create(path)
12
+ ext = File.extname(path)
13
+
14
+ unless @@registered.has_key?(ext)
15
+ warn "Unrecognised extension #{ext}"
16
+ exit 2
17
+ end
18
+
19
+ @@registered[ext].new(path)
20
+ end
21
+
22
+ def self.extensions
23
+ @@registered.values.map(&:extension)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ require 'fileutils'
2
+
3
+ module Alexandria::Book
4
+
5
+ # @abstract Implement {#author}, {#title} and {.extension}.
6
+ class Base
7
+ EXTENSIONS = []
8
+
9
+ attr_reader :path
10
+
11
+ def initialize(path)
12
+ @path = path.to_s
13
+ end
14
+
15
+ def author
16
+ "None"
17
+ end
18
+
19
+ def title
20
+ "Untitled"
21
+ end
22
+
23
+ def self.extension
24
+ ".missing"
25
+ end
26
+
27
+ def write(dir)
28
+ name = File.basename(dir)
29
+ write_path = File.join(dir, "#{name}#{self.class.extension}")
30
+
31
+ FileUtils.mkdir_p dir
32
+ File.write(write_path, File.read(self.path))
33
+ end
34
+
35
+ def inspect
36
+ "#<#{self.class} '#{self.title}'>"
37
+ end
38
+ alias_method :to_s, :inspect
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ require 'peregrin'
2
+
3
+ class Peregrin::Property
4
+ def to_h
5
+ {key => value}
6
+ end
7
+ end
8
+
9
+ module Alexandria::Book
10
+
11
+ class Epub < Base
12
+ def author
13
+ metadata['creator'] || super
14
+ end
15
+
16
+ def title
17
+ metadata['title'].force_encoding("utf-8") || super
18
+ end
19
+
20
+ def self.extension
21
+ ".epub"
22
+ end
23
+
24
+ private
25
+
26
+ def metadata
27
+ @meta ||= Peregrin::Epub.read(@path)
28
+ .to_book
29
+ .properties
30
+ .inject({}) {|a,e| a.merge(e.to_h) }
31
+ end
32
+ end
33
+
34
+ register %w(.epub), Epub
35
+ end
@@ -0,0 +1,26 @@
1
+ require 'mobi'
2
+
3
+ module Alexandria::Book
4
+
5
+ class Mobi < Base
6
+ def author
7
+ metadata.author || super
8
+ end
9
+
10
+ def title
11
+ metadata.title.force_encoding("utf-8") || super
12
+ end
13
+
14
+ def self.extension
15
+ ".mobi"
16
+ end
17
+
18
+ private
19
+
20
+ def metadata
21
+ ::Mobi.metadata File.open(@path)
22
+ end
23
+ end
24
+
25
+ register %w(.mobi .azw .azw3), Mobi
26
+ end
@@ -0,0 +1,51 @@
1
+ module Alexandria
2
+
3
+ class Converter
4
+
5
+ def initialize(original_path, options={})
6
+ @original_path = original_path
7
+ @book = Book.create(@original_path)
8
+ end
9
+
10
+ def dir
11
+ File.dirname(@original_path)
12
+ end
13
+
14
+ def normalised_title
15
+ Helpers.normalise(@book.title)
16
+ end
17
+
18
+ def new_path(ext)
19
+ File.join(dir, normalised_title + ext)
20
+ end
21
+
22
+ def converted_to?(ext)
23
+ File.exist? new_path(ext)
24
+ end
25
+
26
+ def convert_to(new_ext)
27
+ new_path = new_path(new_ext)
28
+
29
+ if converted_to?(new_ext)
30
+ return unless agree("File already exists '#{new_path}', convert again? [y/n]".red)
31
+ end
32
+
33
+ if execute(@original_path, new_path, @options[:quiet])
34
+ puts " created".grey + " #{new_path}"
35
+ else
36
+ puts " problem".red + " converting #{@original_path} to #{new_ext}"
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def execute(from, to, quiet=true)
43
+ if quiet
44
+ `#{EBOOK_CONVERT} \"#{from}\" \"#{to}\"`
45
+ else
46
+ system "#{EBOOK_CONVERT} \"#{from}\" \"#{to}\""
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ class Hash
2
+ def compact
3
+ self.map {|k,v|
4
+ v.respond_to?(:compact) ? [k, v.compact] : [k,v]
5
+ }.reject {|k,v|
6
+ v.nil? || v.empty?
7
+ }.to_h
8
+ end
9
+ end
10
+
11
+ class Object
12
+ def to_h
13
+ Hash[self]
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ module Alexandria
2
+
3
+ class Device
4
+ @@registered = {}
5
+
6
+ def self.register(name, klass)
7
+ @@registered[name] = klass
8
+ end
9
+
10
+ def self.find
11
+ @@registered.map(&:find).compact.first
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ require 'pathname'
2
+
3
+ class Alexandria::Device
4
+
5
+ class Kindle
6
+ def self.find
7
+ return false unless Dir.exist?('/Volumes/Kindle')
8
+ new '/Volumes/Kindle'
9
+ end
10
+
11
+ def initialize(path)
12
+ @path = Pathname.new(path)
13
+ end
14
+
15
+ def each_book
16
+ Dir[@path + 'documents' + '**/*.{azw,azw3,mobi}'].each {|path|
17
+ yield Book.create(File.expand_path(path))
18
+ }
19
+ end
20
+ end
21
+
22
+ register :kindle, Kindle
23
+ end
@@ -0,0 +1,18 @@
1
+ module Alexandria
2
+
3
+ module Helpers
4
+ extend self
5
+
6
+ def normalise(title)
7
+ title.gsub(' ', '_').gsub(/\W/, '').gsub('_', '-').downcase
8
+ end
9
+
10
+ def book_path(author, title)
11
+ normal_author = normalise(author)
12
+ normal_title = normalise(title)
13
+ path = File.join(normal_author, normal_title)
14
+
15
+ File.join LIB_ROOT, path
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ module Alexandria
2
+ module Library
3
+ extend self
4
+
5
+ # Adds a book to the current library, then converts it to all possible formats.
6
+ #
7
+ # @param path [String] Path to the book to add.
8
+ def add(path, options={})
9
+ book = Storage::Book.from_path(File.expand_path(path), options)
10
+
11
+ instance = Dir.glob(book.path + '/*').first
12
+
13
+ extensions_missing = Book.extensions - [File.extname(instance)]
14
+
15
+ converter = Converter.new(instance, options)
16
+
17
+ extensions_missing.each do |extension|
18
+ converter.convert_to(extension)
19
+ end
20
+ end
21
+
22
+ def books(criteria={})
23
+ Storage::Book.all criteria.compact.merge(:order => [:title.asc])
24
+ end
25
+
26
+ def authors(criteria={})
27
+ Storage::Author.all criteria.compact.merge(:order => [:name.asc])
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,60 @@
1
+ module Alexandria
2
+
3
+ module Storage
4
+
5
+ class Book
6
+ include ::DataMapper::Resource
7
+
8
+ property :id, Serial
9
+ property :title, String
10
+ property :path, String
11
+
12
+ belongs_to :author
13
+
14
+ def self.from_path(path, options={})
15
+ book = ::Alexandria::Book.create(path)
16
+
17
+ path = ::Alexandria::Helpers.book_path(book.author, book.title)
18
+
19
+ if found = Book.first(:path => path)
20
+ puts "Book already exists!".red
21
+ return found unless options[:force]
22
+ end
23
+
24
+ book.write(path)
25
+
26
+ author = Author.first_or_create(:name => book.author)
27
+ created = Book.first_or_create(:title => book.title, :author => author, :path => path)
28
+ created.save!
29
+
30
+ puts " created".grey + " #{path}"
31
+
32
+ created
33
+ end
34
+
35
+ def epub_path
36
+ File.join path, File.basename(path) + '.epub'
37
+ end
38
+
39
+ def mobi_path
40
+ File.join path, File.basename(path) + '.mobi'
41
+ end
42
+ end
43
+
44
+ class Author
45
+ include ::DataMapper::Resource
46
+
47
+ property :id, Serial
48
+ property :name, String
49
+
50
+ has n, :books
51
+
52
+ def books
53
+ Book.all(:author => self, :order => [:title.asc])
54
+ end
55
+ end
56
+
57
+ ::DataMapper.finalize
58
+ ::DataMapper.auto_upgrade!
59
+ end
60
+ end
data/lib/alexandria.rb ADDED
@@ -0,0 +1,23 @@
1
+ # coding: UTF-8
2
+
3
+ require 'pathname'
4
+ require 'clive/output'
5
+
6
+ EBOOK_CONVERT = ENV['EBOOK_CONVERT'] ||
7
+ '/Applications/calibre.app/Contents/MacOS/ebook-convert'
8
+ # Fall back to default mac location. I know, mac.
9
+
10
+ require_relative 'alexandria/core_ext'
11
+ require_relative 'alexandria/helpers'
12
+
13
+ require_relative 'alexandria/book'
14
+ require_relative 'alexandria/books/base'
15
+ require_relative 'alexandria/books/epub'
16
+ require_relative 'alexandria/books/mobi'
17
+
18
+ require_relative 'alexandria/device'
19
+ require_relative 'alexandria/devices/kindle'
20
+
21
+ require_relative 'alexandria/converter'
22
+ require_relative 'alexandria/library'
23
+ require_relative 'alexandria/storage'
metadata ADDED
@@ -0,0 +1,159 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hawx-alexandria
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Joshua Hawxwell
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: peregrin
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.2'
30
+ - !ruby/object:Gem::Dependency
31
+ name: mobi
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.2.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.2.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: clive
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.2'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.2'
62
+ - !ruby/object:Gem::Dependency
63
+ name: highline
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '1.6'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '1.6'
78
+ - !ruby/object:Gem::Dependency
79
+ name: data_mapper
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '1.2'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '1.2'
94
+ - !ruby/object:Gem::Dependency
95
+ name: dm-sqlite-adapter
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '1.2'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '1.2'
110
+ description: ! ' An ebook library manager, with one-way kindle syncing.
111
+
112
+ '
113
+ email: m@hawx.me
114
+ executables:
115
+ - alexandria
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - README.md
120
+ - LICENSE
121
+ - bin/alexandria
122
+ - lib/alexandria/book.rb
123
+ - lib/alexandria/books/base.rb
124
+ - lib/alexandria/books/epub.rb
125
+ - lib/alexandria/books/mobi.rb
126
+ - lib/alexandria/converter.rb
127
+ - lib/alexandria/core_ext.rb
128
+ - lib/alexandria/device.rb
129
+ - lib/alexandria/devices/kindle.rb
130
+ - lib/alexandria/helpers.rb
131
+ - lib/alexandria/library.rb
132
+ - lib/alexandria/storage.rb
133
+ - lib/alexandria.rb
134
+ homepage: http://github.com/hawx/alexandria
135
+ licenses: []
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ none: false
142
+ requirements:
143
+ - - ! '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ none: false
148
+ requirements:
149
+ - - ! '>='
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 1.8.23
155
+ signing_key:
156
+ specification_version: 3
157
+ summary: A library for your ebooks.
158
+ test_files: []
159
+ has_rdoc: