kindle-download
change
commit c6d46544b777b214eaed113664abd725b006f807
author Alpha Chen <alpha@kejadlen.dev>
date
parent 7cbdd5f3
diff --git a/kindle-download/Gemfile b/kindle-download/Gemfile
new file mode 100644
index 0000000..720b63d
--- /dev/null
+++ b/kindle-download/Gemfile
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "capybara"
+gem "cuprite", "~> 0.15"
+gem "debug"
+gem "ferrum"
+gem "logger"
diff --git a/kindle-download/Gemfile.lock b/kindle-download/Gemfile.lock
new file mode 100644
index 0000000..8ff00cb
--- /dev/null
+++ b/kindle-download/Gemfile.lock
@@ -0,0 +1,96 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    addressable (2.8.7)
+      public_suffix (>= 2.0.2, < 7.0)
+    base64 (0.2.0)
+    capybara (3.40.0)
+      addressable
+      matrix
+      mini_mime (>= 0.1.3)
+      nokogiri (~> 1.11)
+      rack (>= 1.6.0)
+      rack-test (>= 0.6.3)
+      regexp_parser (>= 1.5, < 3.0)
+      xpath (~> 3.2)
+    concurrent-ruby (1.3.5)
+    cuprite (0.15.1)
+      capybara (~> 3.0)
+      ferrum (~> 0.15.0)
+    date (3.4.1)
+    debug (1.10.0)
+      irb (~> 1.10)
+      reline (>= 0.3.8)
+    ferrum (0.15)
+      addressable (~> 2.5)
+      concurrent-ruby (~> 1.1)
+      webrick (~> 1.7)
+      websocket-driver (~> 0.7)
+    io-console (0.8.0)
+    irb (1.15.1)
+      pp (>= 0.6.0)
+      rdoc (>= 4.0.0)
+      reline (>= 0.4.2)
+    logger (1.6.6)
+    matrix (0.4.2)
+    mini_mime (1.1.5)
+    nokogiri (1.18.2-aarch64-linux-gnu)
+      racc (~> 1.4)
+    nokogiri (1.18.2-aarch64-linux-musl)
+      racc (~> 1.4)
+    nokogiri (1.18.2-arm-linux-gnu)
+      racc (~> 1.4)
+    nokogiri (1.18.2-arm-linux-musl)
+      racc (~> 1.4)
+    nokogiri (1.18.2-arm64-darwin)
+      racc (~> 1.4)
+    nokogiri (1.18.2-x86_64-darwin)
+      racc (~> 1.4)
+    nokogiri (1.18.2-x86_64-linux-gnu)
+      racc (~> 1.4)
+    nokogiri (1.18.2-x86_64-linux-musl)
+      racc (~> 1.4)
+    pp (0.6.2)
+      prettyprint
+    prettyprint (0.2.0)
+    psych (5.2.3)
+      date
+      stringio
+    public_suffix (6.0.1)
+    racc (1.8.1)
+    rack (3.1.10)
+    rack-test (2.2.0)
+      rack (>= 1.3)
+    rdoc (6.12.0)
+      psych (>= 4.0.0)
+    regexp_parser (2.10.0)
+    reline (0.6.0)
+      io-console (~> 0.5)
+    stringio (3.1.3)
+    webrick (1.9.1)
+    websocket-driver (0.7.7)
+      base64
+      websocket-extensions (>= 0.1.0)
+    websocket-extensions (0.1.5)
+    xpath (3.2.0)
+      nokogiri (~> 1.8)
+
+PLATFORMS
+  aarch64-linux-gnu
+  aarch64-linux-musl
+  arm-linux-gnu
+  arm-linux-musl
+  arm64-darwin
+  x86_64-darwin
+  x86_64-linux-gnu
+  x86_64-linux-musl
+
+DEPENDENCIES
+  capybara
+  cuprite (~> 0.15)
+  debug
+  ferrum
+  logger
+
+BUNDLED WITH
+   2.6.2
diff --git a/kindle-download/kindle-download.rb b/kindle-download/kindle-download.rb
new file mode 100755
index 0000000..388f1c7
--- /dev/null
+++ b/kindle-download/kindle-download.rb
@@ -0,0 +1,98 @@
+#!/usr/bin/env ruby
+
+require "logger"
+require "net/http"
+
+require "capybara/cuprite"
+Capybara.javascript_driver = :cuprite
+Capybara.register_driver(:cuprite) do |app|
+  Capybara::Cuprite::Driver.new(app, headless: false)
+end
+Capybara.current_driver = :cuprite
+
+require "capybara/dsl"
+original_verbosity = $VERBOSE
+$VERBOSE = nil
+include Capybara::DSL
+$VERBOSE = original_verbosity
+
+Book = Data.define(:id, :title)
+
+AMAZON_EMAIL = ENV.fetch("AMAZON_EMAIL")
+AMAZON_PASSWORD = ENV.fetch("AMAZON_PASSWORD")
+
+def wait_for_download
+  already_downloaded = Dir[File.expand_path("~/Downloads/*.azw*")]
+  yield
+  loop do
+    current = Dir[File.expand_path("~/Downloads/*.azw*")]
+    new_book, *rest = current - already_downloaded
+    if new_book.nil?
+      sleep 0.1
+    else
+      fail unless rest.empty?
+      return new_book
+    end
+  end
+end
+
+log = Logger.new(STDOUT)
+
+url = "https://www.amazon.com/hz/mycd/digital-console/contentlist/booksPurchases/dateDsc"
+visit url
+
+fill_in "email", with: AMAZON_EMAIL
+fill_in "password", with: AMAZON_PASSWORD
+find("#signInSubmit").click
+
+fill_in "otpCode", with: `op item get --otp ngcal7ifttkh4lbd5ww6slde4q`
+click_on "mfaSubmit"
+
+# I could use pstore here or something, but this is
+# pretty damn convenient
+files = Dir[File.expand_path("~/Downloads/*.azw*")]
+ids = files.map {
+  `xattr -p com.apple.metadata:kMDItemWhereFroms "#{it}"`.chomp
+}
+log.info("Already downloaded: #{files.zip(ids)}")
+
+# this could be smarter, but I am lazy
+(1..6).each do |page_number|
+  url = "https://www.amazon.com/hz/mycd/digital-console/contentlist/booksPurchases/dateDsc?pageNumber=#{page_number}"
+  visit url
+
+  books = all(".digital_entity_title").map {
+    Book.new(id: it["id"].split(?-).last, title: it.text)
+  }.map.with_index.reject {|book, _| ids.include?(book.id) }
+
+  log.info("Downloading: #{books.map(&:first).map(&:title)}")
+
+  books.each do |book, i|
+    # kindle and download don't seem to ever be
+    # visible, so do this awkward dance to actually
+    # associate them to the right book
+    more_actions, kindle, download = all(:xpath, "//span[text()='More actions']").zip(
+      all(:xpath, "//input[@name='actionListRadioButton']", visible: false),
+      all(
+        :xpath,
+        "//div[starts-with(@id, 'DOWNLOAD_AND_TRANSFER_ACTION_') and contains(@id, '_CONFIRM')]",
+        visible: false,
+      ),
+    ).fetch(i)
+
+    more_actions.base.node.scroll_into_view
+    more_actions.click
+    find(:xpath, "//span[text()='Download & transfer via USB']").click
+    kindle.click
+
+    log.info("Downloading #{book.title} (#{book.id})")
+
+    # I'd use page.driver.browser.page.downloads.wait
+    # here, but it (and also .files) don't seem to
+    # work quite right
+    downloaded = wait_for_download { download.click }
+    `xattr -w com.apple.metadata:kMDItemWhereFroms #{book.id} "#{downloaded}"`
+
+    find("#notification-close").click
+  end
+end