1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
#!/usr/bin/env ruby

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "capybara"
  gem "cuprite", "~> 0.15" # not sure why I need this pin
  gem "debug"
  gem "ferrum"
  gem "logger"
end

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 downloaded_books
  Dir[File.expand_path("~/Downloads/*")]
    .select { it =~ /\.(azw3?|tpz)$/ } # tpz?!
end

def wait_for_download
  already_downloaded = downloaded_books
  yield
  loop do
    current = downloaded_books
    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 = downloaded_books
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