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