├── .dockerignore ├── .cargo └── config ├── Cross.toml ├── .gitignore ├── Dockerfile ├── rustfmt.toml ├── .github └── workflows │ ├── docker.yml │ └── rust.yml ├── Cargo.toml ├── LICENSE-MIT ├── rpizerow.Dockerfile ├── src ├── errors.rs ├── auth.rs ├── utils.rs ├── user.rs ├── structures.rs ├── main.rs └── download.rs ├── README.md ├── LICENSE-APACHE └── Cargo.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | data 2 | target 3 | Dockerfile 4 | .dockerignore 5 | .git 6 | .gitignore 7 | -------------------------------------------------------------------------------- /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.arm-unknown-linux-gnueabi] 2 | linker = "arm-bcm2708-linux-gnueabi-cc" 3 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.arm-unknown-linux-gnueabi] 2 | image = "rust-rpi-zerow:v1-openssl" 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.env 3 | .idea/ 4 | .DS_Store 5 | data/ 6 | reddsaver/ 7 | *.iml 8 | .envrc 9 | mock/ 10 | test-data/ 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.50.0 2 | WORKDIR /usr/src 3 | 4 | RUN USER=root cargo new reddsaver 5 | 6 | WORKDIR /usr/src/reddsaver 7 | COPY Cargo.toml Cargo.lock ./ 8 | RUN cargo fetch 9 | 10 | COPY src ./src 11 | RUN cargo build --release 12 | RUN mkdir -pv /app 13 | RUN cp ./target/release/reddsaver /app/reddsaver 14 | 15 | WORKDIR /app 16 | CMD ["./reddsaver"] 17 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | # The "Default" setting has a heuristic which splits lines too aggresively. 3 | use_small_heuristics = "Max" 4 | # Prevent carriage returns 5 | newline_style = "Unix" 6 | # Reorder imports 7 | reorder_imports = true 8 | # Where to put binary operators in the line 9 | binop_separator = "Front" 10 | # Wrap comments into next line 11 | wrap_comments = true 12 | # Format string literals where necessary 13 | format_strings = true 14 | # Item layout inside a imports block 15 | imports_layout = "HorizontalVertical" 16 | # Indent on expressions or items. 17 | indent_style = "Block" 18 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | main: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - 11 | name: Set up QEMU 12 | uses: docker/setup-qemu-action@v1 13 | - 14 | name: Set up Docker Buildx 15 | uses: docker/setup-buildx-action@v1 16 | - 17 | name: Login to DockerHub 18 | uses: docker/login-action@v1 19 | with: 20 | username: ${{ secrets.DOCKERHUB_USERNAME }} 21 | password: ${{ secrets.DOCKERHUB_TOKEN }} 22 | - 23 | name: Build and push 24 | id: docker_build 25 | uses: docker/build-push-action@v2 26 | with: 27 | push: true 28 | tags: manojkarthick/reddsaver:latest 29 | - 30 | name: Image digest 31 | run: echo ${{ steps.docker_build.outputs.digest }} 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reddsaver" 3 | version = "0.4.0" 4 | authors = ["Manoj Karthick Selva Kumar "] 5 | description = "CLI tool to download saved media from Reddit" 6 | edition = "2018" 7 | license = "MIT/Apache-2.0" 8 | readme = "README.md" 9 | homepage = "https://github.com/manojkarthick/reddsaver" 10 | repository = "https://github.com/manojkarthick/reddsaver" 11 | keywords = ["cli", "reddit", "images"] 12 | categories = ["command-line-utilities"] 13 | 14 | [dependencies] 15 | reqwest = { version = "0.10", features = ["json"] } 16 | tokio = { version = "0.2", features = ["full"] } 17 | base64 = "0.13.0" 18 | serde = { version = "1.0", features = ["derive"] } 19 | serde_json = "1.0" 20 | dotenv = "0.15.0" 21 | md5 = "0.7.0" 22 | thiserror = "1.0.22" 23 | log = "0.4.11" 24 | env_logger = "0.8.2" 25 | futures = "0.3.8" 26 | rand = "0.7.3" 27 | random-names = "0.1.3" 28 | clap = "2.33.3" 29 | url = "2.2.0" 30 | tempfile = "3.2.0" 31 | which = "4.2.2" 32 | mime = "0.3.16" 33 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Manoj Karthick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rpizerow.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustembedded/cross:arm-unknown-linux-gnueabi 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | ENV PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabi/pkgconfig 4 | ENV RPI_TOOLS=/rpi_tools 5 | ENV MACHINE=armv6 6 | ENV ARCH=armv6 7 | ENV CC=gcc 8 | ENV OPENSSL_DIR=/openssl 9 | ENV INSTALL_DIR=/openssl 10 | ENV CROSSCOMP_DIR=/rpi_tools/arm-bcm2708/arm-bcm2708-linux-gnueabi/bin 11 | 12 | RUN apt-get update &&\ 13 | apt-get install -y wget openssl libssl-dev pkg-config libudev-dev lib32z1 14 | 15 | # Get Raspberry Pi cross-compiler tools 16 | RUN git -C "/" clone -q --depth=1 https://github.com/raspberrypi/tools.git "${RPI_TOOLS}" 17 | 18 | # Manually cross-compile OpenSSL to link with 19 | 20 | # 1) Download OpenSSL 1.1.0 21 | RUN mkdir -p $OPENSSL_DIR 22 | RUN cd /tmp && \ 23 | wget --no-check-certificate https://www.openssl.org/source/openssl-1.1.0h.tar.gz && \ 24 | tar xzf openssl-1.1.0h.tar.gz 25 | 26 | # 2) Compile 27 | RUN cd /tmp/openssl-1.1.0h && \ 28 | ./Configure linux-generic32 shared \ 29 | --prefix=$INSTALL_DIR --openssldir=$INSTALL_DIR/openssl \ 30 | --cross-compile-prefix=$CROSSCOMP_DIR/arm-bcm2708-linux-gnueabi- && \ 31 | make depend && make && make install 32 | 33 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use mime::FromStrError; 2 | use reqwest::header::ToStrError; 3 | use thiserror::Error; 4 | 5 | #[allow(dead_code)] 6 | #[derive(Error, Debug)] 7 | pub enum ReddSaverError { 8 | #[error("Missing environment variable")] 9 | EnvVarNotPresent(#[from] std::env::VarError), 10 | #[error("Unable to process request")] 11 | ReqwestError(#[from] reqwest::Error), 12 | #[error("Could not create directory")] 13 | CouldNotCreateDirectory, 14 | #[error("Could not save image `{0}` to filesystem")] 15 | CouldNotSaveImageError(String), 16 | #[error("Could not create image `{0}` from `{1}`")] 17 | CouldNotCreateImageError(String, String), 18 | #[error("Unable to join tasks")] 19 | TokioJoinError(#[from] tokio::task::JoinError), 20 | #[error("Could not save string to int")] 21 | ParsingIntError(#[from] std::num::ParseIntError), 22 | #[error("Could not save usize to int")] 23 | TryFromIntError(#[from] std::num::TryFromIntError), 24 | #[error("Data directory not found, please check if it exists")] 25 | DataDirNotFound, 26 | #[error("Could not create or save image")] 27 | IoError(#[from] std::io::Error), 28 | #[error("Unable to parse URL")] 29 | UrlError(#[from] url::ParseError), 30 | #[error("Could not convert to string")] 31 | ToStringConversionError(#[from] ToStrError), 32 | #[error("Could not convert from string")] 33 | FromStringConversionError(#[from] FromStrError), 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | job_on_push: 7 | runs-on: ubuntu-latest 8 | if: "!contains(github.event.head_commit.message, 'skip ci')" 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v1 12 | 13 | - name: Install latest rust toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | default: true 18 | override: true 19 | 20 | - name: Check compilation 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: check 24 | 25 | publish: 26 | if: startsWith(github.ref, 'refs/tags/') 27 | name: Publish for ${{ matrix.os }} 28 | runs-on: ${{ matrix.os }} 29 | strategy: 30 | matrix: 31 | name: [ 32 | linux, 33 | windows, 34 | macos 35 | ] 36 | 37 | include: 38 | - name: linux 39 | os: ubuntu-latest 40 | artifact_name: target/release/reddsaver 41 | asset_name: reddsaver-linux-amd64 42 | - name: windows 43 | os: windows-latest 44 | artifact_name: target/release/reddsaver.exe 45 | asset_name: reddsaver-windows-amd64 46 | - name: macos 47 | os: macos-latest 48 | artifact_name: target/release/reddsaver 49 | asset_name: reddsaver-macos-amd64 50 | 51 | steps: 52 | - name: Checkout repository 53 | uses: actions/checkout@v1 54 | 55 | - name: Install latest rust toolchain 56 | uses: actions-rs/toolchain@v1 57 | with: 58 | profile: minimal 59 | toolchain: stable 60 | 61 | - name: Build 62 | run: cargo build --release --locked 63 | 64 | - name: Upload binaries to release 65 | uses: svenstaro/upload-release-action@v2 66 | with: 67 | repo_token: ${{ secrets.GITHUB_TOKEN }} 68 | file: ${{ matrix.artifact_name }} 69 | asset_name: ${{ matrix.asset_name }} 70 | tag: ${{ github.ref }} 71 | overwrite: true 72 | -------------------------------------------------------------------------------- /src/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::ReddSaverError; 2 | 3 | use log::debug; 4 | use reqwest::header::{AUTHORIZATION, USER_AGENT}; 5 | use serde::{Deserialize, Serialize}; 6 | use std::collections::HashMap; 7 | 8 | /// To generate the Reddit Client ID and secret, go to reddit [preferences](https://www.reddit.com/prefs/apps) 9 | pub struct Client<'a> { 10 | /// Client ID for the application 11 | client_id: &'a str, 12 | /// Client Secret for the application 13 | client_secret: &'a str, 14 | /// Login username 15 | username: &'a str, 16 | /// Login password 17 | password: &'a str, 18 | /// Unique User agent string 19 | user_agent: &'a str, 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Debug)] 23 | pub struct Auth { 24 | /// The generated bearer access token for the application 25 | pub access_token: String, 26 | /// Type of access token: "bearer" 27 | token_type: String, 28 | /// Expiry duration. Defaults to 3600/1 hour 29 | expires_in: i32, 30 | /// Scope of the access token. This app requires * scope 31 | scope: String, 32 | } 33 | 34 | impl<'a> Client<'a> { 35 | pub fn new( 36 | id: &'a str, 37 | secret: &'a str, 38 | username: &'a str, 39 | password: &'a str, 40 | agent: &'a str, 41 | ) -> Self { 42 | Self { 43 | client_id: &id, 44 | client_secret: &secret, 45 | username: &username, 46 | password: &password, 47 | user_agent: &agent, 48 | } 49 | } 50 | 51 | pub async fn login(&self) -> Result { 52 | let basic_token = base64::encode(format!("{}:{}", self.client_id, self.client_secret)); 53 | let grant_type = String::from("password"); 54 | 55 | let mut body = HashMap::new(); 56 | body.insert("username", self.username); 57 | body.insert("password", self.password); 58 | body.insert("grant_type", &grant_type); 59 | 60 | let client = reqwest::Client::new(); 61 | let auth = client 62 | .post("https://www.reddit.com/api/v1/access_token") 63 | .header(USER_AGENT, self.user_agent) 64 | // base64 encoded : should be sent as a basic token 65 | // along with the body of the message 66 | .header(AUTHORIZATION, format!("Basic {}", basic_token)) 67 | // make sure the username and password is sent as form encoded values 68 | // the API does not accept JSON body when trying to obtain a bearer token 69 | .form(&body) 70 | .send() 71 | .await? 72 | .json::() 73 | .await?; 74 | 75 | debug!("Access token is: {}", auth.access_token); 76 | Ok(auth) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::ReddSaverError; 2 | use mime::Mime; 3 | use rand::Rng; 4 | use random_names::RandomName; 5 | use reqwest::header::CONTENT_TYPE; 6 | use std::path::Path; 7 | use std::str::FromStr; 8 | use which::which; 9 | 10 | /// Generate user agent string of the form :. 11 | /// If no arguments passed generate random name and number 12 | pub fn get_user_agent_string(name: Option, version: Option) -> String { 13 | if let (Some(v), Some(n)) = (version, name) { 14 | format!("{}:{}", n, v) 15 | } else { 16 | let random_name = RandomName::new().to_string().replace(" ", "").to_lowercase(); 17 | 18 | let mut rng = rand::thread_rng(); 19 | let random_version = rng.gen::(); 20 | format!("{}:{}", random_name, random_version) 21 | } 22 | } 23 | 24 | /// Check if a particular path is present on the filesystem 25 | pub fn check_path_present(file_path: &str) -> bool { 26 | Path::new(file_path).exists() 27 | } 28 | 29 | /// Function that masks sensitive data such as password and client secrets 30 | pub fn mask_sensitive(word: &str) -> String { 31 | let word_length = word.len(); 32 | return if word.is_empty() { 33 | // return with indication if string is empty 34 | String::from("") 35 | } else if word_length > 0 && word_length <= 3 { 36 | // if string length is between 1-3, mask all characters 37 | "*".repeat(word_length) 38 | } else { 39 | // if string length greater than 5, mask all characters 40 | // except the first two and the last characters 41 | word.chars() 42 | .enumerate() 43 | .map(|(i, c)| if i == 0 || i == 1 || i == word_length - 1 { c } else { '*' }) 44 | .collect() 45 | }; 46 | } 47 | 48 | /// Return delimited subreddit names or EMPTY if None 49 | pub fn print_subreddits(subreddits: &Option>) -> String { 50 | return if let Some(s) = subreddits { s.join(",") } else { String::from("") }; 51 | } 52 | 53 | /// Check if the given application is present in the $PATH 54 | pub fn application_present(name: String) -> bool { 55 | let result = which(name); 56 | match result { 57 | Ok(_) => true, 58 | _ => false, 59 | } 60 | } 61 | 62 | /// Check if the given URL contains an MP4 track using the content type 63 | pub async fn check_url_is_mp4(url: &str) -> Result, ReddSaverError> { 64 | let response = reqwest::get(url).await?; 65 | let headers = response.headers(); 66 | 67 | match headers.get(CONTENT_TYPE) { 68 | None => Ok(None), 69 | Some(content_type) => { 70 | let content_type = Mime::from_str(content_type.to_str()?)?; 71 | let is_video = match (content_type.type_(), content_type.subtype()) { 72 | (mime::VIDEO, mime::MP4) => true, 73 | (mime::APPLICATION, mime::XML) => false, 74 | _ => false, 75 | }; 76 | Ok(Some(is_video)) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/user.rs: -------------------------------------------------------------------------------- 1 | use crate::auth::Auth; 2 | use crate::errors::ReddSaverError; 3 | use crate::structures::{Listing, UserAbout}; 4 | use crate::utils::get_user_agent_string; 5 | use log::{debug, info}; 6 | use reqwest::header::USER_AGENT; 7 | use std::borrow::Borrow; 8 | use std::collections::HashMap; 9 | use std::fmt; 10 | use std::fmt::{Display, Formatter}; 11 | 12 | #[derive(Debug)] 13 | pub struct User<'a> { 14 | /// Contains authentication information about the user 15 | auth: &'a Auth, 16 | /// Username of the user who authorized the application 17 | name: &'a str, 18 | } 19 | 20 | #[derive(Debug)] 21 | pub enum ListingType { 22 | Saved, 23 | Upvoted, 24 | } 25 | 26 | impl Display for ListingType { 27 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 28 | match *self { 29 | ListingType::Saved => write!(f, "saved"), 30 | ListingType::Upvoted => write!(f, "upvoted"), 31 | } 32 | } 33 | } 34 | 35 | impl<'a> User<'a> { 36 | pub fn new(auth: &'a Auth, name: &'a str) -> Self { 37 | User { auth, name } 38 | } 39 | 40 | pub async fn about(&self) -> Result { 41 | // all API requests that use a bearer token should be made to oauth.reddit.com instead 42 | let url = format!("https://oauth.reddit.com/user/{}/about", self.name); 43 | let client = reqwest::Client::new(); 44 | 45 | let response = client 46 | .get(&url) 47 | .bearer_auth(&self.auth.access_token) 48 | // reddit will forbid you from accessing the API if the provided user agent is not unique 49 | .header(USER_AGENT, get_user_agent_string(None, None)) 50 | .send() 51 | .await? 52 | .json::() 53 | .await?; 54 | 55 | debug!("About Response: {:#?}", response); 56 | 57 | Ok(response) 58 | } 59 | 60 | pub async fn listing( 61 | &self, 62 | listing_type: &ListingType, 63 | ) -> Result, ReddSaverError> { 64 | let client = reqwest::Client::new(); 65 | 66 | let mut complete = false; 67 | let mut processed = 0; 68 | let mut after: Option = None; 69 | let mut listing: Vec = Vec::new(); 70 | while !complete { 71 | // during the first call to the API, we would not provide the after query parameter 72 | // in subsequent calls, we use the value for after from the response of the 73 | // previous request and continue doing so till the value of after is null 74 | let url = if processed == 0 { 75 | format!("https://oauth.reddit.com/user/{}/{}", self.name, listing_type.to_string()) 76 | } else { 77 | format!( 78 | "https://oauth.reddit.com/user/{}/{}?after={}", 79 | self.name, 80 | listing_type.to_string(), 81 | after.as_ref().unwrap() 82 | ) 83 | }; 84 | 85 | let response = client 86 | .get(&url) 87 | .bearer_auth(&self.auth.access_token) 88 | .header(USER_AGENT, get_user_agent_string(None, None)) 89 | // the maximum number of items returned by the API in a single request is 100 90 | .query(&[("limit", 100)]) 91 | .send() 92 | .await? 93 | .json::() 94 | .await?; 95 | 96 | // total number of items processed by the method 97 | // note that not all of these items are media, so the downloaded media will be 98 | // lesser than or equal to the number of items present 99 | processed += response.borrow().data.dist; 100 | info!("Number of items processed : {}", processed); 101 | 102 | // if there is a response, continue collecting them into a vector 103 | if response.borrow().data.after.as_ref().is_none() { 104 | info!("Data gathering complete. Yay."); 105 | listing.push(response); 106 | complete = true; 107 | } else { 108 | debug!("Processing till: {}", response.borrow().data.after.as_ref().unwrap()); 109 | after = response.borrow().data.after.clone(); 110 | listing.push(response); 111 | } 112 | } 113 | 114 | Ok(listing) 115 | } 116 | 117 | pub async fn undo(&self, name: &str, listing_type: &ListingType) -> Result<(), ReddSaverError> { 118 | let client = reqwest::Client::new(); 119 | let url: String; 120 | let mut map = HashMap::new(); 121 | map.insert("id", name); 122 | 123 | match listing_type { 124 | ListingType::Upvoted => { 125 | url = format!("https://oauth.reddit.com/api/vote"); 126 | map.insert("dir", "0"); 127 | } 128 | ListingType::Saved => { 129 | url = format!("https://oauth.reddit.com/api/unsave"); 130 | } 131 | } 132 | 133 | let response = client 134 | .post(&url) 135 | .bearer_auth(&self.auth.access_token) 136 | .header(USER_AGENT, get_user_agent_string(None, None)) 137 | .form(&map) 138 | .send() 139 | .await?; 140 | 141 | debug!("Response: {:#?}", response); 142 | 143 | Ok(()) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/structures.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | use std::ops::Add; 4 | 5 | /// Data structure that represents a user's info 6 | #[derive(Debug, Serialize, Deserialize)] 7 | pub struct AboutData { 8 | /// Comment karma of the user 9 | pub comment_karma: i64, 10 | /// The time the user was created in seconds 11 | pub created: f64, 12 | /// The time the user was created in seconds (UTC) 13 | pub created_utc: f64, 14 | /// Undocumented 15 | pub has_subscribed: bool, 16 | /// Whether the user has verified their email 17 | pub has_verified_email: bool, 18 | /// Don't know 19 | pub hide_from_robots: bool, 20 | /// The id of the user 21 | pub id: String, 22 | /// Whether the user is a Reddit employee 23 | pub is_employee: bool, 24 | /// Whether the user is friend of the current user 25 | pub is_friend: bool, 26 | /// Whether the user has Reddit gold or not 27 | pub is_gold: bool, 28 | /// Whether the user is a moderator 29 | pub is_mod: bool, 30 | /// Link karma of the user 31 | pub link_karma: i64, 32 | /// The user's username 33 | pub name: String, 34 | } 35 | 36 | #[derive(Debug, Serialize, Deserialize)] 37 | pub struct UserAbout { 38 | /// The kind of object this is. eg: Comment, Account, Subreddit, etc. 39 | pub kind: String, 40 | /// Contains data about the reddit user 41 | pub data: AboutData, 42 | } 43 | 44 | #[derive(Deserialize, Debug)] 45 | pub struct Listing { 46 | /// The kind of object this is. eg: Comment, Account, Subreddit, etc. 47 | pub kind: String, 48 | /// Contains the data for the children of the listing. 49 | /// Listings are collections of data. For example, saved posts, hot posts in a subreddit 50 | pub data: ListingData, 51 | } 52 | 53 | /// The contents of a call to a 'listing' endpoint. 54 | #[derive(Deserialize, Debug)] 55 | pub struct ListingData { 56 | /// A modhash (essentially a CSRF token) generated for this request. This is generally 57 | /// not required for any use-case, but is provided nevertheless. 58 | pub modhash: Option, 59 | pub before: Option, 60 | pub after: Option, 61 | pub children: Vec, 62 | pub dist: i32, 63 | } 64 | 65 | #[derive(Deserialize, Debug, Clone)] 66 | pub struct Post { 67 | /// The kind of object this is. eg: Comment, Account, Subreddit, etc. 68 | pub kind: String, 69 | /// Contains data about this particular reddit post 70 | pub data: PostData, 71 | } 72 | 73 | /// Represents all types of link posts and self posts on Reddit. 74 | #[derive(Deserialize, Debug, Clone)] 75 | pub struct PostData { 76 | pub subreddit: String, 77 | /// The ID of the post in base-36 form, as used in Reddit's links. 78 | pub id: String, 79 | /// The overall points score of this post, as shown on the upvote counter. This is the 80 | /// same as upvotes - downvotes (however, this figure may be fuzzed by Reddit, and may not 81 | /// be exact) 82 | pub score: i64, 83 | /// The URL to the link thumbnail. This is "self" if this is a self post, or "default" if 84 | /// a thumbnail is not available. 85 | pub thumbnail: Option, 86 | /// The Reddit ID for the subreddit where this was posted, **including the leading `t5_`**. 87 | pub subreddit_id: String, 88 | /// True if the logged-in user has saved this submission. 89 | pub saved: bool, 90 | /// The permanent, long link for this submission. 91 | pub permalink: String, 92 | /// The full 'Thing ID', consisting of a 'kind' and a base-36 identifier. The valid kinds are: 93 | /// - t1_ - Comment 94 | /// - t2_ - Account 95 | /// - t3_ - Link 96 | /// - t4_ - Message 97 | /// - t5_ - Subreddit 98 | /// - t6_ - Award 99 | /// - t8_ - PromoCampaign 100 | pub name: String, 101 | /// A timestamp of the time when the post was created, in the logged-in user's **local** 102 | /// time. 103 | pub created: Value, 104 | /// The linked URL, if this is a link post. 105 | pub url: Option, 106 | /// The title of the post. 107 | pub title: Option, 108 | /// A timestamp of the time when the post was created, in **UTC**. 109 | pub created_utc: Value, 110 | /// Gallery metadata 111 | pub gallery_data: Option, 112 | /// Is post a video? 113 | pub is_video: Option, 114 | /// Reddit Media info 115 | pub media: Option, 116 | } 117 | 118 | #[derive(Deserialize, Debug, Clone)] 119 | pub struct PostMedia { 120 | pub reddit_video: Option, 121 | } 122 | 123 | #[derive(Deserialize, Debug, Clone)] 124 | pub struct RedditVideo { 125 | pub fallback_url: String, 126 | pub is_gif: bool, 127 | } 128 | 129 | #[derive(Deserialize, Debug, Clone)] 130 | pub struct GalleryItems { 131 | /// Representation containing a list of gallery items 132 | pub items: Vec, 133 | } 134 | 135 | #[derive(Deserialize, Debug, Clone)] 136 | pub struct GalleryItem { 137 | /// The reddit media id, can be used to construct a redd.it URL 138 | pub media_id: String, 139 | /// Unique numerical ID for the specific media item 140 | pub id: i64, 141 | } 142 | 143 | #[derive(Deserialize, Debug, Clone)] 144 | pub struct GfyData { 145 | #[serde(rename = "gfyItem")] 146 | pub gfy_item: GfyItem, 147 | } 148 | 149 | #[derive(Deserialize, Debug, Clone)] 150 | pub struct GfyItem { 151 | #[serde(rename = "gifUrl")] 152 | pub gif_url: String, 153 | #[serde(rename = "mp4Url")] 154 | pub mp4_url: String, 155 | } 156 | 157 | #[derive(Debug, Copy, Clone, PartialEq)] 158 | pub struct Summary { 159 | /// Number of media downloaded 160 | pub media_downloaded: i32, 161 | /// Number of media skipping downloading 162 | pub media_skipped: i32, 163 | /// Number of media supported present and parsable 164 | pub media_supported: i32, 165 | } 166 | 167 | impl Add for Summary { 168 | type Output = Self; 169 | 170 | fn add(self, rhs: Self) -> Self::Output { 171 | Self { 172 | media_supported: self.media_supported + rhs.media_supported, 173 | media_downloaded: self.media_downloaded + rhs.media_downloaded, 174 | media_skipped: self.media_skipped + rhs.media_skipped, 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reddsaver ![build](https://github.com/manojkarthick/reddsaver/workflows/build/badge.svg) [![Crates.io](https://img.shields.io/crates/v/reddsaver.svg)](https://crates.io/crates/reddsaver) 2 | 3 | * Command line tool to download saved/upvoted media from Reddit 4 | * Supports: 5 | - Reddit: PNG/JPG images, GIFs, Image galleries, videos 6 | - Giphy: GIFs 7 | - Imgur: Direct images and GIFVs 8 | - Gfycat/Redgifs: GIFs 9 | * GIF/GIFV from Imgur/Gfycat/Redgifs are downloaded as mp4 10 | * Does *not* support downloading images from Imgur post links 11 | 12 | ## Installation 13 | 14 | ### Prerequisites 15 | 16 | To download videos hosted by Reddit, you need to have ffmpeg installed. 17 | Follow this [link](https://www.ffmpeg.org/download.html) for installation instructions. 18 | 19 | ### Recommended method 20 | 21 | You can download release binaries [here](https://github.com/manojkarthick/reddsaver/releases) 22 | 23 | ### Alternative methods 24 | 25 | #### Using MacPorts 26 | 27 | If you are a macports user on macOS, you can install reddsaver using `port`: 28 | 29 | ``` 30 | sudo port selfudpate 31 | sudo port install reddsaver 32 | ``` 33 | 34 | #### Using Homebrew 35 | 36 | If you are a homebrew user on macOS, you can install using `brew tap`: 37 | 38 | ```shell 39 | brew tap manojkarthick/reddsaver 40 | brew install reddsaver 41 | ``` 42 | 43 | #### Arch Linux 44 | 45 | If you are an ArchLinux user, then you can use a tool like `yay` or `paru` to install it from the [AUR](https://aur.archlinux.org/packages/reddsaver-bin/): 46 | ```shell script 47 | yay -S reddsaver 48 | ``` 49 | 50 | #### Using cargo 51 | 52 | If you already have Rust installed, you can also install using `cargo`: 53 | ```shell script 54 | cargo install reddsaver 55 | ``` 56 | 57 | #### Using nix 58 | 59 | If you are a [nix](https://github.com/NixOS/nix) user, you can install reddsaver from [nixpkgs](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/reddsaver/default.nix) 60 | ```shell script 61 | nix-env --install reddsaver 62 | ``` 63 | 64 | or, if you manage your installation using [home-manager](https://github.com/nix-community/home-manager), add to your `home.packages`: 65 | ```shell script 66 | home.packages = [ 67 | pkgs.reddsaver 68 | ]; 69 | ``` 70 | 71 | #### Building and running from source 72 | 73 | Make sure you have rustc `v1.50.0` and cargo installed on your machine. 74 | ```shell script 75 | git clone https://github.com/manojkarthick/reddsaver.git 76 | cargo build --release 77 | ./target/release/reddsaver 78 | ``` 79 | 80 | #### Docker support 81 | 82 | Pre-built docker images are available on [Docker Hub](https://hub.docker.com/u/manojkarthick) 83 | 84 | ```shell script 85 | mkdir -pv data/ 86 | docker run --rm \ 87 | --volume="$PWD/data:/app/data" \ 88 | --volume="$PWD/reddsaver.env:/app/reddsaver.env" \ 89 | manojkarthick/reddsaver:latest -d /app/data -e /app/reddsaver.env 90 | ``` 91 | 92 | ## Running 93 | 94 | 1. Create a new script application at https://www.reddit.com/prefs/apps 95 | * Click on create an app at the bottom of the page 96 | * Input a name for your application, for example: -reddsaver 97 | * Choose "script" as the type of application 98 | * Set "http://localhost:8080" or any other URL for the redirect url 99 | * Click on "create app" - you should now see the application has been created 100 | * Under your application name, you should see a random string - that is your client ID 101 | * The random string next to the field "secret" is your client secret 102 | 2. Copy the client ID and client secret information returned 103 | 3. Create a .env file with the following keys, for example `reddsaver.env`: 104 | ```shell script 105 | CLIENT_ID="" 106 | CLIENT_SECRET="" 107 | USERNAME="" 108 | PASSWORD="" 109 | ``` 110 | _NOTE_: If you have 2FA enabled, please make sure you set `PASSWORD=:<2FA_TOTP_token>` instead 111 | 112 | 4. Run the app! 113 | ```shell script 114 | 115 | # Create a directory to save your images to 116 | mkdir -pv reddsaver/ 117 | 118 | # Check if you installation is working properly 119 | reddsaver --help 120 | 121 | # Check if the right configuration has been picked up 122 | # NOTE: In case the `USERNAME` variable is being overriden by 123 | # your system username, please use 124 | # On Linux/Mac - unset USERNAME 125 | # On Windows - set USERNAME= 126 | # before running to temporarily remove the system username 127 | # from your environment 128 | reddsaver -e reddsaver.env -d reddsaver --show-config 129 | 130 | # Run the app to download the saved media 131 | reddsaver -e reddsaver.env -d reddsaver 132 | 133 | # Also allows you to download upvoted media 134 | reddsaver -e reddsaver.env -d reddsaver --upvoted 135 | ``` 136 | 137 | NOTE: When running the application beyond the first time, if you use the directory as the initial run, the application will skip downloading the images that have already been downloaded. 138 | 139 | View it in action here: 140 | 141 | [![asciicast](https://asciinema.org/a/382339.svg)](https://asciinema.org/a/382339) 142 | 143 | ## Description and command line arguments 144 | 145 | Optionally override the values for the directory to save and the env file to read from: 146 | 147 | ```shell script 148 | ReddSaver 0.4.0 149 | Manoj Karthick Selva Kumar 150 | Simple CLI tool to download saved media from Reddit 151 | 152 | USAGE: 153 | reddsaver [FLAGS] [OPTIONS] 154 | 155 | FLAGS: 156 | -r, --dry-run Dry run and print the URLs of saved media to download 157 | -h, --help Prints help information 158 | -H, --human-readable Use human readable names for files 159 | -s, --show-config Show the current config being used 160 | -U, --undo Unsave or remote upvote for post after processing 161 | -u, --upvoted Download media from upvoted posts 162 | -V, --version Prints version information 163 | 164 | OPTIONS: 165 | -d, --data-dir Directory to save the media to [default: data] 166 | -e, --from-env Set a custom .env style file with secrets [default: .env] 167 | -S, --subreddits ... Download media from these subreddits only 168 | ``` 169 | 170 | Some points to note: 171 | 172 | * By default, reddsaver generates filenames for the images using a MD5 Hash of the URLs. You can instead generate human readable names using the `--human-readable` flag. 173 | * You can check the configuration used by ReddSaver by using the `--show-config` flag. 174 | 175 | ## Other Information 176 | 177 | ### Building for Raspberry Pi Zero W 178 | 179 | To cross-compile for raspberry pi, this project uses [rust-cross](https://github.com/rust-embedded/cross). Make sure you have docker installed on your development machine. 180 | 181 | 1. Build the docker image for rust-cross: `docker build -t rust-rpi-zerow:v1-openssl -f Dockerfile.raspberrypizerow .` 182 | 2. Make sure that the image name used here matches the image name in your `Cross.toml` configuration 183 | 3. Run `cross build --target arm-unknown-linux-gnueabi --release` to build the project 184 | 4. You can find the compiled binary under `target/arm-unknown-linux-gnueabi/release/` 185 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use clap::{crate_version, App, Arg}; 4 | use env_logger::Env; 5 | use log::{debug, info, warn}; 6 | 7 | use auth::Client; 8 | 9 | use crate::download::Downloader; 10 | use crate::errors::ReddSaverError; 11 | use crate::errors::ReddSaverError::DataDirNotFound; 12 | use crate::user::{ListingType, User}; 13 | use crate::utils::*; 14 | 15 | mod auth; 16 | mod download; 17 | mod errors; 18 | mod structures; 19 | mod user; 20 | mod utils; 21 | 22 | #[tokio::main] 23 | async fn main() -> Result<(), ReddSaverError> { 24 | let matches = App::new("ReddSaver") 25 | .version(crate_version!()) 26 | .author("Manoj Karthick Selva Kumar") 27 | .about("Simple CLI tool to download saved media from Reddit") 28 | .arg( 29 | Arg::with_name("environment") 30 | .short("e") 31 | .long("from-env") 32 | .value_name("ENV_FILE") 33 | .help("Set a custom .env style file with secrets") 34 | .default_value(".env") 35 | .takes_value(true), 36 | ) 37 | .arg( 38 | Arg::with_name("data_directory") 39 | .short("d") 40 | .long("data-dir") 41 | .value_name("DATA_DIR") 42 | .help("Directory to save the media to") 43 | .default_value("data") 44 | .takes_value(true), 45 | ) 46 | .arg( 47 | Arg::with_name("show_config") 48 | .short("s") 49 | .long("show-config") 50 | .takes_value(false) 51 | .help("Show the current config being used"), 52 | ) 53 | .arg( 54 | Arg::with_name("dry_run") 55 | .short("r") 56 | .long("dry-run") 57 | .takes_value(false) 58 | .help("Dry run and print the URLs of saved media to download"), 59 | ) 60 | .arg( 61 | Arg::with_name("human_readable") 62 | .short("H") 63 | .long("human-readable") 64 | .takes_value(false) 65 | .help("Use human readable names for files"), 66 | ) 67 | .arg( 68 | Arg::with_name("subreddits") 69 | .short("S") 70 | .long("subreddits") 71 | .multiple(true) 72 | .value_name("SUBREDDITS") 73 | .value_delimiter(",") 74 | .help("Download media from these subreddits only") 75 | .takes_value(true), 76 | ) 77 | .arg( 78 | Arg::with_name("upvoted") 79 | .short("u") 80 | .long("--upvoted") 81 | .takes_value(false) 82 | .help("Download media from upvoted posts"), 83 | ) 84 | .arg( 85 | Arg::with_name("undo") 86 | .short("U") 87 | .long("undo") 88 | .takes_value(false) 89 | .help("Unsave or remote upvote for post after processing"), 90 | ) 91 | .get_matches(); 92 | 93 | let env_file = matches.value_of("environment").unwrap(); 94 | let data_directory = String::from(matches.value_of("data_directory").unwrap()); 95 | // generate the URLs to download from without actually downloading the media 96 | let should_download = !matches.is_present("dry_run"); 97 | // check if ffmpeg is present for combining video streams 98 | let ffmpeg_available = application_present(String::from("ffmpeg")); 99 | // generate human readable file names instead of MD5 Hashed file names 100 | let use_human_readable = matches.is_present("human_readable"); 101 | // restrict downloads to these subreddits 102 | let subreddits: Option> = if matches.is_present("subreddits") { 103 | Some(matches.values_of("subreddits").unwrap().collect()) 104 | } else { 105 | None 106 | }; 107 | let upvoted = matches.is_present("upvoted"); 108 | let listing_type = if upvoted { &ListingType::Upvoted } else { &ListingType::Saved }; 109 | 110 | let undo = matches.is_present("undo"); 111 | 112 | // initialize environment from the .env file 113 | dotenv::from_filename(env_file).ok(); 114 | 115 | // initialize logger for the app and set logging level to info if no environment variable present 116 | let env = Env::default().filter("RS_LOG").default_filter_or("info"); 117 | env_logger::Builder::from_env(env).init(); 118 | 119 | let client_id = env::var("CLIENT_ID")?; 120 | let client_secret = env::var("CLIENT_SECRET")?; 121 | let username = env::var("USERNAME")?; 122 | let password = env::var("PASSWORD")?; 123 | let user_agent = get_user_agent_string(None, None); 124 | 125 | if !check_path_present(&data_directory) { 126 | return Err(DataDirNotFound); 127 | } 128 | 129 | // if the option is show-config, show the configuration and return immediately 130 | if matches.is_present("show_config") { 131 | info!("Current configuration:"); 132 | info!("ENVIRONMENT_FILE = {}", &env_file); 133 | info!("DATA_DIRECTORY = {}", &data_directory); 134 | info!("CLIENT_ID = {}", &client_id); 135 | info!("CLIENT_SECRET = {}", mask_sensitive(&client_secret)); 136 | info!("USERNAME = {}", &username); 137 | info!("PASSWORD = {}", mask_sensitive(&password)); 138 | info!("USER_AGENT = {}", &user_agent); 139 | info!("SUBREDDITS = {}", print_subreddits(&subreddits)); 140 | info!("UPVOTED = {}", upvoted); 141 | info!("UNDO = {}", undo); 142 | info!("FFMPEG AVAILABLE = {}", ffmpeg_available); 143 | 144 | return Ok(()); 145 | } 146 | 147 | if !ffmpeg_available { 148 | warn!( 149 | "No ffmpeg Installation available. \ 150 | Videos hosted by Reddit use separate video and audio streams. \ 151 | Ffmpeg needs be installed to combine the audio and video into a single mp4." 152 | ); 153 | } 154 | 155 | // login to reddit using the credentials provided and get API bearer token 156 | let auth = 157 | Client::new(&client_id, &client_secret, &username, &password, &user_agent).login().await?; 158 | info!("Successfully logged in to Reddit as {}", username); 159 | debug!("Authentication details: {:#?}", auth); 160 | 161 | // get information about the user to display 162 | let user = User::new(&auth, &username); 163 | 164 | let user_info = user.about().await?; 165 | info!("The user details are: "); 166 | info!("Account name: {:#?}", user_info.data.name); 167 | info!("Account ID: {:#?}", user_info.data.id); 168 | info!("Comment Karma: {:#?}", user_info.data.comment_karma); 169 | info!("Link Karma: {:#?}", user_info.data.link_karma); 170 | 171 | info!("Starting data gathering from Reddit. This might take some time. Hold on...."); 172 | // get the saved/upvoted posts for this particular user 173 | let listing = user.listing(listing_type).await?; 174 | debug!("Posts: {:#?}", listing); 175 | 176 | let downloader = Downloader::new( 177 | &user, 178 | &listing, 179 | &listing_type, 180 | &data_directory, 181 | &subreddits, 182 | should_download, 183 | use_human_readable, 184 | undo, 185 | ffmpeg_available, 186 | ); 187 | 188 | downloader.run().await?; 189 | 190 | Ok(()) 191 | } 192 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/LICENSE-2.0 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | https://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/download.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::fs::File; 3 | use std::ops::Add; 4 | use std::path::Path; 5 | use std::process::Command; 6 | use std::sync::{Arc, Mutex}; 7 | use std::{fs, io}; 8 | 9 | use futures::stream::FuturesUnordered; 10 | use futures::TryStreamExt; 11 | use log::{debug, error, info, warn}; 12 | use reqwest::StatusCode; 13 | use tempfile::tempdir; 14 | use url::{Position, Url}; 15 | 16 | use crate::errors::ReddSaverError; 17 | use crate::structures::{GfyData, PostData}; 18 | use crate::structures::{Listing, Summary}; 19 | use crate::user::{ListingType, User}; 20 | use crate::utils::{check_path_present, check_url_is_mp4}; 21 | 22 | static JPG_EXTENSION: &str = "jpg"; 23 | static PNG_EXTENSION: &str = "png"; 24 | static GIF_EXTENSION: &str = "gif"; 25 | static GIFV_EXTENSION: &str = "gifv"; 26 | static MP4_EXTENSION: &str = "mp4"; 27 | 28 | static REDDIT_DOMAIN: &str = "reddit.com"; 29 | static REDDIT_IMAGE_SUBDOMAIN: &str = "i.redd.it"; 30 | static REDDIT_VIDEO_SUBDOMAIN: &str = "v.redd.it"; 31 | static REDDIT_GALLERY_PATH: &str = "gallery"; 32 | 33 | static IMGUR_DOMAIN: &str = "imgur.com"; 34 | static IMGUR_SUBDOMAIN: &str = "i.imgur.com"; 35 | 36 | static GFYCAT_DOMAIN: &str = "gfycat.com"; 37 | static GFYCAT_API_PREFIX: &str = "https://api.gfycat.com/v1/gfycats"; 38 | 39 | static REDGIFS_DOMAIN: &str = "redgifs.com"; 40 | static REDGIFS_API_PREFIX: &str = "https://api.redgifs.com/v1/gfycats"; 41 | 42 | static GIPHY_DOMAIN: &str = "giphy.com"; 43 | static GIPHY_MEDIA_SUBDOMAIN: &str = "media.giphy.com"; 44 | static GIPHY_MEDIA_SUBDOMAIN_0: &str = "media0.giphy.com"; 45 | static GIPHY_MEDIA_SUBDOMAIN_1: &str = "media1.giphy.com"; 46 | static GIPHY_MEDIA_SUBDOMAIN_2: &str = "media2.giphy.com"; 47 | static GIPHY_MEDIA_SUBDOMAIN_3: &str = "media3.giphy.com"; 48 | static GIPHY_MEDIA_SUBDOMAIN_4: &str = "media4.giphy.com"; 49 | 50 | /// Status of media processing 51 | enum MediaStatus { 52 | /// If we are able to successfully download the media 53 | Downloaded, 54 | /// If we are skipping downloading the media due to it already being present 55 | /// or because we could not find the media or because we are unable to decode 56 | /// the media 57 | Skipped, 58 | } 59 | 60 | /// Media Types Supported 61 | #[derive(Debug, PartialEq)] 62 | enum MediaType { 63 | RedditImage, 64 | RedditGif, 65 | RedditVideoWithAudio, 66 | RedditVideoWithoutAudio, 67 | GfycatGif, 68 | GiphyGif, 69 | ImgurImage, 70 | ImgurGif, 71 | } 72 | 73 | /// Information about supported media for downloading 74 | struct SupportedMedia { 75 | /// The components for the media. This is a vector of size one for 76 | /// all media types except Reddit videos and Reddit Galleries. 77 | /// For reddit videos, audio and video are provided separately. 78 | components: Vec, 79 | media_type: MediaType, 80 | } 81 | 82 | #[derive(Debug)] 83 | pub struct Downloader<'a> { 84 | user: &'a User<'a>, 85 | listing: &'a Vec, 86 | listing_type: &'a ListingType, 87 | data_directory: &'a str, 88 | subreddits: &'a Option>, 89 | should_download: bool, 90 | use_human_readable: bool, 91 | undo: bool, 92 | ffmpeg_available: bool, 93 | } 94 | 95 | impl<'a> Downloader<'a> { 96 | pub fn new( 97 | user: &'a User, 98 | listing: &'a Vec, 99 | listing_type: &'a ListingType, 100 | data_directory: &'a str, 101 | subreddits: &'a Option>, 102 | should_download: bool, 103 | use_human_readable: bool, 104 | undo: bool, 105 | ffmpeg_available: bool, 106 | ) -> Downloader<'a> { 107 | Downloader { 108 | user, 109 | listing, 110 | listing_type, 111 | data_directory, 112 | subreddits, 113 | should_download, 114 | use_human_readable, 115 | undo, 116 | ffmpeg_available, 117 | } 118 | } 119 | 120 | pub async fn run(self) -> Result<(), ReddSaverError> { 121 | let mut full_summary = 122 | Summary { media_downloaded: 0, media_skipped: 0, media_supported: 0 }; 123 | 124 | for collection in self.listing { 125 | full_summary = 126 | full_summary.add(self.download_collection(collection, self.listing_type).await?); 127 | } 128 | 129 | info!("#####################################"); 130 | info!("Download Summary:"); 131 | info!("Number of supported media: {}", full_summary.media_supported); 132 | info!("Number of media downloaded: {}", full_summary.media_downloaded); 133 | info!("Number of media skipped: {}", full_summary.media_skipped); 134 | info!("#####################################"); 135 | info!("FIN."); 136 | 137 | Ok(()) 138 | } 139 | 140 | /// Download and save medias from Reddit in parallel 141 | async fn download_collection( 142 | &self, 143 | collection: &Listing, 144 | listing_type: &ListingType, 145 | ) -> Result { 146 | let summary = Arc::new(Mutex::new(Summary { 147 | media_supported: 0, 148 | media_downloaded: 0, 149 | media_skipped: 0, 150 | })); 151 | 152 | collection 153 | .data 154 | .children 155 | .clone() 156 | .into_iter() 157 | // filter out the posts where a URL is present 158 | // not that this application cannot download URLs linked within the text of the post 159 | .filter(|item| item.data.url.is_some()) 160 | .map(|item| { 161 | let summary_arc = summary.clone(); 162 | // since the latency for downloading an media from the network is unpredictable 163 | // we spawn a new async task for the each of the medias to be downloaded 164 | async move { 165 | let subreddit = item.data.subreddit.borrow(); 166 | let post_name = item.data.name.borrow(); 167 | let post_title = match item.data.title.as_ref() { 168 | Some(t) => t, 169 | None => "", 170 | }; 171 | 172 | let is_valid = if let Some(s) = self.subreddits.as_ref() { 173 | if s.contains(&subreddit) { 174 | true 175 | } else { 176 | false 177 | } 178 | } else { 179 | true 180 | }; 181 | 182 | if is_valid { 183 | debug!("Subreddit VALID: {} present in {:#?}", subreddit, subreddit); 184 | 185 | let supported_media_items = get_media(item.data.borrow()).await?; 186 | 187 | for supported_media in supported_media_items { 188 | let media_urls = &supported_media.components; 189 | let media_type = supported_media.media_type; 190 | let mut media_files = Vec::new(); 191 | 192 | // the number of components in the supported media is the number available for download 193 | summary_arc.lock().unwrap().media_supported += supported_media.components.len() as i32; 194 | 195 | let mut local_skipped = 0; 196 | for (index, url) in media_urls.iter().enumerate() { 197 | let mut item_index = format!("{}", index); 198 | let mut extension = 199 | String::from(url.split('.').last().unwrap_or("unknown")).replace("/", "_"); 200 | 201 | // if the media is a reddit video, they have separate audio and video components. 202 | // to differentiate this from albums, which use the regular _0, _1, etc indices, 203 | // we use _component_0, component_1 indices to explicitly inform that these are 204 | // components rather than individual media. 205 | if media_type == MediaType::RedditVideoWithAudio { 206 | item_index = format!("component_{}", index); 207 | }; 208 | // some reddit videos don't have the mp4 extension, eg. DASH__ 209 | // explicitly adding an mp4 extension to make it easy to recognize in the finder 210 | if (media_type == MediaType::RedditVideoWithoutAudio 211 | || media_type == MediaType::RedditVideoWithAudio) 212 | && !extension.ends_with(".mp4") { 213 | extension = format!("{}.{}", extension, ".mp4"); 214 | } 215 | let file_name = self.generate_file_name( 216 | &url, 217 | &subreddit, 218 | &extension, 219 | &post_name, 220 | &post_title, 221 | &item_index, 222 | ); 223 | 224 | if self.should_download { 225 | let status = save_or_skip(url, &file_name); 226 | // update the summary statistics based on the status 227 | match status.await? { 228 | MediaStatus::Downloaded => { 229 | summary_arc.lock().unwrap().media_downloaded += 1; 230 | } 231 | MediaStatus::Skipped => { 232 | local_skipped += 1; 233 | summary_arc.lock().unwrap().media_skipped += 1; 234 | } 235 | } 236 | } else { 237 | info!("Media available at URL: {}", &url); 238 | summary_arc.lock().unwrap().media_skipped += 1; 239 | } 240 | 241 | // push all the available media files into a vector 242 | // this is needed in the next step to combine the components using ffmpeg 243 | media_files.push(file_name); 244 | } 245 | 246 | debug!("Media type: {:#?}", media_type); 247 | debug!("Media files: {:?}", media_files.len()); 248 | debug!("Locally skipped items: {:?}", local_skipped); 249 | 250 | if (media_type == MediaType::RedditVideoWithAudio) 251 | && (media_files.len() == 2) 252 | && (local_skipped < 2) { 253 | if self.ffmpeg_available { 254 | debug!("Assembling components together"); 255 | let first_url = media_urls.first().unwrap(); 256 | let extension = 257 | String::from(first_url.split('.').last().unwrap_or("unknown")); 258 | // this generates the name of the media without the component indices 259 | // this file name is used for saving the ffmpeg combined file 260 | let combined_file_name = self.generate_file_name( 261 | first_url, 262 | &subreddit, 263 | &extension, 264 | &post_name, 265 | &post_title, 266 | "0", 267 | ); 268 | 269 | let temporary_dir = tempdir()?; 270 | let temporary_file_name = temporary_dir.path().join("combined.mp4"); 271 | 272 | if self.should_download { 273 | // if the media is a reddit video and it has two components, then we 274 | // need to assemble them into one file using ffmpeg. 275 | let mut command = Command::new("ffmpeg"); 276 | for media_file in &media_files { 277 | command.arg("-i").arg(media_file); 278 | } 279 | command.arg("-c").arg("copy") 280 | .arg("-map").arg("1:a") 281 | .arg("-map").arg("0:v") 282 | .arg(&temporary_file_name); 283 | 284 | debug!("Executing command: {:#?}", command); 285 | let output = command.output()?; 286 | 287 | // check the status code of the ffmpeg command. if the command is unsuccessful, 288 | // display the error and skip combining the media. 289 | if output.status.success() { 290 | debug!("Successfully combined into temporary file: {:?}", temporary_file_name); 291 | debug!("Renaming file: {} -> {}", temporary_file_name.display(), combined_file_name); 292 | fs::rename(&temporary_file_name, &combined_file_name)?; 293 | } else { 294 | // if we encountered an error, we will write logs from ffmpeg into a new log file 295 | let log_file_name = self.generate_file_name( 296 | first_url, 297 | &subreddit, 298 | "log", 299 | &post_name, 300 | &post_title, 301 | "0", 302 | ); 303 | let err = String::from_utf8(output.stderr).unwrap(); 304 | warn!("Could not combine video {} and audio {}. Saving log to: {}", 305 | media_urls.first().unwrap(), media_urls.last().unwrap(), log_file_name); 306 | fs::write(log_file_name, err)?; 307 | } 308 | } 309 | } else { 310 | warn!("Skipping combining the individual components since ffmpeg is not installed"); 311 | } 312 | } else { 313 | debug!("Skipping combining reddit video."); 314 | } 315 | } 316 | } else { 317 | debug!( 318 | "Subreddit INVALID!: {} NOT present in {:#?}", 319 | subreddit, self.subreddits 320 | ); 321 | } 322 | 323 | if self.undo { 324 | self.user.undo(post_name, listing_type).await?; 325 | } 326 | 327 | Ok::<(), ReddSaverError>(()) 328 | } 329 | }) 330 | .collect::>() 331 | .try_collect::<()>() 332 | .await?; 333 | 334 | let local_summary = *summary.lock().unwrap(); 335 | 336 | debug!("Collection statistics: "); 337 | debug!("Number of supported media: {}", local_summary.media_supported); 338 | debug!("Number of media downloaded: {}", local_summary.media_downloaded); 339 | debug!("Number of media skipped: {}", local_summary.media_skipped); 340 | 341 | Ok(local_summary) 342 | } 343 | 344 | /// Generate a file name in the right format that Reddsaver expects 345 | fn generate_file_name( 346 | &self, 347 | url: &str, 348 | subreddit: &str, 349 | extension: &str, 350 | name: &str, 351 | title: &str, 352 | index: &str, 353 | ) -> String { 354 | return if !self.use_human_readable { 355 | // create a hash for the media using the URL the media is located at 356 | // this helps to make sure the media download always writes the same file 357 | // name irrespective of how many times it's run. If run more than once, the 358 | // media is overwritten by this method 359 | let hash = md5::compute(url); 360 | format!( 361 | // TODO: Fixme, use appropriate prefix 362 | "{}/{}/img-{:x}.{}", 363 | self.data_directory, subreddit, hash, extension 364 | ) 365 | } else { 366 | let canonical_title: String = title 367 | .to_lowercase() 368 | .chars() 369 | // to make sure file names don't exceed operating system maximums, truncate at 200 370 | // you could possibly stretch beyond 200, but this is a conservative estimate that 371 | // leaves 55 bytes for the name string 372 | .take(200) 373 | .enumerate() 374 | .map(|(_, c)| { 375 | if c.is_whitespace() 376 | || c == '.' 377 | || c == '/' 378 | || c == '\\' 379 | || c == ':' 380 | || c == '=' 381 | { 382 | '_' 383 | } else { 384 | c 385 | } 386 | }) 387 | .collect(); 388 | // create a canonical human readable file name using the post's title 389 | // note that the name of the post is something of the form t3_ 390 | let canonical_name: String = 391 | if index == "0" { String::from(name) } else { format!("{}_{}", name, index) } 392 | .replace(".", "_"); 393 | format!( 394 | "{}/{}/{}_{}.{}", 395 | self.data_directory, subreddit, canonical_title, canonical_name, extension 396 | ) 397 | }; 398 | } 399 | } 400 | 401 | /// Helper function that downloads and saves a single media from Reddit or Imgur 402 | async fn save_or_skip(url: &str, file_name: &str) -> Result { 403 | if check_path_present(&file_name) { 404 | debug!("Media from url {} already downloaded. Skipping...", url); 405 | Ok(MediaStatus::Skipped) 406 | } else { 407 | let save_status = download_media(&file_name, &url).await?; 408 | if save_status { 409 | Ok(MediaStatus::Downloaded) 410 | } else { 411 | Ok(MediaStatus::Skipped) 412 | } 413 | } 414 | } 415 | 416 | /// Download media from the given url and save to data directory. Also create data directory if not present already 417 | async fn download_media(file_name: &str, url: &str) -> Result { 418 | // create directory if it does not already exist 419 | // the directory is created relative to the current working directory 420 | let mut status = false; 421 | let directory = Path::new(file_name).parent().unwrap(); 422 | match fs::create_dir_all(directory) { 423 | Ok(_) => (), 424 | Err(_e) => return Err(ReddSaverError::CouldNotCreateDirectory), 425 | } 426 | 427 | let maybe_response = reqwest::get(url).await; 428 | if let Ok(response) = maybe_response { 429 | debug!("URL Response: {:#?}", response); 430 | let maybe_data = response.bytes().await; 431 | if let Ok(data) = maybe_data { 432 | debug!("Bytes length of the data: {:#?}", data.len()); 433 | let maybe_output = File::create(&file_name); 434 | match maybe_output { 435 | Ok(mut output) => { 436 | debug!("Created a file: {}", file_name); 437 | match io::copy(&mut data.as_ref(), &mut output) { 438 | Ok(_) => { 439 | info!("Successfully saved media: {} from url {}", file_name, url); 440 | status = true; 441 | } 442 | Err(_e) => { 443 | error!("Could not save media from url {} to {}", url, file_name); 444 | } 445 | } 446 | } 447 | Err(_) => { 448 | warn!("Could not create a file with the name: {}. Skipping", file_name); 449 | } 450 | } 451 | } 452 | } 453 | 454 | Ok(status) 455 | } 456 | 457 | /// Convert Gfycat/Redgifs GIFs into mp4 URLs for download 458 | async fn gfy_to_mp4(url: &str) -> Result, ReddSaverError> { 459 | let api_prefix = 460 | if url.contains(GFYCAT_DOMAIN) { GFYCAT_API_PREFIX } else { REDGIFS_API_PREFIX }; 461 | let maybe_media_id = url.split("/").last(); 462 | 463 | if let Some(media_id) = maybe_media_id { 464 | let api_url = format!("{}/{}", api_prefix, media_id); 465 | debug!("GFY API URL: {}", api_url); 466 | let client = reqwest::Client::new(); 467 | 468 | // talk to gfycat API and get GIF information 469 | let response = client.get(&api_url).send().await?; 470 | // if the gif is not available anymore, Gfycat might send 471 | // a 404 response. Proceed to get the mp4 URL only if the 472 | // response was HTTP 200 473 | if response.status() == StatusCode::OK { 474 | let data = response.json::().await?; 475 | let supported_media = SupportedMedia { 476 | components: vec![data.gfy_item.mp4_url], 477 | media_type: MediaType::GfycatGif, 478 | }; 479 | Ok(Some(supported_media)) 480 | } else { 481 | Ok(None) 482 | } 483 | } else { 484 | Ok(None) 485 | } 486 | } 487 | 488 | // Get reddit video information and optionally the audio track if it exists 489 | async fn get_reddit_video(url: &str) -> Result, ReddSaverError> { 490 | let maybe_dash_video = url.split("/").last(); 491 | if let Some(dash_video) = maybe_dash_video { 492 | let present = dash_video.contains("DASH"); 493 | // todo: find exhaustive collection of these, or figure out if they are (x, x*2) pairs 494 | let dash_video_only = vec!["DASH_1_2_M", "DASH_2_4_M", "DASH_4_8_M"]; 495 | if present { 496 | return if dash_video_only.contains(&dash_video) { 497 | let supported_media = SupportedMedia { 498 | components: vec![String::from(url)], 499 | media_type: MediaType::RedditVideoWithoutAudio, 500 | }; 501 | Ok(Some(supported_media)) 502 | } else { 503 | let all = url.split("/").collect::>(); 504 | let mut result = all.split_last().unwrap().1.to_vec(); 505 | let dash_audio = "DASH_audio.mp4"; 506 | result.push(dash_audio); 507 | 508 | // dynamically generate audio URLs for reddit videos by changing the video URL 509 | let audio_url = result.join("/"); 510 | // Check the mime type to see the generated URL contains an audio file 511 | // This can be done by checking the content type header for the given URL 512 | // Reddit API response does not seem to expose any easy way to figure this out 513 | if let Some(audio_present) = check_url_is_mp4(&audio_url).await? { 514 | if audio_present { 515 | debug!("Found audio at URL {} for video {}", audio_url, dash_video); 516 | let supported_media = SupportedMedia { 517 | components: vec![String::from(url), audio_url], 518 | media_type: MediaType::RedditVideoWithAudio, 519 | }; 520 | Ok(Some(supported_media)) 521 | } else { 522 | debug!( 523 | "URL {} doesn't seem to have any associated audio at {}", 524 | dash_video, audio_url 525 | ); 526 | let supported_media = SupportedMedia { 527 | components: vec![String::from(url)], 528 | media_type: MediaType::RedditVideoWithoutAudio, 529 | }; 530 | Ok(Some(supported_media)) 531 | } 532 | } else { 533 | // todo: collapse this else block by removing the bool check 534 | let supported_media = SupportedMedia { 535 | components: vec![String::from(url)], 536 | media_type: MediaType::RedditVideoWithoutAudio, 537 | }; 538 | Ok(Some(supported_media)) 539 | } 540 | }; 541 | } 542 | } 543 | 544 | Ok(None) 545 | } 546 | 547 | /// Check if a particular URL contains supported media. 548 | async fn get_media(data: &PostData) -> Result, ReddSaverError> { 549 | let original = data.url.as_ref().unwrap(); 550 | let mut media: Vec = Vec::new(); 551 | 552 | if let Ok(u) = Url::parse(original) { 553 | let mut parsed = u.clone(); 554 | 555 | match parsed.path_segments_mut() { 556 | Ok(mut p) => p.pop_if_empty(), 557 | Err(_) => return Ok(media), 558 | }; 559 | 560 | let url = &parsed[..Position::AfterPath]; 561 | let gallery_info = data.gallery_data.borrow(); 562 | 563 | // reddit images and gifs 564 | if url.contains(REDDIT_IMAGE_SUBDOMAIN) { 565 | // if the URL uses the reddit image subdomain and if the extension is 566 | // jpg, png or gif, then we can use the URL as is. 567 | if url.ends_with(JPG_EXTENSION) || url.ends_with(PNG_EXTENSION) { 568 | let translated = String::from(url); 569 | let supported_media = SupportedMedia { 570 | components: vec![translated], 571 | media_type: MediaType::RedditImage, 572 | }; 573 | media.push(supported_media); 574 | } 575 | if url.ends_with(GIF_EXTENSION) { 576 | let translated = String::from(url); 577 | let translated = SupportedMedia { 578 | components: vec![translated], 579 | media_type: MediaType::RedditGif, 580 | }; 581 | media.push(translated); 582 | } 583 | } 584 | 585 | // reddit mp4 videos 586 | if url.contains(REDDIT_VIDEO_SUBDOMAIN) { 587 | // if the URL uses the reddit video subdomain and if the extension is 588 | // mp4, then we can use the URL as is. 589 | if url.ends_with(MP4_EXTENSION) { 590 | let video_url = String::from(url); 591 | if let Some(supported_media) = get_reddit_video(&video_url).await? { 592 | media.push(supported_media); 593 | } 594 | } else { 595 | // if the URL uses the reddit video subdomain, but the link does not 596 | // point directly to the mp4, then use the fallback URL to get the 597 | // appropriate link. The video quality might range from 96p to 720p 598 | if let Some(m) = &data.media { 599 | if let Some(v) = &m.reddit_video { 600 | let fallback_url = 601 | String::from(&v.fallback_url).replace("?source=fallback", ""); 602 | if let Some(supported_media) = get_reddit_video(&fallback_url).await? { 603 | media.push(supported_media); 604 | } 605 | } 606 | } 607 | } 608 | } 609 | 610 | // reddit image galleries 611 | if url.contains(REDDIT_DOMAIN) && url.contains(REDDIT_GALLERY_PATH) { 612 | if let Some(gallery) = gallery_info { 613 | // collect all the URLs for the images in the album 614 | let mut image_urls = Vec::new(); 615 | for item in gallery.items.iter() { 616 | // extract the media ID from each gallery item and reconstruct the image URL 617 | let image_url = format!( 618 | "https://{}/{}.{}", 619 | REDDIT_IMAGE_SUBDOMAIN, item.media_id, JPG_EXTENSION 620 | ); 621 | image_urls.push(image_url); 622 | } 623 | let supported_media = 624 | SupportedMedia { components: image_urls, media_type: MediaType::RedditImage }; 625 | media.push(supported_media); 626 | } 627 | } 628 | 629 | // gfycat and redgifs 630 | if url.contains(GFYCAT_DOMAIN) || url.contains(REDGIFS_DOMAIN) { 631 | // if the Gfycat/Redgifs URL points directly to the mp4, download as is 632 | if url.ends_with(MP4_EXTENSION) { 633 | let supported_media = SupportedMedia { 634 | components: vec![String::from(url)], 635 | media_type: MediaType::GfycatGif, 636 | }; 637 | media.push(supported_media); 638 | } else { 639 | // if the provided link is a gfycat post link, use the gfycat API 640 | // to get the URL. gfycat likes to use lowercase names in their posts 641 | // but the ID for the GIF is Pascal-cased. The case-conversion info 642 | // can only be obtained from the API at the moment 643 | if let Some(supported_media) = gfy_to_mp4(url).await? { 644 | media.push(supported_media); 645 | } 646 | } 647 | } 648 | 649 | // giphy 650 | if url.contains(GIPHY_DOMAIN) { 651 | // giphy has multiple CDN networks named {media0, .., media5} 652 | // links can point to the canonical media subdomain or any content domains 653 | if url.contains(GIPHY_MEDIA_SUBDOMAIN) 654 | || url.contains(GIPHY_MEDIA_SUBDOMAIN_0) 655 | || url.contains(GIPHY_MEDIA_SUBDOMAIN_1) 656 | || url.contains(GIPHY_MEDIA_SUBDOMAIN_2) 657 | || url.contains(GIPHY_MEDIA_SUBDOMAIN_3) 658 | || url.contains(GIPHY_MEDIA_SUBDOMAIN_4) 659 | { 660 | // if we encounter gif, mp4 or gifv - download as is 661 | if url.ends_with(GIF_EXTENSION) 662 | || url.ends_with(MP4_EXTENSION) 663 | || url.ends_with(GIFV_EXTENSION) 664 | { 665 | let supported_media = SupportedMedia { 666 | components: vec![String::from(url)], 667 | media_type: MediaType::GiphyGif, 668 | }; 669 | media.push(supported_media); 670 | } 671 | } else { 672 | // if the link points to the giphy post rather than the media link, 673 | // use the scheme below to get the actual URL for the gif. 674 | let path = &parsed[Position::AfterHost..Position::AfterPath]; 675 | let media_id = path.split("-").last().unwrap(); 676 | let supported_media = SupportedMedia { 677 | components: vec![format!( 678 | "https://{}/media/{}.gif", 679 | GIPHY_MEDIA_SUBDOMAIN, media_id 680 | )], 681 | media_type: MediaType::GiphyGif, 682 | }; 683 | media.push(supported_media); 684 | } 685 | } 686 | 687 | // imgur 688 | // NOTE: only support direct links for gifv and images 689 | // *No* support for image and gallery posts. 690 | if url.contains(IMGUR_DOMAIN) { 691 | if url.contains(IMGUR_SUBDOMAIN) && url.ends_with(GIFV_EXTENSION) { 692 | // if the extension is gifv, then replace gifv->mp4 to get the video URL 693 | let supported_media = SupportedMedia { 694 | components: vec![url.replace(GIFV_EXTENSION, MP4_EXTENSION)], 695 | media_type: MediaType::ImgurGif, 696 | }; 697 | media.push(supported_media); 698 | } 699 | if url.contains(IMGUR_SUBDOMAIN) 700 | && (url.ends_with(PNG_EXTENSION) || url.ends_with(JPG_EXTENSION)) 701 | { 702 | let supported_media = SupportedMedia { 703 | components: vec![String::from(url)], 704 | media_type: MediaType::ImgurImage, 705 | }; 706 | media.push(supported_media); 707 | } 708 | } 709 | } 710 | 711 | Ok(media) 712 | } 713 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi 0.3.9", 21 | ] 22 | 23 | [[package]] 24 | name = "atty" 25 | version = "0.2.14" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 28 | dependencies = [ 29 | "hermit-abi", 30 | "libc", 31 | "winapi 0.3.9", 32 | ] 33 | 34 | [[package]] 35 | name = "autocfg" 36 | version = "1.0.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 39 | 40 | [[package]] 41 | name = "base64" 42 | version = "0.13.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 45 | 46 | [[package]] 47 | name = "bitflags" 48 | version = "1.2.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 51 | 52 | [[package]] 53 | name = "bumpalo" 54 | version = "3.7.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" 57 | 58 | [[package]] 59 | name = "bytes" 60 | version = "0.5.6" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" 63 | 64 | [[package]] 65 | name = "bytes" 66 | version = "1.0.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" 69 | 70 | [[package]] 71 | name = "cc" 72 | version = "1.0.68" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" 75 | 76 | [[package]] 77 | name = "cfg-if" 78 | version = "0.1.10" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 81 | 82 | [[package]] 83 | name = "cfg-if" 84 | version = "1.0.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 87 | 88 | [[package]] 89 | name = "clap" 90 | version = "2.33.3" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 93 | dependencies = [ 94 | "ansi_term", 95 | "atty", 96 | "bitflags", 97 | "strsim", 98 | "textwrap", 99 | "unicode-width", 100 | "vec_map", 101 | ] 102 | 103 | [[package]] 104 | name = "core-foundation" 105 | version = "0.9.1" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" 108 | dependencies = [ 109 | "core-foundation-sys", 110 | "libc", 111 | ] 112 | 113 | [[package]] 114 | name = "core-foundation-sys" 115 | version = "0.8.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" 118 | 119 | [[package]] 120 | name = "dotenv" 121 | version = "0.15.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 124 | 125 | [[package]] 126 | name = "either" 127 | version = "1.6.1" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 130 | 131 | [[package]] 132 | name = "encoding_rs" 133 | version = "0.8.28" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" 136 | dependencies = [ 137 | "cfg-if 1.0.0", 138 | ] 139 | 140 | [[package]] 141 | name = "env_logger" 142 | version = "0.8.4" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" 145 | dependencies = [ 146 | "atty", 147 | "humantime", 148 | "log", 149 | "regex", 150 | "termcolor", 151 | ] 152 | 153 | [[package]] 154 | name = "fnv" 155 | version = "1.0.7" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 158 | 159 | [[package]] 160 | name = "foreign-types" 161 | version = "0.3.2" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 164 | dependencies = [ 165 | "foreign-types-shared", 166 | ] 167 | 168 | [[package]] 169 | name = "foreign-types-shared" 170 | version = "0.1.1" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 173 | 174 | [[package]] 175 | name = "form_urlencoded" 176 | version = "1.0.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 179 | dependencies = [ 180 | "matches", 181 | "percent-encoding", 182 | ] 183 | 184 | [[package]] 185 | name = "fuchsia-cprng" 186 | version = "0.1.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 189 | 190 | [[package]] 191 | name = "fuchsia-zircon" 192 | version = "0.3.3" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 195 | dependencies = [ 196 | "bitflags", 197 | "fuchsia-zircon-sys", 198 | ] 199 | 200 | [[package]] 201 | name = "fuchsia-zircon-sys" 202 | version = "0.3.3" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 205 | 206 | [[package]] 207 | name = "futures" 208 | version = "0.3.15" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" 211 | dependencies = [ 212 | "futures-channel", 213 | "futures-core", 214 | "futures-executor", 215 | "futures-io", 216 | "futures-sink", 217 | "futures-task", 218 | "futures-util", 219 | ] 220 | 221 | [[package]] 222 | name = "futures-channel" 223 | version = "0.3.15" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" 226 | dependencies = [ 227 | "futures-core", 228 | "futures-sink", 229 | ] 230 | 231 | [[package]] 232 | name = "futures-core" 233 | version = "0.3.15" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" 236 | 237 | [[package]] 238 | name = "futures-executor" 239 | version = "0.3.15" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" 242 | dependencies = [ 243 | "futures-core", 244 | "futures-task", 245 | "futures-util", 246 | ] 247 | 248 | [[package]] 249 | name = "futures-io" 250 | version = "0.3.15" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" 253 | 254 | [[package]] 255 | name = "futures-macro" 256 | version = "0.3.15" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" 259 | dependencies = [ 260 | "autocfg", 261 | "proc-macro-hack", 262 | "proc-macro2", 263 | "quote", 264 | "syn", 265 | ] 266 | 267 | [[package]] 268 | name = "futures-sink" 269 | version = "0.3.15" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" 272 | 273 | [[package]] 274 | name = "futures-task" 275 | version = "0.3.15" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" 278 | 279 | [[package]] 280 | name = "futures-util" 281 | version = "0.3.15" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" 284 | dependencies = [ 285 | "autocfg", 286 | "futures-channel", 287 | "futures-core", 288 | "futures-io", 289 | "futures-macro", 290 | "futures-sink", 291 | "futures-task", 292 | "memchr", 293 | "pin-project-lite 0.2.6", 294 | "pin-utils", 295 | "proc-macro-hack", 296 | "proc-macro-nested", 297 | "slab", 298 | ] 299 | 300 | [[package]] 301 | name = "getrandom" 302 | version = "0.1.16" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 305 | dependencies = [ 306 | "cfg-if 1.0.0", 307 | "libc", 308 | "wasi 0.9.0+wasi-snapshot-preview1", 309 | ] 310 | 311 | [[package]] 312 | name = "getrandom" 313 | version = "0.2.3" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 316 | dependencies = [ 317 | "cfg-if 1.0.0", 318 | "libc", 319 | "wasi 0.10.2+wasi-snapshot-preview1", 320 | ] 321 | 322 | [[package]] 323 | name = "h2" 324 | version = "0.2.7" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" 327 | dependencies = [ 328 | "bytes 0.5.6", 329 | "fnv", 330 | "futures-core", 331 | "futures-sink", 332 | "futures-util", 333 | "http", 334 | "indexmap", 335 | "slab", 336 | "tokio", 337 | "tokio-util", 338 | "tracing", 339 | "tracing-futures", 340 | ] 341 | 342 | [[package]] 343 | name = "hashbrown" 344 | version = "0.9.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" 347 | 348 | [[package]] 349 | name = "hermit-abi" 350 | version = "0.1.18" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 353 | dependencies = [ 354 | "libc", 355 | ] 356 | 357 | [[package]] 358 | name = "http" 359 | version = "0.2.4" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" 362 | dependencies = [ 363 | "bytes 1.0.1", 364 | "fnv", 365 | "itoa", 366 | ] 367 | 368 | [[package]] 369 | name = "http-body" 370 | version = "0.3.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" 373 | dependencies = [ 374 | "bytes 0.5.6", 375 | "http", 376 | ] 377 | 378 | [[package]] 379 | name = "httparse" 380 | version = "1.4.1" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" 383 | 384 | [[package]] 385 | name = "httpdate" 386 | version = "0.3.2" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" 389 | 390 | [[package]] 391 | name = "humantime" 392 | version = "2.1.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 395 | 396 | [[package]] 397 | name = "hyper" 398 | version = "0.13.10" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" 401 | dependencies = [ 402 | "bytes 0.5.6", 403 | "futures-channel", 404 | "futures-core", 405 | "futures-util", 406 | "h2", 407 | "http", 408 | "http-body", 409 | "httparse", 410 | "httpdate", 411 | "itoa", 412 | "pin-project", 413 | "socket2", 414 | "tokio", 415 | "tower-service", 416 | "tracing", 417 | "want", 418 | ] 419 | 420 | [[package]] 421 | name = "hyper-tls" 422 | version = "0.4.3" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" 425 | dependencies = [ 426 | "bytes 0.5.6", 427 | "hyper", 428 | "native-tls", 429 | "tokio", 430 | "tokio-tls", 431 | ] 432 | 433 | [[package]] 434 | name = "idna" 435 | version = "0.2.3" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 438 | dependencies = [ 439 | "matches", 440 | "unicode-bidi", 441 | "unicode-normalization", 442 | ] 443 | 444 | [[package]] 445 | name = "indexmap" 446 | version = "1.6.2" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" 449 | dependencies = [ 450 | "autocfg", 451 | "hashbrown", 452 | ] 453 | 454 | [[package]] 455 | name = "iovec" 456 | version = "0.1.4" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" 459 | dependencies = [ 460 | "libc", 461 | ] 462 | 463 | [[package]] 464 | name = "ipnet" 465 | version = "2.3.1" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" 468 | 469 | [[package]] 470 | name = "itoa" 471 | version = "0.4.7" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 474 | 475 | [[package]] 476 | name = "js-sys" 477 | version = "0.3.51" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" 480 | dependencies = [ 481 | "wasm-bindgen", 482 | ] 483 | 484 | [[package]] 485 | name = "kernel32-sys" 486 | version = "0.2.2" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 489 | dependencies = [ 490 | "winapi 0.2.8", 491 | "winapi-build", 492 | ] 493 | 494 | [[package]] 495 | name = "lazy_static" 496 | version = "1.4.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 499 | 500 | [[package]] 501 | name = "libc" 502 | version = "0.2.97" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" 505 | 506 | [[package]] 507 | name = "log" 508 | version = "0.4.14" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 511 | dependencies = [ 512 | "cfg-if 1.0.0", 513 | ] 514 | 515 | [[package]] 516 | name = "matches" 517 | version = "0.1.8" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 520 | 521 | [[package]] 522 | name = "md5" 523 | version = "0.7.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 526 | 527 | [[package]] 528 | name = "memchr" 529 | version = "2.4.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" 532 | 533 | [[package]] 534 | name = "mime" 535 | version = "0.3.16" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 538 | 539 | [[package]] 540 | name = "mime_guess" 541 | version = "2.0.3" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" 544 | dependencies = [ 545 | "mime", 546 | "unicase", 547 | ] 548 | 549 | [[package]] 550 | name = "mio" 551 | version = "0.6.23" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" 554 | dependencies = [ 555 | "cfg-if 0.1.10", 556 | "fuchsia-zircon", 557 | "fuchsia-zircon-sys", 558 | "iovec", 559 | "kernel32-sys", 560 | "libc", 561 | "log", 562 | "miow 0.2.2", 563 | "net2", 564 | "slab", 565 | "winapi 0.2.8", 566 | ] 567 | 568 | [[package]] 569 | name = "mio-named-pipes" 570 | version = "0.1.7" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" 573 | dependencies = [ 574 | "log", 575 | "mio", 576 | "miow 0.3.7", 577 | "winapi 0.3.9", 578 | ] 579 | 580 | [[package]] 581 | name = "mio-uds" 582 | version = "0.6.8" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" 585 | dependencies = [ 586 | "iovec", 587 | "libc", 588 | "mio", 589 | ] 590 | 591 | [[package]] 592 | name = "miow" 593 | version = "0.2.2" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" 596 | dependencies = [ 597 | "kernel32-sys", 598 | "net2", 599 | "winapi 0.2.8", 600 | "ws2_32-sys", 601 | ] 602 | 603 | [[package]] 604 | name = "miow" 605 | version = "0.3.7" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 608 | dependencies = [ 609 | "winapi 0.3.9", 610 | ] 611 | 612 | [[package]] 613 | name = "native-tls" 614 | version = "0.2.7" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" 617 | dependencies = [ 618 | "lazy_static", 619 | "libc", 620 | "log", 621 | "openssl", 622 | "openssl-probe", 623 | "openssl-sys", 624 | "schannel", 625 | "security-framework", 626 | "security-framework-sys", 627 | "tempfile", 628 | ] 629 | 630 | [[package]] 631 | name = "net2" 632 | version = "0.2.37" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" 635 | dependencies = [ 636 | "cfg-if 0.1.10", 637 | "libc", 638 | "winapi 0.3.9", 639 | ] 640 | 641 | [[package]] 642 | name = "num_cpus" 643 | version = "1.13.0" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 646 | dependencies = [ 647 | "hermit-abi", 648 | "libc", 649 | ] 650 | 651 | [[package]] 652 | name = "once_cell" 653 | version = "1.8.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" 656 | 657 | [[package]] 658 | name = "openssl" 659 | version = "0.10.35" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" 662 | dependencies = [ 663 | "bitflags", 664 | "cfg-if 1.0.0", 665 | "foreign-types", 666 | "libc", 667 | "once_cell", 668 | "openssl-sys", 669 | ] 670 | 671 | [[package]] 672 | name = "openssl-probe" 673 | version = "0.1.4" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" 676 | 677 | [[package]] 678 | name = "openssl-sys" 679 | version = "0.9.65" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" 682 | dependencies = [ 683 | "autocfg", 684 | "cc", 685 | "libc", 686 | "pkg-config", 687 | "vcpkg", 688 | ] 689 | 690 | [[package]] 691 | name = "percent-encoding" 692 | version = "2.1.0" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 695 | 696 | [[package]] 697 | name = "pin-project" 698 | version = "1.0.7" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" 701 | dependencies = [ 702 | "pin-project-internal", 703 | ] 704 | 705 | [[package]] 706 | name = "pin-project-internal" 707 | version = "1.0.7" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" 710 | dependencies = [ 711 | "proc-macro2", 712 | "quote", 713 | "syn", 714 | ] 715 | 716 | [[package]] 717 | name = "pin-project-lite" 718 | version = "0.1.12" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" 721 | 722 | [[package]] 723 | name = "pin-project-lite" 724 | version = "0.2.6" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" 727 | 728 | [[package]] 729 | name = "pin-utils" 730 | version = "0.1.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 733 | 734 | [[package]] 735 | name = "pkg-config" 736 | version = "0.3.19" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 739 | 740 | [[package]] 741 | name = "ppv-lite86" 742 | version = "0.2.10" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 745 | 746 | [[package]] 747 | name = "proc-macro-hack" 748 | version = "0.5.19" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 751 | 752 | [[package]] 753 | name = "proc-macro-nested" 754 | version = "0.1.7" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" 757 | 758 | [[package]] 759 | name = "proc-macro2" 760 | version = "1.0.27" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 763 | dependencies = [ 764 | "unicode-xid", 765 | ] 766 | 767 | [[package]] 768 | name = "quote" 769 | version = "1.0.9" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 772 | dependencies = [ 773 | "proc-macro2", 774 | ] 775 | 776 | [[package]] 777 | name = "rand" 778 | version = "0.3.23" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" 781 | dependencies = [ 782 | "libc", 783 | "rand 0.4.6", 784 | ] 785 | 786 | [[package]] 787 | name = "rand" 788 | version = "0.4.6" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 791 | dependencies = [ 792 | "fuchsia-cprng", 793 | "libc", 794 | "rand_core 0.3.1", 795 | "rdrand", 796 | "winapi 0.3.9", 797 | ] 798 | 799 | [[package]] 800 | name = "rand" 801 | version = "0.7.3" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 804 | dependencies = [ 805 | "getrandom 0.1.16", 806 | "libc", 807 | "rand_chacha 0.2.2", 808 | "rand_core 0.5.1", 809 | "rand_hc 0.2.0", 810 | ] 811 | 812 | [[package]] 813 | name = "rand" 814 | version = "0.8.4" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 817 | dependencies = [ 818 | "libc", 819 | "rand_chacha 0.3.1", 820 | "rand_core 0.6.3", 821 | "rand_hc 0.3.1", 822 | ] 823 | 824 | [[package]] 825 | name = "rand_chacha" 826 | version = "0.2.2" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 829 | dependencies = [ 830 | "ppv-lite86", 831 | "rand_core 0.5.1", 832 | ] 833 | 834 | [[package]] 835 | name = "rand_chacha" 836 | version = "0.3.1" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 839 | dependencies = [ 840 | "ppv-lite86", 841 | "rand_core 0.6.3", 842 | ] 843 | 844 | [[package]] 845 | name = "rand_core" 846 | version = "0.3.1" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 849 | dependencies = [ 850 | "rand_core 0.4.2", 851 | ] 852 | 853 | [[package]] 854 | name = "rand_core" 855 | version = "0.4.2" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 858 | 859 | [[package]] 860 | name = "rand_core" 861 | version = "0.5.1" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 864 | dependencies = [ 865 | "getrandom 0.1.16", 866 | ] 867 | 868 | [[package]] 869 | name = "rand_core" 870 | version = "0.6.3" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 873 | dependencies = [ 874 | "getrandom 0.2.3", 875 | ] 876 | 877 | [[package]] 878 | name = "rand_hc" 879 | version = "0.2.0" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 882 | dependencies = [ 883 | "rand_core 0.5.1", 884 | ] 885 | 886 | [[package]] 887 | name = "rand_hc" 888 | version = "0.3.1" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 891 | dependencies = [ 892 | "rand_core 0.6.3", 893 | ] 894 | 895 | [[package]] 896 | name = "random-names" 897 | version = "0.1.3" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "2acffd642d440454af8228cfa926bb58491953b3d86c653627ad6ea48f04fbc5" 900 | dependencies = [ 901 | "rand 0.3.23", 902 | ] 903 | 904 | [[package]] 905 | name = "rdrand" 906 | version = "0.4.0" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 909 | dependencies = [ 910 | "rand_core 0.3.1", 911 | ] 912 | 913 | [[package]] 914 | name = "reddsaver" 915 | version = "0.4.0" 916 | dependencies = [ 917 | "base64", 918 | "clap", 919 | "dotenv", 920 | "env_logger", 921 | "futures", 922 | "log", 923 | "md5", 924 | "mime", 925 | "rand 0.7.3", 926 | "random-names", 927 | "reqwest", 928 | "serde", 929 | "serde_json", 930 | "tempfile", 931 | "thiserror", 932 | "tokio", 933 | "url", 934 | "which", 935 | ] 936 | 937 | [[package]] 938 | name = "redox_syscall" 939 | version = "0.2.9" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" 942 | dependencies = [ 943 | "bitflags", 944 | ] 945 | 946 | [[package]] 947 | name = "regex" 948 | version = "1.5.4" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 951 | dependencies = [ 952 | "aho-corasick", 953 | "memchr", 954 | "regex-syntax", 955 | ] 956 | 957 | [[package]] 958 | name = "regex-syntax" 959 | version = "0.6.25" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 962 | 963 | [[package]] 964 | name = "remove_dir_all" 965 | version = "0.5.3" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 968 | dependencies = [ 969 | "winapi 0.3.9", 970 | ] 971 | 972 | [[package]] 973 | name = "reqwest" 974 | version = "0.10.10" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" 977 | dependencies = [ 978 | "base64", 979 | "bytes 0.5.6", 980 | "encoding_rs", 981 | "futures-core", 982 | "futures-util", 983 | "http", 984 | "http-body", 985 | "hyper", 986 | "hyper-tls", 987 | "ipnet", 988 | "js-sys", 989 | "lazy_static", 990 | "log", 991 | "mime", 992 | "mime_guess", 993 | "native-tls", 994 | "percent-encoding", 995 | "pin-project-lite 0.2.6", 996 | "serde", 997 | "serde_json", 998 | "serde_urlencoded", 999 | "tokio", 1000 | "tokio-tls", 1001 | "url", 1002 | "wasm-bindgen", 1003 | "wasm-bindgen-futures", 1004 | "web-sys", 1005 | "winreg", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "ryu" 1010 | version = "1.0.5" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 1013 | 1014 | [[package]] 1015 | name = "schannel" 1016 | version = "0.1.19" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" 1019 | dependencies = [ 1020 | "lazy_static", 1021 | "winapi 0.3.9", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "security-framework" 1026 | version = "2.3.1" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" 1029 | dependencies = [ 1030 | "bitflags", 1031 | "core-foundation", 1032 | "core-foundation-sys", 1033 | "libc", 1034 | "security-framework-sys", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "security-framework-sys" 1039 | version = "2.3.0" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" 1042 | dependencies = [ 1043 | "core-foundation-sys", 1044 | "libc", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "serde" 1049 | version = "1.0.126" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" 1052 | dependencies = [ 1053 | "serde_derive", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "serde_derive" 1058 | version = "1.0.126" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" 1061 | dependencies = [ 1062 | "proc-macro2", 1063 | "quote", 1064 | "syn", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "serde_json" 1069 | version = "1.0.64" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" 1072 | dependencies = [ 1073 | "itoa", 1074 | "ryu", 1075 | "serde", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "serde_urlencoded" 1080 | version = "0.7.0" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" 1083 | dependencies = [ 1084 | "form_urlencoded", 1085 | "itoa", 1086 | "ryu", 1087 | "serde", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "signal-hook-registry" 1092 | version = "1.4.0" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 1095 | dependencies = [ 1096 | "libc", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "slab" 1101 | version = "0.4.3" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" 1104 | 1105 | [[package]] 1106 | name = "socket2" 1107 | version = "0.3.19" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" 1110 | dependencies = [ 1111 | "cfg-if 1.0.0", 1112 | "libc", 1113 | "winapi 0.3.9", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "strsim" 1118 | version = "0.8.0" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1121 | 1122 | [[package]] 1123 | name = "syn" 1124 | version = "1.0.73" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" 1127 | dependencies = [ 1128 | "proc-macro2", 1129 | "quote", 1130 | "unicode-xid", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "tempfile" 1135 | version = "3.2.0" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 1138 | dependencies = [ 1139 | "cfg-if 1.0.0", 1140 | "libc", 1141 | "rand 0.8.4", 1142 | "redox_syscall", 1143 | "remove_dir_all", 1144 | "winapi 0.3.9", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "termcolor" 1149 | version = "1.1.2" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 1152 | dependencies = [ 1153 | "winapi-util", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "textwrap" 1158 | version = "0.11.0" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1161 | dependencies = [ 1162 | "unicode-width", 1163 | ] 1164 | 1165 | [[package]] 1166 | name = "thiserror" 1167 | version = "1.0.25" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" 1170 | dependencies = [ 1171 | "thiserror-impl", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "thiserror-impl" 1176 | version = "1.0.25" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" 1179 | dependencies = [ 1180 | "proc-macro2", 1181 | "quote", 1182 | "syn", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "tinyvec" 1187 | version = "1.2.0" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" 1190 | dependencies = [ 1191 | "tinyvec_macros", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "tinyvec_macros" 1196 | version = "0.1.0" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1199 | 1200 | [[package]] 1201 | name = "tokio" 1202 | version = "0.2.25" 1203 | source = "registry+https://github.com/rust-lang/crates.io-index" 1204 | checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" 1205 | dependencies = [ 1206 | "bytes 0.5.6", 1207 | "fnv", 1208 | "futures-core", 1209 | "iovec", 1210 | "lazy_static", 1211 | "libc", 1212 | "memchr", 1213 | "mio", 1214 | "mio-named-pipes", 1215 | "mio-uds", 1216 | "num_cpus", 1217 | "pin-project-lite 0.1.12", 1218 | "signal-hook-registry", 1219 | "slab", 1220 | "tokio-macros", 1221 | "winapi 0.3.9", 1222 | ] 1223 | 1224 | [[package]] 1225 | name = "tokio-macros" 1226 | version = "0.2.6" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" 1229 | dependencies = [ 1230 | "proc-macro2", 1231 | "quote", 1232 | "syn", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "tokio-tls" 1237 | version = "0.3.1" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" 1240 | dependencies = [ 1241 | "native-tls", 1242 | "tokio", 1243 | ] 1244 | 1245 | [[package]] 1246 | name = "tokio-util" 1247 | version = "0.3.1" 1248 | source = "registry+https://github.com/rust-lang/crates.io-index" 1249 | checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" 1250 | dependencies = [ 1251 | "bytes 0.5.6", 1252 | "futures-core", 1253 | "futures-sink", 1254 | "log", 1255 | "pin-project-lite 0.1.12", 1256 | "tokio", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "tower-service" 1261 | version = "0.3.1" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" 1264 | 1265 | [[package]] 1266 | name = "tracing" 1267 | version = "0.1.26" 1268 | source = "registry+https://github.com/rust-lang/crates.io-index" 1269 | checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" 1270 | dependencies = [ 1271 | "cfg-if 1.0.0", 1272 | "log", 1273 | "pin-project-lite 0.2.6", 1274 | "tracing-core", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "tracing-core" 1279 | version = "0.1.18" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" 1282 | dependencies = [ 1283 | "lazy_static", 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "tracing-futures" 1288 | version = "0.2.5" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" 1291 | dependencies = [ 1292 | "pin-project", 1293 | "tracing", 1294 | ] 1295 | 1296 | [[package]] 1297 | name = "try-lock" 1298 | version = "0.2.3" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1301 | 1302 | [[package]] 1303 | name = "unicase" 1304 | version = "2.6.0" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1307 | dependencies = [ 1308 | "version_check", 1309 | ] 1310 | 1311 | [[package]] 1312 | name = "unicode-bidi" 1313 | version = "0.3.5" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" 1316 | dependencies = [ 1317 | "matches", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "unicode-normalization" 1322 | version = "0.1.19" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 1325 | dependencies = [ 1326 | "tinyvec", 1327 | ] 1328 | 1329 | [[package]] 1330 | name = "unicode-width" 1331 | version = "0.1.8" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 1334 | 1335 | [[package]] 1336 | name = "unicode-xid" 1337 | version = "0.2.2" 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" 1339 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 1340 | 1341 | [[package]] 1342 | name = "url" 1343 | version = "2.2.2" 1344 | source = "registry+https://github.com/rust-lang/crates.io-index" 1345 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1346 | dependencies = [ 1347 | "form_urlencoded", 1348 | "idna", 1349 | "matches", 1350 | "percent-encoding", 1351 | ] 1352 | 1353 | [[package]] 1354 | name = "vcpkg" 1355 | version = "0.2.15" 1356 | source = "registry+https://github.com/rust-lang/crates.io-index" 1357 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1358 | 1359 | [[package]] 1360 | name = "vec_map" 1361 | version = "0.8.2" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1364 | 1365 | [[package]] 1366 | name = "version_check" 1367 | version = "0.9.3" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 1370 | 1371 | [[package]] 1372 | name = "want" 1373 | version = "0.3.0" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1376 | dependencies = [ 1377 | "log", 1378 | "try-lock", 1379 | ] 1380 | 1381 | [[package]] 1382 | name = "wasi" 1383 | version = "0.9.0+wasi-snapshot-preview1" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 1386 | 1387 | [[package]] 1388 | name = "wasi" 1389 | version = "0.10.2+wasi-snapshot-preview1" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 1392 | 1393 | [[package]] 1394 | name = "wasm-bindgen" 1395 | version = "0.2.74" 1396 | source = "registry+https://github.com/rust-lang/crates.io-index" 1397 | checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" 1398 | dependencies = [ 1399 | "cfg-if 1.0.0", 1400 | "serde", 1401 | "serde_json", 1402 | "wasm-bindgen-macro", 1403 | ] 1404 | 1405 | [[package]] 1406 | name = "wasm-bindgen-backend" 1407 | version = "0.2.74" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" 1410 | dependencies = [ 1411 | "bumpalo", 1412 | "lazy_static", 1413 | "log", 1414 | "proc-macro2", 1415 | "quote", 1416 | "syn", 1417 | "wasm-bindgen-shared", 1418 | ] 1419 | 1420 | [[package]] 1421 | name = "wasm-bindgen-futures" 1422 | version = "0.4.24" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" 1425 | dependencies = [ 1426 | "cfg-if 1.0.0", 1427 | "js-sys", 1428 | "wasm-bindgen", 1429 | "web-sys", 1430 | ] 1431 | 1432 | [[package]] 1433 | name = "wasm-bindgen-macro" 1434 | version = "0.2.74" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" 1437 | dependencies = [ 1438 | "quote", 1439 | "wasm-bindgen-macro-support", 1440 | ] 1441 | 1442 | [[package]] 1443 | name = "wasm-bindgen-macro-support" 1444 | version = "0.2.74" 1445 | source = "registry+https://github.com/rust-lang/crates.io-index" 1446 | checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" 1447 | dependencies = [ 1448 | "proc-macro2", 1449 | "quote", 1450 | "syn", 1451 | "wasm-bindgen-backend", 1452 | "wasm-bindgen-shared", 1453 | ] 1454 | 1455 | [[package]] 1456 | name = "wasm-bindgen-shared" 1457 | version = "0.2.74" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" 1460 | 1461 | [[package]] 1462 | name = "web-sys" 1463 | version = "0.3.51" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" 1466 | dependencies = [ 1467 | "js-sys", 1468 | "wasm-bindgen", 1469 | ] 1470 | 1471 | [[package]] 1472 | name = "which" 1473 | version = "4.2.2" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" 1476 | dependencies = [ 1477 | "either", 1478 | "lazy_static", 1479 | "libc", 1480 | ] 1481 | 1482 | [[package]] 1483 | name = "winapi" 1484 | version = "0.2.8" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 1487 | 1488 | [[package]] 1489 | name = "winapi" 1490 | version = "0.3.9" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1493 | dependencies = [ 1494 | "winapi-i686-pc-windows-gnu", 1495 | "winapi-x86_64-pc-windows-gnu", 1496 | ] 1497 | 1498 | [[package]] 1499 | name = "winapi-build" 1500 | version = "0.1.1" 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" 1502 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 1503 | 1504 | [[package]] 1505 | name = "winapi-i686-pc-windows-gnu" 1506 | version = "0.4.0" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1509 | 1510 | [[package]] 1511 | name = "winapi-util" 1512 | version = "0.1.5" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1515 | dependencies = [ 1516 | "winapi 0.3.9", 1517 | ] 1518 | 1519 | [[package]] 1520 | name = "winapi-x86_64-pc-windows-gnu" 1521 | version = "0.4.0" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1524 | 1525 | [[package]] 1526 | name = "winreg" 1527 | version = "0.7.0" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" 1530 | dependencies = [ 1531 | "winapi 0.3.9", 1532 | ] 1533 | 1534 | [[package]] 1535 | name = "ws2_32-sys" 1536 | version = "0.2.1" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" 1539 | dependencies = [ 1540 | "winapi 0.2.8", 1541 | "winapi-build", 1542 | ] 1543 | --------------------------------------------------------------------------------