|
|
@@ -0,0 +1,176 @@ |
|
|
class Book < Airrecord::Table |
|
|
class Endorser < Airrecord::Table |
|
|
self.base_key = "" |
|
|
self.table_name = "Endorser" |
|
|
end |
|
|
|
|
|
self.base_key = "" |
|
|
self.table_name = "Books" |
|
|
|
|
|
has_many :endorsements, class: 'Book::Endorser', column: 'Endorsements' |
|
|
|
|
|
GOODREADS_BLACKLIST = %w( |
|
|
to-read favorites currently-reading owned |
|
|
series favourites re-read owned-books |
|
|
books-i-own wish-list si audiobook |
|
|
book-club ebook kindle to-buy |
|
|
) |
|
|
|
|
|
GOODREADS_MERGE = { |
|
|
"Non-fiction" => "Nonfiction", |
|
|
"Classic" => "Classics", |
|
|
"Cookbook" => "Cooking", |
|
|
"Cookbooks" => "Cooking", |
|
|
"Biography" => "Memoir", |
|
|
"Biographies" => "Memoir", |
|
|
"Autobiography" => "Memoir", |
|
|
"Auto-biography" => "Memoir", |
|
|
"Sci-fi" => "Science Fiction", |
|
|
"Scifi" => "Science Fiction", |
|
|
"Management" => "Leadership", |
|
|
"Self-help" => "Personal Development", |
|
|
"Selfhelp" => "Personal Development", |
|
|
"Personal-development" => "Personal Development", |
|
|
"Self-improvement" => "Personal Development", |
|
|
"Science-fiction" => "Science Fiction", |
|
|
"Ya" => "Young-adult", |
|
|
"Tech" => "Technology", |
|
|
"Young-adult" => "Young Adult", |
|
|
"Computer-science" => "Programming", |
|
|
"Investing" => "Economics", |
|
|
"Fitness" => "Health", |
|
|
"Food" => "Cooking", |
|
|
"Finance" => "Economics", |
|
|
"Software" => "Programming", |
|
|
"Literature" => "Classics", |
|
|
} |
|
|
|
|
|
CATEGORIES = [ |
|
|
"Business", "Psychology", "Science", "Personal Development", "Philosophy", |
|
|
"History", "Fiction", "Memoir", "Leadership", "Classics", "Economics", |
|
|
"Cooking", "Programming", "Health", "Politics", "Technology", "Science Fiction", |
|
|
"Entrepreneurship", "Design", "Writing", "Fantasy", "Young Adult", "Nonfiction", |
|
|
] |
|
|
|
|
|
def goodreads_id |
|
|
query = self["ISBN"] if self["ISBN"] |
|
|
query ||= "\"#{self[:title]}\"" |
|
|
|
|
|
search = goodreads_client.search_books(query) |
|
|
if search.results.respond_to?(:work) |
|
|
matches = [search.results.work].flatten |
|
|
|
|
|
if self[:author] |
|
|
best_match = matches.find { |match| |
|
|
character_difference?(match["best_book"]["author"]["name"], self[:author]) |
|
|
} |
|
|
end |
|
|
|
|
|
best_match ||= matches.first |
|
|
return unless best_match |
|
|
best_match.best_book.id |
|
|
end |
|
|
end |
|
|
|
|
|
def goodreads_book |
|
|
@book ||= begin |
|
|
id = goodreads_id |
|
|
return unless id |
|
|
goodreads_client.book(id) |
|
|
end |
|
|
end |
|
|
|
|
|
def goodreads_categories(n = 5) |
|
|
popular = goodreads_book.popular_shelves |
|
|
return [] if popular.blank? |
|
|
|
|
|
shelves = popular.shelf |
|
|
return [] unless shelves.first.respond_to?(:name) |
|
|
|
|
|
shelves.map(&:name).reject { |name| |
|
|
GOODREADS_BLACKLIST.include?(name) |
|
|
}.first(n).map { |name| |
|
|
name = name.capitalize |
|
|
name = GOODREADS_MERGE[name] if GOODREADS_MERGE[name] |
|
|
(CATEGORIES.include?(name) && name) || nil |
|
|
}.compact.uniq |
|
|
end |
|
|
|
|
|
def populate_from_goodreads(prevent_duplicates_from: []) |
|
|
book = goodreads_book |
|
|
|
|
|
unless book |
|
|
$stderr.puts "Unable to find book #{self["Title"]}" |
|
|
return |
|
|
end |
|
|
|
|
|
before = self.serializable_fields |
|
|
self["Title"] = book.title |
|
|
self["ISBN"] = book.isbn13 || self["ISBN"] |
|
|
self["Publication Year"] = book.work.original_publication_year.to_s || book.publication_year.to_s |
|
|
self["Goodreads Rating"] = book.average_rating |
|
|
self["Pages"] = book.num_pages |
|
|
authors = [book.authors.author].flatten |
|
|
self["Author"] = authors.first.name |
|
|
self["Categories"] = goodreads_categories.sort |
|
|
self["Goodreads Ratings"] = book.work.ratings_count |
|
|
|
|
|
difference = HashDiff.diff(before, self.serializable_fields) |
|
|
|
|
|
flagged = false |
|
|
author_ok = true |
|
|
|
|
|
$stderr.puts "\x1b[35m#{before["Title"]}\x1b[0m" |
|
|
difference.each do |(type, key, prev, new)| |
|
|
if key == "Author" && type == "~" |
|
|
unless authors.any? { |author| character_difference?(author.name, prev) } |
|
|
$stderr.puts "Author changed too much" |
|
|
flagged = true |
|
|
author_ok = false |
|
|
end |
|
|
end |
|
|
|
|
|
if key == "Title" && type == "~" |
|
|
unless new.downcase.start_with?(prev.downcase) || author_ok |
|
|
$stderr.puts "New title '#{new}' didn't start with old title '#{prev}'" |
|
|
flagged = true |
|
|
end |
|
|
end |
|
|
|
|
|
if type == "~" |
|
|
$stderr.puts "\x1b[34m#{type} #{key}: \x1b[31m#{prev} => \x1b[32m#{new}\x1b[0m" |
|
|
elsif type == "+" |
|
|
$stderr.puts "\x1b[34m#{type} #{key}: \x1b[32m#{prev}\x1b[0m" |
|
|
end |
|
|
end |
|
|
|
|
|
|
|
|
if flagged |
|
|
Rollbar.warn("Skipping book", title: self[:title]) |
|
|
elsif prevent_duplicates_from.find { |other| other["ISBN"] == self["ISBN"] } |
|
|
$stderr.puts "Skipping #{self[:title]} due to duplicate" |
|
|
else |
|
|
if self.new_record? |
|
|
self.create |
|
|
else |
|
|
self.save |
|
|
end |
|
|
end |
|
|
end |
|
|
|
|
|
private |
|
|
|
|
|
def goodreads_client |
|
|
self.class.goodreads_client |
|
|
end |
|
|
|
|
|
def self.goodreads_client |
|
|
@client ||= begin |
|
|
Goodreads::Client.new(api_key: '', api_secret: '') |
|
|
end |
|
|
end |
|
|
|
|
|
def character_difference?(a, b, n = 4) |
|
|
(a.split('') - b.split('')).size <= n && (b.split('') - a.split('')).size <= n |
|
|
end |
|
|
end |