class Alexandria::Library
Constants
- AMERICAN_UPC_LOOKUP
- BOOK_REMOVED
- DEFAULT_DIR
- EXT
Attributes
deleted_books[RW]
name[R]
ruined_books[RW]
updating[RW]
Public Class Methods
canonicalise_ean(code)
click to toggle source
# File lib/alexandria/models/library.rb, line 144 def self.canonicalise_ean(code) code = code.to_s.delete("- ") if valid_ean?(code) code elsif valid_isbn?(code) code = "978" + code[0..8] code + String(ean_checksum(extract_numbers(code))) elsif valid_upc?(code) isbn10 = canonicalise_isbn code = "978" + isbn10[0..8] code + String(ean_checksum(extract_numbers(code))) end end
canonicalise_isbn(isbn)
click to toggle source
# File lib/alexandria/models/library.rb, line 158 def self.canonicalise_isbn(isbn) numbers = extract_numbers(isbn) return isbn if valid_ean?(isbn) && (numbers[0..2] != [9, 7, 8]) canonical = if valid_ean?(isbn) # Looks like an EAN number -- extract the intersting part and # calculate a checksum. It would be nice if we could validate # the EAN number somehow. numbers[3..11] + [isbn_checksum(numbers[3..11])] elsif valid_upc?(isbn) # Seems to be a valid UPC number. prefix = upc_convert(numbers[0..5]) isbn_sans_chcksm = prefix + numbers[(8 + prefix.length)..17] isbn_sans_chcksm + [isbn_checksum(isbn_sans_chcksm)] elsif valid_isbn?(isbn) # Seems to be a valid ISBN number. numbers[0..-2] + [isbn_checksum(numbers[0..-2])] end return unless canonical canonical.map(&:to_s).join end
deleted_libraries()
click to toggle source
# File lib/alexandria/models/library.rb, line 255 def self.deleted_libraries @@deleted_libraries end
ean_checksum(numbers)
click to toggle source
# File lib/alexandria/models/library.rb, line 100 def self.ean_checksum(numbers) -(numbers.values_at(1, 3, 5, 7, 9, 11).sum * 3 + numbers.values_at(0, 2, 4, 6, 8, 10).sum) % 10 end
extract_numbers(entry)
click to toggle source
# File lib/alexandria/models/library.rb, line 76 def self.extract_numbers(entry) return [] if entry.nil? || entry.empty? normalized = entry.delete("- ").upcase return [] unless /\A[\dX]*\Z/.match?(normalized) normalized.chars.map do |char| char == "X" ? 10 : char.to_i end end
generate_new_name(existing_libraries, from_base = _("Untitled"))
click to toggle source
# File lib/alexandria/models/library.rb, line 41 def self.generate_new_name(existing_libraries, from_base = _("Untitled")) i = 1 name = nil all_libraries = existing_libraries + @@deleted_libraries loop do name = i == 1 ? from_base : from_base + " #{i}" break unless all_libraries.find { |x| x.name == name } i += 1 end name end
identify_csv_type(header)
click to toggle source
LibraryThing has 15 fields (Apr 2010), Goodreads has 29 we shall guess that “PUBLICATION INFO” implies LibraryThing and “Year Published” implies Goodreads
# File lib/alexandria/import_library_csv.rb, line 182 def self.identify_csv_type(header) is_librarything = false is_goodreads = false header.each do |head| case head when "'PUBLICATION INFO'" is_librarything = true break when "Year Published" is_goodreads = true break end end return LibraryThingCSVImport.new(header) if is_librarything return GoodreadsCSVImport.new(header) if is_goodreads raise _("Not Recognized") unless is_librarything || is_goodreads end
import_as_csv_file(name, filename, on_iterate_cb, _on_error_cb)
click to toggle source
# File lib/alexandria/import_library.rb, line 139 def self.import_as_csv_file(name, filename, on_iterate_cb, _on_error_cb) require "alexandria/import_library_csv" books_and_covers = [] line_count = File.readlines(filename).reduce(0) { |count, _line| count + 1 } import_count = 0 max_import = line_count - 1 reader = CSV.open(filename, "r") # Goodreads & LibraryThing now use csv header lines header = reader.shift importer = identify_csv_type(header) failed_once = false begin reader.each do |row| book = importer.row_to_book(row) cover = nil if book.isbn # if we can search by ISBN, try to grab the cover begin dl_book, dl_cover = BookProviders.isbn_search(book.isbn) if dl_book.authors.size > book.authors.size # LibraryThing only supports a single author, so # attempt to include more author information if it's # available book.authors = dl_book.authors end book.edition = dl_book.edition unless book.edition cover = dl_cover rescue StandardError log.debug { "Failed to get cover for #{book.title} #{book.isbn}" } end end books_and_covers << [book, cover] import_count += 1 on_iterate_cb&.call(import_count, max_import) end rescue CSV::IllegalFormatError unless failed_once failed_once = true # probably Goodreads' wonky ISBN fields ,,="043432432X", # this is a hack to fix up such files data = File.read(filename) data.gsub!(/,="/, ',"') csv_fixed = Tempfile.new("alexandria_import_csv_fixed_") csv_fixed.write(data) csv_fixed.close reader = CSV.open(csv_fixed.path, "r") header = reader.shift importer = identify_csv_type(header) retry end end # TODO: Pass in library store as an argument library = LibraryCollection.instance.library_store.load_library name books_and_covers.each do |book, cover_uri| log.debug { "Saving #{book.isbn} cover" } library.save_cover(book, cover_uri) unless cover_uri.nil? log.debug { "Saving #{book.isbn}" } library << book library.save(book) end [library, []] end
import_as_isbn_list(name, filename, on_iterate_cb, on_error_cb)
click to toggle source
# File lib/alexandria/import_library.rb, line 210 def self.import_as_isbn_list(name, filename, on_iterate_cb, on_error_cb) log.debug { "Starting import_as_isbn_list... " } isbn_list = File.readlines(filename).map do |line| log.debug { "Trying line #{line}" } [line.chomp, canonicalise_isbn(line.chomp)] unless line == "\n" end log.debug { "Isbn list: #{isbn_list.inspect}" } isbn_list.compact! return nil if isbn_list.empty? max_iterations = isbn_list.length * 2 current_iteration = 1 books = [] bad_isbns = [] failed_lookup_isbns = [] isbn_list.each do |isbn| begin if isbn[1] books << BookProviders.isbn_search(isbn[1]) else bad_isbns << isbn[0] end rescue BookProviders::SearchEmptyError => ex log.debug { ex.message } failed_lookup_isbns << isbn[1] log.debug { "NOTE : ignoring on_error_cb #{on_error_cb}" } # return nil unless # (on_error_cb and on_error_cb.call(e.message)) end on_iterate_cb&.call(current_iteration += 1, max_iterations) end log.debug { "Bad Isbn list: #{bad_isbns.inspect}" } if bad_isbns # TODO: Pass in library store as an argument library = LibraryCollection.instance.library_store.load_library name log.debug { "Going with these #{books.length} books: #{books.inspect}" } books.each do |book, cover_uri| log.debug { "Saving #{book.isbn} cover..." } library.save_cover(book, cover_uri) unless cover_uri.nil? log.debug { "Saving #{book.isbn}..." } library << book library.save(book) on_iterate_cb&.call(current_iteration += 1, max_iterations) end [library, bad_isbns, failed_lookup_isbns] end
import_as_tellico_xml_archive(name, filename, on_iterate_cb, _on_error_cb)
click to toggle source
# File lib/alexandria/import_library.rb, line 69 def self.import_as_tellico_xml_archive(name, filename, on_iterate_cb, _on_error_cb) log.debug { "Starting import_as_tellico_xml_archive... " } return nil unless system("unzip -qqt \"#{filename}\"") tmpdir = File.join(Dir.tmpdir, "tellico_export") FileUtils.rm_rf(tmpdir) Dir.mkdir(tmpdir) Dir.chdir(tmpdir) do system("unzip -qq \"#{filename}\"") file = File.exist?("bookcase.xml") ? "bookcase.xml" : "tellico.xml" xml = REXML::Document.new(File.open(file)) raise unless ["bookcase", "tellico"].include? xml.root.name # FIXME: handle multiple collections raise unless xml.root.elements.size == 1 collection = xml.root.elements[1] raise unless collection.name == "collection" type = collection.attribute("type").value.to_i raise unless (type == 2) || (type == 5) content = [] entries = collection.elements.to_a("entry") (total = entries.size).times do |n| entry = entries[n] elements = entry.elements # Feed an array in here, tomorrow. keys = ["isbn", "publisher", "pub_year", "binding"] book_elements = [neaten(elements["title"].text)] book_elements += if elements["authors"].nil? [[]] else [elements["authors"].elements.to_a.map \ { |x| neaten(x.text) }] end book_elements += keys.map do |key| neaten(elements[key].text) if elements[key] end book_elements[2] = Library.canonicalise_ean(book_elements[2]) # publishing_year book_elements[4] = book_elements[4].to_i unless book_elements[4].nil? log.debug { book_elements.inspect } cover = (neaten(elements["cover"].text) if elements["cover"]) log.debug { cover } book = Book.new(*book_elements) if elements["rating"] rating = elements["rating"].text.to_i book.rating = rating if Book::VALID_RATINGS.member? rating end book.notes = neaten(elements["comments"].text) if elements["comments"] content << [book, cover] on_iterate_cb&.call(n + 1, total) end # TODO: Pass in library store as an argument library = LibraryCollection.instance.library_store.load_library name content.each do |book, cover| library.save_cover(book, File.join(Dir.pwd, "images", cover)) unless cover.nil? library << book library.save(book) end return [library, []] rescue StandardError => ex log.info { ex.message } return nil end end
import_autodetect(*args)
click to toggle source
# File lib/alexandria/import_library.rb, line 53 def self.import_autodetect(*args) log.debug { args.inspect } filename = args[1] log.debug { "Filename is #{filename} and ext is #{filename[-4..]}" } log.debug { "Beginning import: #{args[0]}, #{args[1]}" } if filename[-4..] == ".txt" import_as_isbn_list(*args) elsif [".tc", ".bc"].include? filename[-3..] import_as_tellico_xml_archive(*args) elsif [".csv"].include? filename[-4..] import_as_csv_file(*args) else raise _("Unsupported type") end end
isbn_checksum(numbers)
click to toggle source
# File lib/alexandria/models/library.rb, line 87 def self.isbn_checksum(numbers) sum = (0...numbers.length).reduce(0) do |accumulator, i| accumulator + numbers[i] * (i + 1) end % 11 sum == 10 ? "X" : sum end
jpeg?(file)
click to toggle source
# File lib/alexandria/models/library.rb, line 392 def self.jpeg?(file) File.read(file, 10)[6..9] == "JFIF" end
move(source_library, dest_library, *books)
click to toggle source
# File lib/alexandria/models/library.rb, line 55 def self.move(source_library, dest_library, *books) dest = dest_library.path books.each do |book| FileUtils.mv(source_library.yaml(book), dest) if File.exist?(source_library.cover(book)) FileUtils.mv(source_library.cover(book), dest) end source_library.changed source_library.old_delete(book) source_library.notify_observers(source_library, BOOK_REMOVED, book) dest_library.changed dest_library.delete_if { |book2| book2.ident == book.ident } dest_library << book dest_library.notify_observers(dest_library, BOOK_ADDED, book) end end
neaten(str)
click to toggle source
# File lib/alexandria/import_library.rb, line 260 def self.neaten(str) if str str.strip else str end end
new(name, store = nil)
click to toggle source
# File lib/alexandria/models/library.rb, line 403 def initialize(name, store = nil) @name = name @store = store @deleted_books = [] end
really_delete_deleted_libraries()
click to toggle source
# File lib/alexandria/models/library.rb, line 259 def self.really_delete_deleted_libraries @@deleted_libraries.each do |library| FileUtils.rm_rf(library.path) end end
upc_checksum(numbers)
click to toggle source
# File lib/alexandria/models/library.rb, line 113 def self.upc_checksum(numbers) -(numbers.values_at(0, 2, 4, 6, 8, 10).sum * 3 + numbers.values_at(1, 3, 5, 7, 9).sum) % 10 end
upc_convert(upc)
click to toggle source
# File lib/alexandria/models/library.rb, line 139 def self.upc_convert(upc) test_upc = upc.map(&:to_s).join extract_numbers(AMERICAN_UPC_LOOKUP[test_upc]) end
valid_ean?(ean)
click to toggle source
# File lib/alexandria/models/library.rb, line 105 def self.valid_ean?(ean) numbers = extract_numbers(ean) ((numbers.length == 13) && (ean_checksum(numbers[0..11]) == numbers[12])) || ((numbers.length == 18) && (ean_checksum(numbers[0..11]) == numbers[12])) end
valid_isbn?(isbn)
click to toggle source
# File lib/alexandria/models/library.rb, line 95 def self.valid_isbn?(isbn) numbers = extract_numbers(isbn) (numbers.length == 10) && isbn_checksum(numbers).zero? end
valid_upc?(upc)
click to toggle source
# File lib/alexandria/models/library.rb, line 118 def self.valid_upc?(upc) numbers = extract_numbers(upc) ((numbers.length == 17) && (upc_checksum(numbers[0..10]) == numbers[11])) end
Public Instance Methods
==(other)
click to toggle source
# File lib/alexandria/models/library.rb, line 377 def ==(other) other.is_a?(self.class) && other.name == name end
action_name()
click to toggle source
# File lib/alexandria/ui/init.rb, line 57 def action_name "MoveIn" + name.gsub(/\s/, "") end
copy_covers(somewhere)
click to toggle source
# File lib/alexandria/models/library.rb, line 381 def copy_covers(somewhere) FileUtils.rm_rf(somewhere) FileUtils.mkdir(somewhere) each do |book| next unless File.exist?(cover(book)) FileUtils.cp(cover(book), File.join(somewhere, final_cover(book))) end end
cover(something)
click to toggle source
# File lib/alexandria/models/library.rb, line 336 def cover(something) ident = case something when Book if something.isbn && !something.isbn.empty? something.ident else "g#{something.ident}" # g is for generated id... end when String, Integer something else raise NotImplementedError end File.join(path, ident.to_s + EXT[:cover]) end
delete(book = nil)
click to toggle source
# File lib/alexandria/models/library.rb, line 274 def delete(book = nil) if book.nil? # Delete the whole library. raise if @@deleted_libraries.include?(self) @@deleted_libraries << self else if @deleted_books.include?(book) doubles = @deleted_books.select { |b| b.equal?(book) } unless doubles.empty? raise ArgumentError, format(_("Book %<isbn>s was already deleted"), isbn: book.isbn) end end @deleted_books << book i = index(book) # We check object IDs there because the user could have added # a book with the same identifier as another book he/she # previously deleted and that he/she is trying to redo. if i && self[i].equal?(book) changed old_delete(book) # FIX this will old_delete all '==' books notify_observers(self, BOOK_REMOVED, book) end end end
Also aliased as: old_delete
deleted?()
click to toggle source
# File lib/alexandria/models/library.rb, line 301 def deleted? @@deleted_libraries.include?(self) end
final_cover(book)
click to toggle source
# File lib/alexandria/models/library.rb, line 396 def final_cover(book) # TODO: what about PNG? book.ident + (Library.jpeg?(cover(book)) ? ".jpg" : ".gif") end
n_rated()
click to toggle source
# File lib/alexandria/models/library.rb, line 369 def n_rated count { |x| !x.rating.nil? && x.rating > 0 } end
n_unrated()
click to toggle source
# File lib/alexandria/models/library.rb, line 373 def n_unrated length - n_rated end
name=(name)
click to toggle source
# File lib/alexandria/models/library.rb, line 364 def name=(name) File.rename(path, File.join(@store.library_dir, name)) @name = name end
old_cover(book)
click to toggle source
# File lib/alexandria/models/library.rb, line 332 def old_cover(book) File.join(path, book.ident.to_s + EXT[:cover]) end
path()
click to toggle source
# File lib/alexandria/models/library.rb, line 33 def path File.join(@store.library_dir, @name) end
really_delete_deleted_books()
click to toggle source
# File lib/alexandria/models/library.rb, line 265 def really_delete_deleted_books @deleted_books.each do |book| [yaml(book), cover(book)].each do |file| FileUtils.rm_f(file) end end end
save(book, final = false)
click to toggle source
# File lib/alexandria/models/library.rb, line 200 def save(book, final = false) changed unless final # Let's initialize the saved identifier if not already # (backward compatibility from 0.4.0). book.saved_ident ||= book.ident if book.ident != book.saved_ident FileUtils.rm(yaml(book.saved_ident)) if File.exist?(cover(book.saved_ident)) FileUtils.mv(cover(book.saved_ident), cover(book.ident)) end # Notify before updating the saved identifier, so the views # can still use the old one to update their models. notify_observers(self, BOOK_UPDATED, book) unless final book.saved_ident = book.ident end # #was File.exist? but that returns true for empty files... CathalMagus already_there = (File.size?(yaml(book)) && !@deleted_books.include?(book)) temp_book = book.dup temp_book.library = nil File.open(yaml(temp_book), "w") { |io| io.puts temp_book.to_yaml } # Do not notify twice. return unless changed? notify_observers(self, already_there ? BOOK_UPDATED : BOOK_ADDED, book) end
save_cover(book, cover_uri)
click to toggle source
# File lib/alexandria/models/library.rb, line 239 def save_cover(book, cover_uri) Dir.chdir(path) do # Fetch the cover picture. cover_file = cover(book) data = fetch_image(cover_uri) if data File.write(cover_file, data) # Remove the file if its blank. File.delete(cover_file) if Alexandria::UI::Icons.blank?(cover_file) end end end
select() { |book| ... }
click to toggle source
# File lib/alexandria/models/library.rb, line 324 def select filtered_library = Library.new(@name) each do |book| filtered_library << book if yield(book) end filtered_library end
Also aliased as: old_select
simple_save(book)
click to toggle source
# File lib/alexandria/models/library.rb, line 182 def simple_save(book) # Let's initialize the saved identifier if not already # (backward compatibility from 0.4.0) # book.saved_ident ||= book.ident book.saved_ident = book.ident if book.saved_ident.nil? || book.saved_ident.empty? if book.ident != book.saved_ident FileUtils.rm(yaml(book.saved_ident)) if File.exist?(cover(book.saved_ident)) FileUtils.mv(cover(book.saved_ident), cover(book.ident)) end end book.saved_ident = book.ident filename = book.saved_ident.to_s + ".yaml" File.open(filename, "w") { |io| io.puts book.to_yaml } filename end
transport()
click to toggle source
# File lib/alexandria/models/library.rb, line 234 def transport config = Alexandria::Preferences.instance.http_proxy_config config ? Net::HTTP.Proxy(*config) : Net::HTTP end
undelete(book = nil)
click to toggle source
# File lib/alexandria/models/library.rb, line 305 def undelete(book = nil) if book.nil? # Undelete the whole library. raise unless @@deleted_libraries.include?(self) @@deleted_libraries.delete(self) else raise unless @deleted_books.include?(book) @deleted_books.delete(book) unless include?(book) changed self << book notify_observers(self, BOOK_ADDED, book) end end end
updating?()
click to toggle source
# File lib/alexandria/models/library.rb, line 37 def updating? @updating end
yaml(something, basedir = path)
click to toggle source
# File lib/alexandria/models/library.rb, line 352 def yaml(something, basedir = path) ident = case something when Book something.ident when String, Integer something else raise NotImplementedError end File.join(basedir, ident.to_s + EXT[:book]) end