Skip to main content

ph_dl/
lib.rs

1use indicatif::{ProgressBar, ProgressStyle};
2use regex::Regex;
3use reqwest::{blocking::Client, header, Url};
4use std::{
5    fs,
6    io::{self, copy, Read},
7    path::Path,
8    process,
9};
10
11struct DownloadProgress<R> {
12    inner: R,
13    progress_bar: ProgressBar,
14}
15
16impl<R: Read> Read for DownloadProgress<R> {
17    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
18        self.inner.read(buf).map(|n| {
19            self.progress_bar.inc(n as u64);
20            n
21        })
22    }
23}
24
25pub fn run(url: String, download: bool, show_quality: bool, quality: String) {
26    if !url.contains("playlist") {
27        find_video(url, download, &show_quality, &quality);
28    } else {
29        // It's playlist
30        let content = get_html(&url, false);
31        if content.contains("Error Page Not Found") {
32            eprintln!("Playlist is private, cannot access it. Try making it public");
33            process::exit(1);
34        }
35        find_playlist_videos(download, content);
36    }
37}
38
39fn find_video(url: String, download: bool, show_quality: &bool, quality: &str) {
40    let content = get_html(&url, true);
41
42    let title = {
43        let re = Regex::new("<title>(.*) - Pornhub.com</title>").unwrap();
44        let i = match re.captures(&content) {
45            Some(i) => i.get(1).unwrap().as_str().to_string(),
46            None => "video".to_string(),
47        };
48        i.replace("&amp;", "&")
49            .replace("&#039;", "'")
50            .replace("/", "╱") // A good resource for finding similar symbols: https://shapecatcher.com/; it uses AI to search Unicode
51    };
52
53    let url = get_direct_url(&content, &show_quality, quality);
54
55    if !show_quality {
56        println!("Direct URL: {}", &url);
57        if download {
58            download_video(&url, &title);
59            println!("Saved");
60        }
61    }
62}
63
64fn find_playlist_videos(download: bool, content: String) {
65    let re_urls = Regex::new(
66        r#"<a href="/view_video\.php\?viewkey=(ph[0-9a-ö]+)&pkey=[0-9]+" title=".*".*class="fade"#,
67    )
68    .unwrap();
69
70    // Empty variables for quality
71    let show_quality = false;
72    let quality = "none".to_string();
73
74    for url in re_urls.captures_iter(&content) {
75        let url = format!(
76            "https://www.pornhub.com/view_video.php?viewkey={}",
77            url.get(1).unwrap().as_str().to_string()
78        );
79        find_video(url, download, &show_quality, &quality);
80    }
81}
82
83fn download_video(url: &str, name: &str) {
84    // Get file size in bytes
85    let total_size: u64 = {
86        let resp = ureq::head(url).call();
87        resp.header("content-length").unwrap().parse().unwrap()
88    };
89
90    let name = format!("{}.mp4", name);
91
92    let url = Url::parse(url).unwrap();
93    let client = Client::new();
94
95    let mut request = client.get(url.as_str());
96    let pb = ProgressBar::new(total_size);
97    pb.set_style(ProgressStyle::default_bar()
98        .template("{spinner:.yellow} [{elapsed_precise}] [{bar:40.yellow/blue}] {bytes}/{total_bytes} ({eta})")
99        .progress_chars("#>-"));
100
101    let mut file = Path::new(&name);
102
103    if file.exists() {
104        let size = file.metadata().unwrap().len();
105        request = request.header(header::RANGE, format!("bytes={}-", size));
106        pb.inc(size);
107    }
108
109    let mut source = DownloadProgress {
110        progress_bar: pb,
111        inner: request.send().unwrap(),
112    };
113
114    let dest_bool = match fs::OpenOptions::new().create(true).append(true).open(&file) {
115        Err(_e) => "err",
116        Ok(_o) => "ok",
117    };
118
119    let mut dest: std::fs::File;
120
121    if dest_bool == "ok" {
122        println!("Saving as: {}", name);
123
124        dest = fs::OpenOptions::new()
125            .create(true)
126            .append(true)
127            .open(&file)
128            .unwrap();
129    } else {
130        // Find filename that doesn't exists
131        let filename: String;
132        if Path::new("video.mp4").exists() {
133            let mut count = 0;
134            loop {
135                if !Path::new(format!("video_{:02}.mp4", count).as_str()).exists() {
136                    break;
137                }
138                count += 1;
139            }
140            filename = format!("video_{:02}.mp4", count);
141        } else {
142            filename = "video.mp4".to_string();
143        }
144        // define file variable again
145        file = Path::new(filename.as_str());
146        dest = fs::OpenOptions::new()
147            .create(true)
148            .append(true)
149            .open(&file)
150            .unwrap();
151
152        println!(
153            "Error occurred, when using video's title. Saving as {}",
154            filename
155        );
156    }
157
158    let _ = copy(&mut source, &mut dest).unwrap();
159}
160
161fn get_html(url: &str, nokia: bool) -> String {
162    if nokia {
163        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";
164        let resp = ureq::request("GET", &url)
165            .set("User-Agent", user_agent)
166            .call();
167
168        resp.into_string().unwrap()
169    } else {
170        let resp = ureq::request("GET", &url).call();
171
172        resp.into_string().unwrap()
173    }
174}
175
176fn get_direct_url(html: &str, show_quality: &bool, quality: &str) -> String {
177    let re_1080p = Regex::new("\"quality_1080p\":\"(.*)\",\"quality_720p\"").unwrap();
178    let re_720p = Regex::new("\"quality_720p\":\"(.*)\",\"quality_240p\"").unwrap();
179    let re_480p = Regex::new("\"quality_480p\":\"(.*)\",\"mediaPriority").unwrap();
180    let re_240p = Regex::new("\"quality_240p\":\"(.*)\",\"quality_480p\"").unwrap();
181
182    // Quality stuff
183    if *show_quality {
184        let mut available_qualities: Vec<String> = Vec::new();
185
186        // Check 1080p
187        if re_1080p.is_match(&html) {
188            available_qualities.push("1 - 1080p".to_string());
189        }
190
191        // Check 720p
192        if re_720p.is_match(&html) {
193            available_qualities.push("2 - 720p".to_string());
194        }
195
196        // Check 480p
197        if re_480p.is_match(&html) {
198            available_qualities.push("3 - 480p".to_string());
199        }
200
201        // Check 240p
202        if re_240p.is_match(&html) {
203            available_qualities.push("4 - 240p".to_string());
204        }
205
206        // Print available qualities and exit
207        for quality in available_qualities {
208            println!("{}", quality);
209        }
210    }
211
212    if quality != "none" {
213        if quality == "1" {
214            let result = match re_1080p.captures(&html) {
215                Some(i) => i.get(1).unwrap().as_str().to_string().replace("\\/", "/"),
216                None => "Error".to_string(),
217            };
218            if result == "Error" {
219                eprintln!("Couldn't find selected quality");
220            }
221            return result;
222        }
223        if quality == "2" {
224            let result = match re_720p.captures(&html) {
225                Some(i) => i.get(1).unwrap().as_str().to_string().replace("\\/", "/"),
226                None => "Error".to_string(),
227            };
228            if result == "Error" {
229                eprintln!("Couldn't find selected quality");
230            }
231            return result;
232        }
233        if quality == "3" {
234            let result = match re_480p.captures(&html) {
235                Some(i) => i.get(1).unwrap().as_str().to_string().replace("\\/", "/"),
236                None => "Error".to_string(),
237            };
238            if result == "Error" {
239                eprintln!("Couldn't find selected quality");
240            }
241            return result;
242        }
243        if quality == "4" {
244            let result = match re_240p.captures(&html) {
245                Some(i) => i.get(1).unwrap().as_str().to_string().replace("\\/", "/"),
246                None => "Error".to_string(),
247            };
248            if result == "Error" {
249                eprintln!("Couldn't find selected quality");
250            }
251            return result;
252        }
253    }
254
255    // Normal stuff
256    // Check that video exists
257    if html.contains("Error Page Not Found") {
258        eprintln!("Video is probably deleted from the ph");
259        process::exit(1);
260    }
261
262    // Find highest quality link
263    let mut result = match re_1080p.captures(&html) {
264        Some(i) => i.get(1).unwrap().as_str().to_string().replace("\\/", "/"),
265        None => "Error".to_string(),
266    };
267    if result == "Error" {
268        result = match re_720p.captures(&html) {
269            Some(i) => i.get(1).unwrap().as_str().to_string().replace("\\/", "/"),
270            None => "Error".to_string(),
271        };
272    }
273    if result == "Error" {
274        result = match re_480p.captures(&html) {
275            Some(i) => i.get(1).unwrap().as_str().to_string().replace("\\/", "/"),
276            None => "Error".to_string(),
277        };
278    }
279    if result == "Error" {
280        result = match re_240p.captures(&html) {
281            Some(i) => i.get(1).unwrap().as_str().to_string().replace("\\/", "/"),
282            None => "Error".to_string(),
283        };
284    }
285
286    // If still error, throw error
287    if result == "Error" {
288        eprintln!("Couldn't find direct download link");
289        process::exit(1);
290    }
291    result
292}