ph-dl 1.3.2

Get highest quality direct link to download PH videos, or download them automatically.
Documentation
use indicatif::{ProgressBar, ProgressStyle};
use regex::Regex;
use reqwest::{blocking::Client, header, Url};
use std::{
    fs,
    io::{self, copy, Read},
    path::Path,
    process,
};
use serde_json::Value;

struct DownloadProgress<R> {
    inner: R,
    progress_bar: ProgressBar,
}

impl<R: Read> Read for DownloadProgress<R> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.inner.read(buf).map(|n| {
            self.progress_bar.inc(n as u64);
            n
        })
    }
}

pub fn run(url: String, download: bool, show_quality: bool, quality: String) {
    if !url.contains("playlist") {
        find_video(url, download, &show_quality, &quality);
    } else {
        // It's playlist
        let content = get_html(&url, false);
        if content.contains("Error Page Not Found") {
            eprintln!("Playlist is private, cannot access it. Try making it public");
            process::exit(1);
        }
        find_playlist_videos(download, content);
    }
}

fn find_video(url: String, download: bool, show_quality: &bool, quality: &str) {
    let content = get_html(&url, true);

    let title = {
        let re = Regex::new("<title>(.*) - Pornhub.com</title>").unwrap();
        let i = match re.captures(&content) {
            Some(i) => i.get(1).unwrap().as_str().to_string(),
            None => "video".to_string(),
        };
        i.replace("&amp;", "&")
            .replace("&#039;", "'")
            .replace("/", "") // A good resource for finding similar symbols: https://shapecatcher.com/; it uses AI to search Unicode
    };

    let url = get_direct_url(&content, &show_quality, quality);

    if !show_quality {
        if download {
            download_video(&url, &title);
            println!("Saved");
        }
        else {
            println!("Direct URL: {}", &url);
        }
    }
}

fn find_playlist_videos(download: bool, content: String) {
    let re_urls = Regex::new(
        r#"<a href="/view_video\.php\?viewkey=(ph[0-9a-ö]+)&pkey=[0-9]+" title=".*".*class="fade"#,
    )
    .unwrap();

    // Empty variables for quality
    let show_quality = false;
    let quality = "none".to_string();

    for url in re_urls.captures_iter(&content) {
        let url = format!(
            "https://www.pornhub.com/view_video.php?viewkey={}",
            url.get(1).unwrap().as_str().to_string()
        );
        find_video(url, download, &show_quality, &quality);
    }
}

fn download_video(url: &str, name: &str) {
    // Get file size in bytes
    let total_size: u64 = {
        let resp = ureq::head(url).call();
        resp.header("content-length").unwrap().parse().unwrap()
    };

    let name = format!("{}.mp4", name);

    let url = Url::parse(url).unwrap();
    let client = Client::new();

    let mut request = client.get(url.as_str());
    let pb = ProgressBar::new(total_size);
    pb.set_style(ProgressStyle::default_bar()
        .template("{spinner:.yellow} [{elapsed_precise}] [{bar:40.yellow/blue}] {bytes}/{total_bytes} ({eta})")
        .progress_chars("#>-"));

    let mut file = Path::new(&name);

    if file.exists() {
        let size = file.metadata().unwrap().len();
        request = request.header(header::RANGE, format!("bytes={}-", size));
        pb.inc(size);
    }

    let mut source = DownloadProgress {
        progress_bar: pb,
        inner: request.send().unwrap(),
    };

    let dest_bool = match fs::OpenOptions::new().create(true).append(true).open(&file) {
        Err(_e) => "err",
        Ok(_o) => "ok",
    };

    let mut dest: std::fs::File;

    if dest_bool == "ok" {
        println!("Saving as: {}", name);

        dest = fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(&file)
            .unwrap();
    } else {
        // Find filename that doesn't exists
        let filename: String;
        if Path::new("video.mp4").exists() {
            let mut count = 0;
            loop {
                if !Path::new(format!("video_{:02}.mp4", count).as_str()).exists() {
                    break;
                }
                count += 1;
            }
            filename = format!("video_{:02}.mp4", count);
        } else {
            filename = "video.mp4".to_string();
        }
        // define file variable again
        file = Path::new(filename.as_str());
        dest = fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(&file)
            .unwrap();

        println!(
            "Error occurred, when using video's title. Saving as {}",
            filename
        );
    }

    let _ = copy(&mut source, &mut dest).unwrap();
}

fn get_html(url: &str, nokia: bool) -> String {
    if nokia {
        let user_agent = "Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 635) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537";
        let resp = ureq::request("GET", &url)
            .set("User-Agent", user_agent)
            .call();

        resp.into_string().unwrap()
    } else {
        let resp = ureq::request("GET", &url).call();

        resp.into_string().unwrap()
    }
}

fn get_direct_url(html: &str, show_quality: &bool, quality: &str) -> String {
    let parsed: Value = {
        let re = Regex::new("var qualityItems_.* = (\\[.*\\}\\]);").unwrap();
        let i = match re.captures(&html) {
            Some(i) => i.get(1).unwrap().as_str(),
            None => "error",
        };
        if i == "error" {
            eprintln!("Download button is probably missing for this video");
            process::exit(1);
        }
        serde_json::from_str(i).unwrap()
    };
    // Get len manually
    let parsed_len: usize = {
        let mut counter = 0;
        loop {
            match parsed[counter]["id"] {
                Value::String(_) => {counter += 1;},
                _ => { break; },
            };
        }
        counter
    };
   
    // Find all qualities
    let mut qualities: Vec<String> = vec![
        "".to_string(), // 1080p
        "".to_string(), // 720P
        "".to_string(), // 480P
        "".to_string(), // 240P
    ];
    let mut counter = 0;
    while counter < parsed_len {
        if parsed[counter]["id"] == "quality720p" {
            qualities[1] = parsed[counter]["url"].to_string().replace("\"", "");
            counter += 1;
            continue;
        }
        if parsed[counter]["id"] == "quality480p" {
            qualities[2] = parsed[counter]["url"].to_string().replace("\"", "");
            counter += 1;
            continue;
        }
        if parsed[counter]["id"] == "quality240p" {
            qualities[3] = parsed[counter]["url"].to_string().replace("\"", "");
            counter += 1;
            continue;
        }
        counter += 1;
    }
    // Find 1080p manually
    let re_1080p = Regex::new("\"quality\":\"720\"\\},\\{\"defaultQuality\":false,\"format\":\"mp4\",\"videoUrl\":\"(.*)\",\"quality\":\"1080\"\\}\\]").unwrap();
    qualities[0] = match re_1080p.captures(html) {
        Some(i) => i.get(1).unwrap().as_str().to_string().replace("\\/", "/"),
        None => "".to_string(),
    };

    // Print qualities if requested
    if *show_quality {
        let mut available_qualities: Vec<String> = Vec::new();

        // Check 1080p
        if qualities[0] != "" {
            available_qualities.push("1 - 1080p".to_string());
        }

        // Check 720p
        if qualities[1] != "" {
            available_qualities.push("2 - 720p".to_string());
        }

        // Check 480p
        if qualities[2] != "" {
            available_qualities.push("3 - 480p".to_string());
        }

        // Check 240p
        if qualities[3] != "" {
            available_qualities.push("4 - 240p".to_string());
        }

        // Print available qualities and exit
        for quality in available_qualities {
            println!("{}", quality);
        }
    }

    if quality != "none" {
        return if quality == "1" && qualities[0] != "" {
            qualities[0].to_string()
        }
        else if quality == "2" && qualities[1] != "" {
            qualities[1].to_string()
        }
        else if quality == "3" && qualities[2] != "" {
            qualities[2].to_string()
        }
        else if quality == "4" && qualities[3] != "" {
            qualities[3].to_string()
        }
        else {
            eprintln!("Couldn't find selected quality");
            "Error".to_string()
        }
    }

    // Normal stuff
    // Check that video page exists
    if html.contains("Error Page Not Found") {
        eprintln!("Video is probably deleted from the ph");
        process::exit(1);
    }

    // Find highest quality link
    return if qualities[0] != "" {
        qualities[0].to_string()
    }
    else if qualities[1] != "" {
        qualities[1].to_string()
    }
    else if qualities[2] != "" {
        qualities[2].to_string()
    }
    else if qualities[3] != "" {
        qualities[3].to_string()
    }
    else {
        eprintln!("Couldn't find direct download link");
        process::exit(1);
    }
}