Skip to main content

send_to_kindle/
lib.rs

1#![doc(
2    html_logo_url = "https://raw.githubusercontent.com/gstavrinos/send-to-kindle/master/media/send-to-kindle256.png", 
3    html_favicon_url = "https://raw.githubusercontent.com/gstavrinos/send-to-kindle/master/media/send-to-kindle128.png"
4)]
5
6//!
7//! # Send to Kindle
8//!
9//! send-to-kindle is a command-line utility and rust library for sending files to your kindle app
10//! or devices by (ab)using the www.amazon.com/sendtokindle web interface.
11//!
12//! **For this reason, it should be used with caution. Getting suspended by Amazon's spam
13//! prevention systems is always a possibility. USE send-to-kindle AT YOUR OWN RISK!**
14//!
15//! # Command-line tool basic usage
16//!
17//! ```
18//! cargo run -- --username <username> --password <password> --directory <path_to_books>
19//! --extension epub
20//! ```
21//!
22//! The `--directory` flag can be swapped with the `--file` flag to just send a single file. If an
23//! extension is provided, it will ensure that the selected file has the requested extension.
24//! 
25//! For more info on the command-line utility and flags for corner cases, use the `--help` flag.
26//!
27//! # Library usage
28//!
29//! Just two functions are provided: One for a list of strings representing the files to be
30//! uploaded (<a href="fn.send_files_to_kindle.html">send_files_to_kindle</a>), and one for a path to a file or directory that can be filtered using a string
31//! for the files' extension (<a href="fn.send_to_kindle.html">send_to_kindle</a>). (epub, azw3, mobi etc).
32//!
33//! For more info, click on each function's definition and read the extensive documentation there.
34//!
35//!
36
37use thirtyfour::prelude::{WebDriverResult, By};
38use thirtyfour::{FirefoxCapabilities, WebDriver};
39use thirtyfour::common::capabilities::firefox::FirefoxPreferences;
40
41#[derive(clap::Parser, Debug)]
42#[command(author, version, about, long_about = None)]
43pub struct Args {
44   /// Username of the amazon account connected to kindle (app or device)
45   #[arg(short, long)]
46   pub username: String,
47
48   /// Password of the amazon account connected to kindle (app or device)
49   #[arg(short, long)]
50   pub password: String,
51
52   /// The single file that will be sent to kindle. Ignored if a directory is also specified
53   #[arg(short, long, default_value_t = String::from(""))]
54   pub file: String,
55
56   /// The directory from which all files will be sent to kindle
57   #[arg(short, long, default_value_t = String::from(""))]
58   pub directory: String,
59
60   /// Only the file(s) with this extension (e.g. epub) will be sent to kindle
61   #[arg(short, long, default_value_t = String::from(""))]
62   pub extension: String,
63
64   /// Seconds to wait to upload per file, before giving up.
65   #[arg(long, default_value_t = 60)]
66   pub file_timeout: usize,
67
68   /// Run the geckodriver daemon
69   #[arg(long, default_value_t = true)]
70   pub geckodriver_daemon: bool,
71
72   /// Enable debugging mode, that runs the browser with GUI, does not automatically send the files and prompts for user input in terminal to close the window. Only for development purposes.
73   #[arg(long, default_value_t = false)]
74   pub debugging_mode: bool,
75
76   /// Bypass the amazon(.com) url in case you need something else (e.g. .jp). You most probably won't need to use this.
77   #[arg(long, default_value_t = String::from("https://www.amazon.com/sendtokindle"))]
78   pub amazon_url: String,
79
80}
81
82impl Default for Args {
83    fn default() -> Args {
84        Args {
85            username: String::from(""),
86            password: String::from(""),
87            file: String::from(""),
88            directory: String::from(""),
89            extension: String::from(""),
90            file_timeout: 60,
91            geckodriver_daemon: true,
92            debugging_mode: false,
93            amazon_url: String::from("https://www.amazon.com/sendtokindle"),
94        }
95    }
96}
97
98impl Args {
99    pub fn new(u: &str, p: &str) -> Args {
100        let mut args = Args::default();
101        args.username = String::from(u);
102        args.password = String::from(p);
103        return args;
104    }
105}
106
107/// * **Parameters:**
108///
109///     - username: The amazon username as str
110///     - password: The amazon password as str
111///     - files: A list of files to be uploaded as String Vec
112///     - file_timeout: Seconds assigned to each file before uploading times out
113///     - url: The amazon url to be used
114///     - daemon: Whether to run the geckodriver daemon or not. If not, it will have to be run externally
115///     - debugging_mode: For development purposes, enables GUI on the browser, does not send the files and prompts for user input to end the process
116/// * **Description:** Send the _files_ to the kindle app and devices associated with the _username_ and _password_ amazon account
117///
118/// * **Notes:** It is recommended to use the Args::default or Args::new functions to ensure some sane defaults and easier setup. Please note that the _files_ vector of strings is not included in the Args struct and it is expected to be constructed separately.
119pub async fn send_files_to_kindle(username: &str, password: &str, files: Vec<String>, file_timeout: usize, url: &str, daemon: bool, debugging_mode: bool) -> WebDriverResult<()> {
120    let mut gd_daemon = std::process::Command::new("echo").stdin(std::process::Stdio::null()).stdout(std::process::Stdio::null()).spawn()?;
121    if daemon {
122        gd_daemon = std::process::Command::new("geckodriver").stdin(std::process::Stdio::null()).stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn()?;
123    }
124    let user_agent = "Linux";
125
126    let mut prefs = FirefoxPreferences::new();
127    prefs.set_user_agent(user_agent.to_string())?;
128
129    let mut caps = FirefoxCapabilities::new();
130    caps.set_preferences(prefs)?;
131    if !debugging_mode {
132        caps.set_headless()?;
133    }
134
135    let driver = WebDriver::new("http://localhost:4444", caps).await?;
136    driver.goto(url).await?;
137    println!("Reached {}", url);
138
139    let signin_button = driver.find(By::Id("s2k-dnd-sign-in-button")).await?;
140    signin_button.click().await?;
141    println!("Found sign in button");
142
143    let email_input = driver.find(By::Css("input[type='email']")).await?;
144    email_input.send_keys(username).await?;
145    println!("Found email input and sent user email");
146    let continue_button = driver.find(By::Css("input[type='submit'][id='continue']")).await?;
147    continue_button.click().await?;
148    println!("Found and clicked continue button");
149    let password_input = driver.find(By::Css("input[type='password'][id='ap_password']")).await?;
150    password_input.send_keys(password).await?;
151    println!("Found password input and sent user password");
152    let sis_button = driver.find(By::Css("input[type='submit'][id='signInSubmit']")).await?;
153    sis_button.click().await?;
154    println!("Found and clicked sign in button");
155    driver.execute(r#"
156
157        let elem = document.getElementById("s2k-home-wrapper");
158        
159        var input = document.createElement("input");
160        input.id = "hacky-file-input";
161        input.type = "file";
162        input.multiple = true;
163        elem.appendChild(input);
164    "#, Vec::new()).await?;
165
166    let file_input = driver.find(By::Id("hacky-file-input")).await?;
167    for f in files.clone() {
168        file_input.send_keys(f).await?;
169    }
170    driver.execute(r#"
171        let elem = document.getElementById("s2k-home-wrapper");
172
173        function CustomDataTransfer() {
174          var f = document.getElementById("hacky-file-input").files;
175          this.dropEffect = 'all';
176          this.effectAllowed = 'all';
177          this.items = [];
178          this.types = ['Files'];
179          this.files = f;
180        };
181
182        var customDropEvent = new DragEvent('drop');
183        Object.defineProperty(customDropEvent, 'dataTransfer', {
184          value: new CustomDataTransfer()
185        });
186        var button_input = document.createElement("button");
187        button_input.id = "hacky-button-file-input";
188        button_input.addEventListener('click', function(e) {
189            e.preventDefault();
190
191            // the fake event will be called on the button click
192            document.getElementById("s2k-dnd-area").dispatchEvent(customDropEvent);
193          });
194        elem.appendChild(button_input); // put it into the DOM
195
196        "#, Vec::new()
197        ).await?;
198    let dnd_area = driver.find(By::Id("s2k-dnd-area")).await?;
199    if !dnd_area.is_displayed().await? {
200        println!("Something's off with the dnd area...");
201    }
202    else {
203        println!("Found dnd area, we are ready to send some files!");
204    }
205    let file_input_button = driver.find(By::Id("hacky-button-file-input")).await?;
206    file_input_button.click().await?;
207    driver.find(By::Css(".s2k-r2s-file-item")).await?;
208    println!("Found at least one item in the dnd area");
209    let add_to_library_label = driver.find(By::Id("s2k-r2s-add2lib")).await?;
210    let add_to_library_checkbox = add_to_library_label.find(By::Css("input[type='checkbox']")).await?;
211    if add_to_library_checkbox.prop("checked").await?.unwrap() != "true" {
212        add_to_library_label.click().await?;
213    }
214    println!("Found the 'Add to your library' checkbox, and ensured that it is checked");
215    if !debugging_mode {
216        let start = std::time::Instant::now();
217        let send_button = driver.find(By::Id("s2k-r2s-send-button")).await?;
218        send_button.click().await?;
219        println!("Found and clicked the send button");
220        let mut uploading = true;
221        println!("Waiting for files to upload...");
222        while uploading {
223            uploading = !dnd_area.is_displayed().await?;
224            if start.elapsed().as_secs() as usize > files.clone().len() * file_timeout {
225                println!("Waited for more than {} seconds per file and still appears that not everything was done. Giving up. (If your connection is slow, try increasing the --file-timeout argument)", file_timeout);
226                break;
227            }
228            std::thread::sleep(std::time::Duration::from_secs(3));
229            print!(".");
230        }
231        if !uploading {
232            println!("\nEverything uploaded successfully! :)");
233        }
234    }
235    else {
236        println!("DEBUGGING MODE: Press enter to close the browser window...");
237        let mut _s = String::new();
238        std::io::stdin().read_line(&mut _s)?;
239    }
240    driver.close_window().await?;
241    if daemon {
242        gd_daemon.kill()?;
243    }
244    Ok(())
245}
246
247/// * **Parameters:**
248///
249///     - username: The amazon username as str
250///     - password: The amazon password as str
251///     - f: A path to a file or directory to be uploaded as str
252///     - ext: A file format extension to filter the files to be uploaded as str
253///     - file_timeout: Seconds assigned to each file before uploading times out
254///     - url: The amazon url to be used
255///     - daemon: Whether to run the geckodriver daemon or not. If not, it will have to be run externally
256///     - debugging_mode: For development purposes, enables GUI on the browser, does not send the files and prompts for user input to end the process
257/// * **Description:** Send the _files_ to the kindle app and devices associated with the _username_ and _password_ amazon account
258///
259/// * **Notes:** It is recommended to use the Args::default or Args::new functions to ensure some sane defaults and easier setup. Please note that the _f_ str should be selected either from the file or directory field of the Args struct. _f_ is filtered based on the _ext_ parameter and the a vector of strings is constructed and passed to _send_files_to_kindle_
260pub async fn send_to_kindle(username: &str, password: &str, f: &str, ext: &str, file_timeout: usize, url: &str, daemon: bool, debugging_mode: bool) -> WebDriverResult<()> {
261    let mut files = Vec::<String>::new();
262    let source = std::path::Path::new(f);
263    if source.is_file() {
264        if ext == "" || ext == source.extension().unwrap_or(std::ffi::OsStr::new("")) {
265            files.push(String::from(f));
266        }
267    }
268    else if source.is_dir() {
269        for file in std::fs::read_dir(f).unwrap() {
270            let file_path = file.unwrap().path();
271            if file_path.ends_with(ext) {
272                files.push(file_path.display().to_string());
273            }
274        }
275    }
276    send_files_to_kindle(username, password, files, file_timeout, url, daemon, debugging_mode).await
277}
278