Class: Alexandria::Library

Inherits:
Array
  • Object
show all
Extended by:
GetText
Includes:
Exportable, Logging, GetText, Observable
Defined in:
lib/alexandria/export_library.rb,
lib/alexandria/import_library.rb,
lib/alexandria/models/library.rb,
lib/alexandria/import_library_csv.rb,
lib/alexandria/ui/init.rb

Direct Known Subclasses

SortedLibrary

Defined Under Namespace

Classes: InvalidISBNError, NoISBNError

Constant Summary collapse

DIR =
File.join(ENV['HOME'], '.alexandria')
EXT =
{ book: '.yaml', cover: '.cover' }.freeze
FIX_BIGNUM_REGEX =
/loaned_since:\s*(\!ruby\/object\:Bignum\s*)?(\d+)\n/
AMERICAN_UPC_LOOKUP =
{
  '014794' => '08041', '018926' => '0445', '02778' => '0449',
  '037145' => '0812', '042799' => '0785',  '043144' => '0688',
  '044903' => '0312', '045863' => '0517', '046594' => '0064',
  '047132' => '0152', '051487' => '08167', '051488' => '0140',
  '060771' => '0002', '065373' => '0373', '070992' => '0523',
  '070993' => '0446', '070999' => '0345', '071001' => '0380',
  '071009' => '0440', '071125' => '088677', '071136' => '0451',
  '071149' => '0451', '071152' => '0515', '071162' => '0451',
  '071268' => '08217', '071831' => '0425', '071842' => '08439',
  '072742' => '0441', '076714' => '0671', '076783' => '0553',
  '076814' => '0449', '078021' => '0872', '079808' => '0394',
  '090129' => '0679', '099455' => '0061', '099769' => '0451'
}.freeze
@@deleted_libraries =
[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

included, #log

Methods included from Exportable

#export_as_bibtex, #export_as_csv_list, #export_as_html, #export_as_ipod_notes, #export_as_isbn_list, #export_as_onix_xml_archive, #export_as_tellico_xml_archive

Instance Attribute Details

#deleted_booksObject

Returns the value of attribute deleted_books.



35
36
37
# File 'lib/alexandria/models/library.rb', line 35

def deleted_books
  @deleted_books
end

#nameObject

Returns the value of attribute name.



34
35
36
# File 'lib/alexandria/models/library.rb', line 34

def name
  @name
end

#ruined_booksObject

Returns the value of attribute ruined_books.



35
36
37
# File 'lib/alexandria/models/library.rb', line 35

def ruined_books
  @ruined_books
end

#updatingObject

Returns the value of attribute updating.



35
36
37
# File 'lib/alexandria/models/library.rb', line 35

def updating
  @updating
end

Class Method Details

.canonicalise_ean(code) ⇒ Object



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/alexandria/models/library.rb', line 347

def self.canonicalise_ean(code)
  code = code.to_s.delete('- ')
  if valid_ean?(code)
    return code
  elsif valid_isbn?(code)
    code = '978' + code[0..8]
    return code + String(ean_checksum(extract_numbers(code)))
  elsif valid_upc?(code)
    isbn10 = canonicalise_isbn
    code = '978' + isbn10[0..8]
    return code + String(ean_checksum(extract_numbers(code)))
    ## raise "fix function Alexandria::Library.canonicalise_ean"
  else
    raise InvalidISBNError, code
  end
end

.canonicalise_isbn(isbn) ⇒ Object



364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/alexandria/models/library.rb', line 364

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])]
              else
                raise InvalidISBNError, isbn
              end

  canonical.map(&:to_s).join
end

.deleted_librariesObject



468
469
470
# File 'lib/alexandria/models/library.rb', line 468

def self.deleted_libraries
  @@deleted_libraries
end

.ean_checksum(numbers) ⇒ Object



299
300
301
302
# File 'lib/alexandria/models/library.rb', line 299

def self.ean_checksum(numbers)
  -(numbers.values_at(1, 3, 5, 7, 9, 11).reduce(:+) * 3 +
    numbers.values_at(0, 2, 4, 6, 8, 10).reduce(:+)) % 10
end

.extract_numbers(isbn) ⇒ Object

Raises:



275
276
277
278
279
280
281
282
# File 'lib/alexandria/models/library.rb', line 275

def self.extract_numbers(isbn)
  raise NoISBNError, 'Nil ISBN' if isbn.nil? || isbn.empty?

  isbn.delete('- ').upcase.split('').map do |x|
    raise InvalidISBNError, isbn unless x =~ /[\dX]/
    x == 'X' ? 10 : x.to_i
  end
end

.generate_new_name(existing_libraries, from_base = _('Untitled')) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/alexandria/models/library.rb', line 54

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) ⇒ Object

LibraryThing has 15 fields (Apr 2010), Goodreads has 29 we shall guess that “PUBLICATION INFO” implies LibraryThing and “Year Published” implies Goodreads



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/alexandria/import_library_csv.rb', line 185

def self.identify_csv_type(header)
  is_librarything = false
  is_goodreads = false
  header.each do |head|
    if head == "'PUBLICATION INFO'"
      is_librarything = true
      break
    elsif head == 'Year Published'
      is_goodreads = true
      break
    end
  end
  if is_librarything
    return LibraryThingCSVImport.new(header)
  elsif is_goodreads
    return GoodreadsCSVImport.new(header)
  end
  raise 'Not Recognized' unless is_librarything || is_goodreads
end

.import_as_csv_file(name, filename, on_iterate_cb, _on_error_cb) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/alexandria/import_library.rb', line 170

def self.import_as_csv_file(name, filename, on_iterate_cb, _on_error_cb)
  require 'alexandria/import_library_csv'
  books_and_covers = []
  line_count = IO.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 = Alexandria::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
          puts "failed to get cover for #{book.title} #{book.isbn}" if $DEBUG
          # note failure
        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

  library = Library.load(name)

  books_and_covers.each do |book, cover_uri|
    puts "Saving #{book.isbn} cover..." if $DEBUG
    library.save_cover(book, cover_uri) unless cover_uri.nil?
    puts "Saving #{book.isbn}..." if $DEBUG
    library << book
    library.save(book)
  end
  [library, []]
end

.import_as_isbn_list(name, filename, on_iterate_cb, on_error_cb) ⇒ Object



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/alexandria/import_library.rb', line 241

def self.import_as_isbn_list(name, filename, on_iterate_cb,
                             on_error_cb)
  puts 'Starting import_as_isbn_list... '
  isbn_list = IO.readlines(filename).map do |line|
    puts "Trying line #{line}" if $DEBUG
    # Let's preserve the failing isbns so we can report them later.
    begin
      [line.chomp, canonicalise_isbn(line.chomp)] unless line == "\n"
    rescue => e
      puts e.message
      [line.chomp, nil]
    end
  end
  puts "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 << Alexandria::BookProviders.isbn_search(isbn[1])
      else
        bad_isbns << isbn[0]
      end
    rescue => e
      puts e.message
      failed_lookup_isbns << isbn[1]
      puts "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
  puts "Bad Isbn list: #{bad_isbns.inspect}" if bad_isbns
  library = load(name)
  puts "Going with these #{books.length} books: #{books.inspect}" if $DEBUG
  books.each do |book, cover_uri|
    puts "Saving #{book.isbn} cover..." if $DEBUG
    library.save_cover(book, cover_uri) unless cover_uri.nil?
    puts "Saving #{book.isbn}..." if $DEBUG
    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) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/alexandria/import_library.rb', line 87

def self.import_as_tellico_xml_archive(name, filename,
                                       on_iterate_cb, _on_error_cb)
  puts 'Starting import_as_tellico_xml_archive... '
  return nil unless system("unzip -qqt \"#{filename}\"")
  tmpdir = File.join(Dir.tmpdir, 'tellico_export')
  FileUtils.rm_rf(tmpdir) if File.exist?(tmpdir)
  Dir.mkdir(tmpdir)
  Dir.chdir(tmpdir) do
    begin
      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?
                           [elements['authors'].elements.to_a.map \
                                             { |x| neaten(x.text) }]
                         else
                           [[]]
                         end
        book_elements += keys.map { |key|
          neaten(elements[key].text) if elements[key]
        }
        # isbn
        if book_elements[2].nil? || book_elements[2].strip.empty?
          book_elements[2] = nil
        else
          begin
            book_elements[2] = book_elements[2].strip
            book_elements[2] = Library.canonicalise_ean(book_elements[2])
          rescue => ex
            puts book_elements[2]
            puts ex.message
            puts ex.backtrace.join("\n> ")
            raise ex
          end
        end
        book_elements[4] = book_elements[4].to_i unless book_elements[4].nil? # publishing_year
        puts book_elements.inspect
        cover = (neaten(elements['cover'].text) if elements['cover'])
        puts cover
        book = Book.new(*book_elements)
        if elements['rating'] && Book::VALID_RATINGS.member?(elements['rating'].text.to_i)
          book.rating = elements['rating'].text.to_i
        end
        book.notes = neaten(elements['comments'].text) if elements['comments']
        content << [book, cover]
        on_iterate_cb&.call(n + 1, total)
      end

      library = Library.load(name)
      content.each do |book, cover|
        unless cover.nil?
          library.save_cover(book,
                             File.join(Dir.pwd, 'images',
                                       cover))
        end
        library << book
        library.save(book)
      end
      return [library, []]
    rescue => e
      puts e.message
      return nil
    end
  end
end

.import_autodetect(*args) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/alexandria/import_library.rb', line 65

def self.import_autodetect(*args)
  puts args.inspect
  filename = args[1]
  puts "Filename is #{filename} and ext is #{filename[-4..-1]}"
  puts "Beginning import: #{args[0]}, #{args[1]}"
  if filename[-4..-1] == '.txt'
    import_as_isbn_list(*args)
  elsif ['.tc', '.bc'].include? filename[-3..-1]
    begin
      import_as_tellico_xml_archive(*args)
    rescue => e
      puts e.message
      puts e.backtrace.join("\n>> ")
    end
  elsif ['.csv'].include? filename[-4..-1]
    import_as_csv_file(*args)
  else
    puts 'Bailing on this import!'
    raise 'Not supported type'
  end
end

.isbn_checksum(numbers) ⇒ Object



284
285
286
287
288
289
290
# File 'lib/alexandria/models/library.rb', line 284

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) ⇒ Boolean

Returns:

  • (Boolean)


602
603
604
# File 'lib/alexandria/models/library.rb', line 602

def self.jpeg?(file)
  IO.read(file, 10)[6..9] == 'JFIF'
end

.load(name) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/alexandria/models/library.rb', line 70

def self.load(name)
  test = [0, nil]
  ruined_books = []
  library = Library.new(name)
  FileUtils.mkdir_p(library.path) unless File.exist?(library.path)
  Dir.chdir(library.path) do
    Dir['*' + EXT[:book]].each do |filename|
      test[1] = filename if (test[0]).zero?

      unless File.size? test[1]
        log.warn { "Book file #{test[1]} was empty" }
        md = /([\dxX]{10,13})#{EXT[:book]}/.match(filename)
        if md
          file_isbn = md[1]
          ruined_books << [nil, file_isbn, library]
        else
          log.warn { "Filename #{filename} does not contain an ISBN" }
          # TODO: delete this file...
        end
        next
      end
      book = regularize_book_from_yaml(test[1])
      old_isbn = book.isbn
      old_pub_year = book.publishing_year
      begin
        begin
          book.isbn = canonicalise_ean(book.isbn).to_s unless book.isbn.nil?
          raise "Not a book: #{book.inspect}" unless book.is_a?(Book)
        rescue InvalidISBNError
          book.isbn = old_isbn
        end

        book.publishing_year = book.publishing_year.to_i unless book.publishing_year.nil?

        # Or if isbn has changed
        raise "#{test[1]} isbn is not okay" unless book.isbn == old_isbn

        # Re-save book if Alexandria::DATA_VERSION changes
        raise "#{test[1]} version is not okay" unless book.version == Alexandria::DATA_VERSION

        # Or if publishing year has changed
        raise "#{test[1]} pub year is not okay" unless book.publishing_year == old_pub_year

        # ruined_books << [book, book.isbn, library]
        book.library = library.name

        ## TODO copy cover image file, if necessary
        # due to #26909 cover files for books without ISBN are re-saved as "g#{ident}.cover"
        if book.isbn.nil? || book.isbn.empty?
          if File.exist? library.old_cover(book)
            log.debug { "#{library.name}; book #{book.title} has no ISBN, fixing cover image" }
            FileUtils::Verbose.mv(library.old_cover(book), library.cover(book))
          end
        end

        library << book
      rescue
        book.version = Alexandria::DATA_VERSION
        savedfilename = library.simple_save(book)
        test[0] = test[0] + 1
        test[1] = savedfilename

        # retries the Dir.each block...
        # but gives up after three tries
        redo unless test[0] > 2
      else
        test = [0, nil]
      end
    end

    # Since 0.4.0 the cover files '_small.jpg' and
    # '_medium.jpg' have been deprecated for a single medium
    # cover file named '.cover'.

    Dir['*' + '_medium.jpg'].each do |medium_cover|
      begin
        FileUtils.mv(medium_cover,
                     medium_cover.sub(/_medium\.jpg$/,
                                      EXT[:cover]))
      rescue
      end
    end

    Dir['*' + EXT[:cover]].each do |cover|
      next if cover[0] == 'g'
      md = /(.+)\.cover/.match(cover)
      begin
        ean = canonicalise_ean(md[1])
      rescue
        ean = md[1]
      end
      begin
        FileUtils.mv(cover, ean + EXT[:cover]) unless cover == ean + EXT[:cover]
      rescue
      end
    end

    FileUtils.rm_f(Dir['*_small.jpg'])
  end
  library.ruined_books = ruined_books

  library
end

.loadallObject



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/alexandria/models/library.rb', line 224

def self.loadall
  a = []
  begin
    Dir.entries(DIR).each do |file|
      # Skip hidden files.
      next if file =~ /^\./
      # Skip non-directory files.
      next unless File.stat(File.join(DIR, file)).directory?

      a << load(file)
    end
  rescue Errno::ENOENT
    FileUtils.mkdir_p(DIR)
  end
  # Create the default library if there is no library yet.

  a << load(_('My Library')) if a.empty?

  a
end

.move(source_library, dest_library, *books) ⇒ Object



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/alexandria/models/library.rb', line 245

def self.move(source_library, dest_library, *books)
  dest = dest_library.path
  books.each do |book|
    FileUtils.mv(source_library.yaml(book), dest)
    FileUtils.mv(source_library.cover(book), dest) if File.exist?(source_library.cover(book))

    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) ⇒ Object



293
294
295
296
297
298
299
# File 'lib/alexandria/import_library.rb', line 293

def self.neaten(str)
  if str
    str.strip
  else
    str
  end
end

.really_delete_deleted_librariesObject



472
473
474
475
476
# File 'lib/alexandria/models/library.rb', line 472

def self.really_delete_deleted_libraries
  @@deleted_libraries.each do |library|
    FileUtils.rm_rf(library.path)
  end
end

.regularize_book_from_yaml(name) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/alexandria/models/library.rb', line 174

def self.regularize_book_from_yaml(name)
  text = IO.read(name)

  # Code to remove the mystery string in books imported from Amazon
  # (In the past, still?) To allow ruby-amazon to be removed.

  # The string is removed on load, but can't make it stick, maybe has to do with cache

  if text =~ /!str:Amazon::Search::Response/
    log.debug { "Removing Ruby/Amazon strings from #{name}" }
    text.gsub!('!str:Amazon::Search::Response', '')
  end

  # Backward compatibility with versions <= 0.6.0, where the
  # loaned_since field was a numeric.
  if (md = FIX_BIGNUM_REGEX.match(text))
    new_yaml = Time.at(md[2].to_i).to_yaml
    # Remove the "---" prefix.
    new_yaml.sub!(/^\s*\-+\s*/, '')
    text.sub!(md[0], "loaned_since: #{new_yaml}\n")
  end

  # TODO: Ensure book loading passes through Book#initialize
  book = YAML.safe_load(text, whitelist_classes = [Book, Time])

  unless book.isbn.class == String
    # HACK
    md = /isbn: (.+)/.match(text)
    if md
      string_isbn = md[1].strip
      book.isbn = string_isbn
    end
  end

  # another HACK of the same type as above
  unless book.saved_ident.class == String

    md2 = /saved_ident: (.+)/.match(text)
    if md2
      string_saved_ident = md2[1].strip
      log.debug { "fixing saved_ident #{book.saved_ident} -> #{string_saved_ident}" }
      book.saved_ident = string_saved_ident
    end
  end
  if (book.isbn.class == String) && book.isbn.empty?
    book.isbn = nil # save trouble later
  end
  book
end

.upc_checksum(numbers) ⇒ Object



314
315
316
317
# File 'lib/alexandria/models/library.rb', line 314

def self.upc_checksum(numbers)
  -(numbers.values_at(0, 2, 4, 6, 8, 10).reduce(:+) * 3 +
    numbers.values_at(1, 3, 5, 7, 9).reduce(:+)) % 10
end

.upc_convert(upc) ⇒ Object



342
343
344
345
# File 'lib/alexandria/models/library.rb', line 342

def self.upc_convert(upc)
  test_upc = upc.map(&:to_s).join
  extract_numbers(AMERICAN_UPC_LOOKUP[test_upc])
end

.valid_ean?(ean) ⇒ Boolean

Returns:

  • (Boolean)


304
305
306
307
308
309
310
311
312
# File 'lib/alexandria/models/library.rb', line 304

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]))
rescue InvalidISBNError
  false
end

.valid_isbn?(isbn) ⇒ Boolean

Returns:

  • (Boolean)


292
293
294
295
296
297
# File 'lib/alexandria/models/library.rb', line 292

def self.valid_isbn?(isbn)
  numbers = extract_numbers(isbn)
  (numbers.length == 10) && isbn_checksum(numbers).zero?
rescue InvalidISBNError
  false
end

.valid_upc?(upc) ⇒ Boolean

Returns:

  • (Boolean)


319
320
321
322
323
324
325
# File 'lib/alexandria/models/library.rb', line 319

def self.valid_upc?(upc)
  numbers = extract_numbers(upc)
  ((numbers.length == 17) &&
   (upc_checksum(numbers[0..10]) == numbers[11]))
rescue InvalidISBNError
  false
end

Instance Method Details

#==(object) ⇒ Object



588
589
590
# File 'lib/alexandria/models/library.rb', line 588

def ==(object)
  object.is_a?(self.class) && object.name == name
end

#action_nameObject



62
63
64
# File 'lib/alexandria/ui/init.rb', line 62

def action_name
  'MoveIn' + name.gsub(/\s/, '')
end

#copy_covers(somewhere) ⇒ Object



592
593
594
595
596
597
598
599
600
# File 'lib/alexandria/models/library.rb', line 592

def copy_covers(somewhere)
  FileUtils.rm_rf(somewhere) if File.exist?(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) ⇒ Object



543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/alexandria/models/library.rb', line 543

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
            something
          when Integer
            something
          else
            raise "#{something} is a #{something.class}"
          end
  File.join(path, ident.to_s + EXT[:cover])
end

#delete(book = nil) ⇒ Object



487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/alexandria/models/library.rb', line 487

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) }
      raise ArgumentError, "Book #{book.isbn} was already deleted" unless doubles.empty?
    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

#deleted?Boolean

Returns:

  • (Boolean)


510
511
512
# File 'lib/alexandria/models/library.rb', line 510

def deleted?
  @@deleted_libraries.include?(self)
end

#final_cover(book) ⇒ Object



606
607
608
609
# File 'lib/alexandria/models/library.rb', line 606

def final_cover(book)
  # TODO: what about PNG?
  book.ident + (Library.jpeg?(cover(book)) ? '.jpg' : '.gif')
end

#n_ratedObject



580
581
582
# File 'lib/alexandria/models/library.rb', line 580

def n_rated
  count { |x| !x.rating.nil? && x.rating > 0 }
end

#n_unratedObject



584
585
586
# File 'lib/alexandria/models/library.rb', line 584

def n_unrated
  length - n_rated
end

#old_cover(book) ⇒ Object



539
540
541
# File 'lib/alexandria/models/library.rb', line 539

def old_cover(book)
  File.join(path, book.ident.to_s + EXT[:cover])
end

#old_deleteObject



486
# File 'lib/alexandria/models/library.rb', line 486

alias old_delete delete

#old_selectObject



530
# File 'lib/alexandria/models/library.rb', line 530

alias old_select select

#pathObject



46
47
48
# File 'lib/alexandria/models/library.rb', line 46

def path
  File.join(DIR, @name)
end

#really_delete_deleted_booksObject



478
479
480
481
482
483
484
# File 'lib/alexandria/models/library.rb', line 478

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) ⇒ Object



409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/alexandria/models/library.rb', line 409

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))
    FileUtils.mv(cover(book.saved_ident), cover(book.ident)) if File.exist?(cover(book.saved_ident))

    # 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.
  if changed?
    notify_observers(self,
                     already_there ? BOOK_UPDATED : BOOK_ADDED,
                     book)
  end
end

#save_cover(book, cover_uri) ⇒ Object



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'lib/alexandria/models/library.rb', line 446

def save_cover(book, cover_uri)
  Dir.chdir(path) do
    # Fetch the cover picture.
    cover_file = cover(book)
    File.open(cover_file, 'w') do |io|
      uri = URI.parse(cover_uri)
      if uri.scheme.nil?
        # Regular filename.
        File.open(cover_uri) { |io2| io.puts io2.read }
      else
        # Try open-uri.
        io.puts transport.get(uri)
      end
    end

    # Remove the file if its blank.
    File.delete(cover_file) if Alexandria::UI::Icons.blank?(cover_file)
  end
end

#selectObject



531
532
533
534
535
536
537
# File 'lib/alexandria/models/library.rb', line 531

def select
  filtered_library = Library.new(@name)
  each do |book|
    filtered_library << book if yield(book)
  end
  filtered_library
end

#simple_save(book) ⇒ Object



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/alexandria/models/library.rb', line 387

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
    # log.debug { "Backwards compatibility step: #{book.saved_ident.inspect}, #{book.ident.inspect}" }
    FileUtils.rm(yaml(book.saved_ident))
  end
  if File.exist?(cover(book.saved_ident))
    begin
      FileUtils.mv(cover(book.saved_ident), cover(book.ident))
    rescue
    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

#transportObject



441
442
443
444
# File 'lib/alexandria/models/library.rb', line 441

def transport
  config = Alexandria::Preferences.instance.http_proxy_config
  config ? Net::HTTP.Proxy(*config) : Net::HTTP
end

#undelete(book = nil) ⇒ Object



514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# File 'lib/alexandria/models/library.rb', line 514

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?Boolean

Returns:

  • (Boolean)


50
51
52
# File 'lib/alexandria/models/library.rb', line 50

def updating?
  @updating
end

#yaml(something, basedir = path) ⇒ Object



561
562
563
564
565
566
567
568
569
570
571
572
573
# File 'lib/alexandria/models/library.rb', line 561

def yaml(something, basedir = path)
  ident = case something
          when Book
            something.ident
          when String
            something
          when Integer
            something
          else
            raise "#{something} is #{something.class}"
          end
  File.join(basedir, ident.to_s + EXT[:book])
end