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 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("&", "&")
49 .replace("'", "'")
50 .replace("/", "╱") };
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 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 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 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 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 if *show_quality {
184 let mut available_qualities: Vec<String> = Vec::new();
185
186 if re_1080p.is_match(&html) {
188 available_qualities.push("1 - 1080p".to_string());
189 }
190
191 if re_720p.is_match(&html) {
193 available_qualities.push("2 - 720p".to_string());
194 }
195
196 if re_480p.is_match(&html) {
198 available_qualities.push("3 - 480p".to_string());
199 }
200
201 if re_240p.is_match(&html) {
203 available_qualities.push("4 - 240p".to_string());
204 }
205
206 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 if html.contains("Error Page Not Found") {
258 eprintln!("Video is probably deleted from the ph");
259 process::exit(1);
260 }
261
262 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 result == "Error" {
288 eprintln!("Couldn't find direct download link");
289 process::exit(1);
290 }
291 result
292}