Compare commits
204 Commits
97066a184a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a08d2afe7 | ||
|
|
5224a2eb47 | ||
|
|
e7fb0ed723 | ||
|
|
6a7bc68849 | ||
|
|
27e2bcdbba | ||
|
|
182eb8ac01 | ||
|
|
e2f3bc2ecb | ||
|
|
4f9c7835bf | ||
|
|
87b9d20240 | ||
|
|
708560d2e8 | ||
|
|
cacd45d893 | ||
|
|
602dbe50f0 | ||
|
|
cce6104df3 | ||
|
|
34992242b7 | ||
|
|
aaff7d00c6 | ||
|
|
eb49998593 | ||
|
|
cf04441a69 | ||
|
|
6fac9d6d45 | ||
|
|
2edb12a024 | ||
|
|
7f3ae83b1b | ||
|
|
0b3f1fdc1d | ||
|
|
792e246121 | ||
|
|
0fc3bed6a7 | ||
|
|
c0368b2876 | ||
|
|
4a7528c516 | ||
|
|
97eeccf2bd | ||
|
|
5ab2afa967 | ||
|
|
262b908692 | ||
|
|
89eecbe790 | ||
|
|
27bb3daec4 | ||
|
|
f1eb3c236b | ||
|
|
e7854ac1ac | ||
|
|
ca67eff142 | ||
|
|
0e347234b3 | ||
|
|
11c8c1a48f | ||
|
|
6536fb13b3 | ||
|
|
9789afb12b | ||
|
|
b986faa1d4 | ||
|
|
7124b388fa | ||
|
|
632931f515 | ||
|
|
9739560c03 | ||
|
|
80d874a004 | ||
|
|
64dc7455ee | ||
|
|
9e30eedc77 | ||
|
|
75e28608bd | ||
|
|
e22a3f2d6d | ||
|
|
07b812be64 | ||
|
|
61e38caed5 | ||
|
|
e5a6c8decc | ||
|
|
d856ade32b | ||
|
|
2de6a7d42b | ||
|
|
39e38249b7 | ||
|
|
e924c89573 | ||
|
|
3f57569511 | ||
|
|
23190ee05c | ||
|
|
12053ce6db | ||
|
|
5522f2e37d | ||
|
|
8f885c79d4 | ||
|
|
d7e7f70bd2 | ||
|
|
0e02a1b821 | ||
|
|
cafb990fd4 | ||
|
|
53ac33f856 | ||
|
|
ef57172fdd | ||
|
|
f91f06c45e | ||
|
|
ee6919315b | ||
|
|
b4b57ccfc7 | ||
|
|
36e549b176 | ||
|
|
85c270b906 | ||
|
|
14671d6842 | ||
|
|
a875cec9f6 | ||
|
|
8d4a357edf | ||
|
|
474a4b7f38 | ||
|
|
35cd6a440f | ||
|
|
d9b505e516 | ||
|
|
2d719ad2d7 | ||
|
|
4d2470e028 | ||
|
|
e79fd15b91 | ||
|
|
f8d382568b | ||
|
|
43c22846c5 | ||
|
|
6c542ce6b4 | ||
|
|
d6b1f5d93f | ||
|
|
df01dc36f7 | ||
|
|
629000ba37 | ||
|
|
d864bc8a4e | ||
|
|
a0e0a8e4b1 | ||
|
|
09c06df163 | ||
|
|
dcb5148da6 | ||
|
|
7dd58ebfc4 | ||
|
|
3c2eba8658 | ||
|
|
12af9a89cd | ||
|
|
8a9baa1552 | ||
|
|
d4b96a70ee | ||
|
|
ef4a86d3ca | ||
|
|
68c5f4971c | ||
|
|
77f6d27f5a | ||
|
|
d930958081 | ||
|
|
8dd46954d6 | ||
|
|
0662512ebf | ||
|
|
b2a07b0392 | ||
|
|
499e528697 | ||
|
|
a6be0f33ef | ||
|
|
983e861a63 | ||
|
|
7c73601954 | ||
|
|
43a2d09a55 | ||
|
|
67e7b96758 | ||
|
|
efedc0e6e4 | ||
|
|
ef625527a2 | ||
|
|
28a4c57616 | ||
|
|
d84cc715a8 | ||
|
|
5b2a7430bc | ||
|
|
81b967e811 | ||
|
|
f9ccdd8b33 | ||
|
|
20d069f01f | ||
|
|
37d11034d8 | ||
|
|
29aa6fc007 | ||
|
|
259106fa13 | ||
|
|
23f6571911 | ||
|
|
8e6f115871 | ||
|
|
53737784b7 | ||
|
|
154e3a149e | ||
|
|
611c8a99e7 | ||
|
|
92e43d2449 | ||
|
|
4be7ccc6e1 | ||
|
|
39acd8ef96 | ||
|
|
661a28b6ac | ||
|
|
3f98a9eecb | ||
|
|
3e4f5526b0 | ||
|
|
4d80b827e1 | ||
|
|
b75a2cc298 | ||
|
|
f12f50e787 | ||
|
|
d9fed99104 | ||
|
|
025ee713e3 | ||
|
|
913472ebfb | ||
|
|
584abfd431 | ||
|
|
1b4bc6cb13 | ||
|
|
8effce7c2b | ||
|
|
428307f52d | ||
|
|
5e5838debf | ||
|
|
a096ec66f2 | ||
|
|
c17590ccb3 | ||
|
|
436e33d015 | ||
|
|
8a57d0c2bf | ||
|
|
c7e67a3cba | ||
|
|
31adceb3e9 | ||
|
|
edb23b62ba | ||
|
|
ff18f3eb34 | ||
|
|
c3f994ccbb | ||
|
|
9caec79427 | ||
|
|
7d514895cd | ||
|
|
8f5fc41bd2 | ||
|
|
437deb388b | ||
|
|
23a643b9dc | ||
|
|
6434939a69 | ||
|
|
4f1b58d583 | ||
|
|
bb5f610c60 | ||
|
|
c673a1c22b | ||
|
|
e7b10cbe4f | ||
|
|
53a4c62bfe | ||
|
|
44b42170be | ||
|
|
f10491dd73 | ||
|
|
09adedae72 | ||
|
|
2a32690894 | ||
|
|
59d30695e9 | ||
|
|
c05991ee23 | ||
|
|
61aa6a966e | ||
|
|
24e4c5dfd7 | ||
|
|
c135f60894 | ||
|
|
746147c7c0 | ||
|
|
812d1c205f | ||
|
|
79b833b857 | ||
|
|
87965d4659 | ||
|
|
c0d8b8b2f4 | ||
|
|
0ba1c62daa | ||
|
|
6dd63ae620 | ||
|
|
fef5ee5796 | ||
|
|
07281e8360 | ||
|
|
ee8abaed8d | ||
|
|
d01436ab6a | ||
|
|
caed5088f5 | ||
|
|
b383a36077 | ||
|
|
0f2983ca15 | ||
|
|
f7a836c353 | ||
|
|
e80eb79613 | ||
|
|
750be251c0 | ||
|
|
49ca76ab48 | ||
|
|
2248d11d3e | ||
|
|
5dcc046005 | ||
|
|
9f4e8eeff0 | ||
|
|
7c645bf653 | ||
|
|
60e3db9a8e | ||
|
|
7185d89a64 | ||
|
|
8add6f44aa | ||
|
|
88f1126ec5 | ||
|
|
7d8f0d1b4f | ||
|
|
8017263d21 | ||
|
|
0a1516b82a | ||
|
|
58871d8db9 | ||
|
|
e67025e104 | ||
|
|
ca44f08393 | ||
|
|
5b544dbbf6 | ||
|
|
102fc37683 | ||
|
|
944746bf12 | ||
|
|
673458b630 | ||
|
|
6405596fb8 |
27
Cargo.toml
27
Cargo.toml
@@ -11,11 +11,26 @@ env_logger = "0.11.8"
|
|||||||
error-chain = "0.12.4"
|
error-chain = "0.12.4"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
htmlentity = "1.3.2"
|
htmlentity = "1.3.2"
|
||||||
ntex = { version = "2.0", features = ["tokio"] }
|
ntex = { version = "2.15.1", features = ["tokio"] }
|
||||||
ntex-files = "2.0.0"
|
ntex-files = "2.0.0"
|
||||||
serde = "1.0.219"
|
serde = "1.0.228"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.145"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1.49", features = ["full"] }
|
||||||
wreq = { version = "5", features = ["full"] }
|
wreq = { version = "5.3.0", features = ["full", "cookies", "multipart"] }
|
||||||
wreq-util = "2"
|
wreq-util = "2"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.3.2"
|
||||||
|
capitalize = "0.3.4"
|
||||||
|
url = "2.5.7"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
scraper = "0.24.0"
|
||||||
|
once_cell = "1.21.3"
|
||||||
|
rustc-hash = "2.1.1"
|
||||||
|
async-trait = "0.1"
|
||||||
|
regex = "1.12.2"
|
||||||
|
titlecase = "3.6.0"
|
||||||
|
dashmap = "6.1.0"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unexpected_cfgs = "allow"
|
||||||
|
# Or keep it as a warning but whitelist the cfg:
|
||||||
|
# unexpected_cfgs = { level = "warn", check-cfg = ['cfg(has_error_description_deprecated)'] }
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -5,3 +5,48 @@ Rust based hottub server
|
|||||||
the following URL:
|
the following URL:
|
||||||
|
|
||||||
[hottub.spacemoehre.de](hottub://source?url=hottub.spacemoehre.de)
|
[hottub.spacemoehre.de](hottub://source?url=hottub.spacemoehre.de)
|
||||||
|
|
||||||
|
## build it yourself
|
||||||
|
|
||||||
|
Get, Build and Host the docker image:
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://gitea.spacemoehre.de/simon/hottub
|
||||||
|
sudo docker build hottub
|
||||||
|
cd hottub && cargo build --release
|
||||||
|
nano docker-compose.yml # adjust compose file
|
||||||
|
sudo docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify setup, replace the url with your setup url
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -v http://127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
->
|
||||||
|
|
||||||
|
```
|
||||||
|
* Trying 127.0.0.1:80...
|
||||||
|
* Connected to 127.0.0.1 (127.0.0.1) port 80
|
||||||
|
> GET / HTTP/1.1
|
||||||
|
> Host: 127.0.0.1:80
|
||||||
|
> User-Agent: curl/8.5.0
|
||||||
|
> Accept: */*
|
||||||
|
>
|
||||||
|
< HTTP/1.1 302 Found
|
||||||
|
< content-length: 0
|
||||||
|
< location: hottub://source?url=127.0.0.1:80
|
||||||
|
< date: Mon, 29 Sep 2025 14:58:15 GMT
|
||||||
|
<
|
||||||
|
* Connection #0 to host 127.0.0.1 left intact
|
||||||
|
```
|
||||||
|
|
||||||
|
make sure that you get a code 302 to a `hottub://` url
|
||||||
|
|
||||||
|
## Update via git pull
|
||||||
|
|
||||||
|
To Update (i.e. for new supported sites) do
|
||||||
|
```
|
||||||
|
cd /path/to/hottub && git pull && cargo build --release && sudo docker container restart hottub
|
||||||
|
```
|
||||||
281
archive/hentaimoon.rs
Normal file
281
archive/hentaimoon.rs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::env;
|
||||||
|
use std::vec;
|
||||||
|
use wreq::{Client, Proxy};
|
||||||
|
use wreq_util::Emulation;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HentaimoonProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl HentaimoonProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
HentaimoonProvider {
|
||||||
|
url: "https://hentai-moon.com".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"popular" => "/most-popular",
|
||||||
|
"top-rated" => "/top-rated",
|
||||||
|
_ => "/latest-updates/",
|
||||||
|
};
|
||||||
|
|
||||||
|
let list_str = match sort {
|
||||||
|
"popular" => "list_videos_common_videos_list",
|
||||||
|
"top-rated" => "list_videos_common_videos_list",
|
||||||
|
_ => "list_videos_most_recent_videos",
|
||||||
|
};
|
||||||
|
|
||||||
|
let video_url = format!("{}{}?mode=async^&function=get_block^&block_id={}^&from={}", self.url, sort_string, list_str, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
||||||
|
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
||||||
|
|
||||||
|
let mut response = client.get(video_url.clone())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
if response.status().is_redirection(){
|
||||||
|
response = client.get(response.headers()["Location"].to_str().unwrap())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
}
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response.text().await?;
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
} else {
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let flare = Flaresolverr::new(flare_url);
|
||||||
|
let result = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: video_url.clone(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let video_items = match result {
|
||||||
|
Ok(res) => {
|
||||||
|
// println!("FlareSolverr response: {}", res);
|
||||||
|
self.get_video_items_from_html(res.solution.response)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error solving FlareSolverr: {}", e);
|
||||||
|
return Err("Failed to solve FlareSolverr".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_lowercase().trim().replace(" ", "-");
|
||||||
|
let video_url = format!("{}/search/{}/?mode=async&function=get_block&block_id=list_videos_videos_list_search_result&q={}&category_ids=&sort_by=&from_videos={}&from_albums={}&", self.url, search_string, search_string, page, page);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
||||||
|
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
||||||
|
|
||||||
|
let mut response = client.get(video_url.clone())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
|
||||||
|
if response.status().is_redirection(){
|
||||||
|
|
||||||
|
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response.text().await?;
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
} else {
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let flare = Flaresolverr::new(flare_url);
|
||||||
|
let result = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: video_url.clone(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let video_items = match result {
|
||||||
|
Ok(res) => self.get_video_items_from_html(res.solution.response),
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error solving FlareSolverr: {}", e);
|
||||||
|
return Err("Failed to solve FlareSolverr".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("<div class=\"pagination\"").collect::<Vec<&str>>()[0]
|
||||||
|
.split("<div class=\"item \">")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0].to_string();
|
||||||
|
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let raw_duration = video_segment.split("<div class=\"duration\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let thumb = video_segment.split("<img class=\"thumb ").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-original=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let views_part = video_segment.split("<div class=\"views\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let views = parse_abbreviated_number(&views_part).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"hentaimoon".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.views(views)
|
||||||
|
;
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Provider for HentaimoonProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = options;
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q,)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ impl SpankbangProvider {
|
|||||||
let old_items = match cache.get(&url) {
|
let old_items = match cache.get(&url) {
|
||||||
Some((time, items)) => {
|
Some((time, items)) => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||||
println!("Cache hit for URL: {}", url);
|
// println!("Cache hit for URL: {}", url);
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
@@ -123,7 +123,7 @@ impl SpankbangProvider {
|
|||||||
let old_items = match cache.get(&url) {
|
let old_items = match cache.get(&url) {
|
||||||
Some((time, items)) => {
|
Some((time, items)) => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||||
println!("Cache hit for URL: {}", url);
|
// println!("Cache hit for URL: {}", url);
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
@@ -336,7 +336,6 @@ impl SpankbangProvider {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
let raw_videos = raw_videos_vec[1..].to_vec();
|
let raw_videos = raw_videos_vec[1..].to_vec();
|
||||||
println!("Found {} video items", raw_videos.len());
|
|
||||||
let futures = raw_videos.into_iter().map(|el| self.parse_video_item(el.to_string(), client, cookies.clone(), pool.clone()));
|
let futures = raw_videos.into_iter().map(|el| self.parse_video_item(el.to_string(), client, cookies.clone(), pool.clone()));
|
||||||
let results: Vec<Result<VideoItem>> = join_all(futures).await;
|
let results: Vec<Result<VideoItem>> = join_all(futures).await;
|
||||||
let video_items: Vec<VideoItem> = results
|
let video_items: Vec<VideoItem> = results
|
||||||
@@ -6,18 +6,53 @@ services:
|
|||||||
container_name: hottub
|
container_name: hottub
|
||||||
entrypoint: supervisord
|
entrypoint: supervisord
|
||||||
command: ["-c", "/app/supervisord/supervisord.conf"]
|
command: ["-c", "/app/supervisord/supervisord.conf"]
|
||||||
|
# In case you dont want the burpsuite proxy and only wanna run the server in the docker without compiling outside:
|
||||||
|
# entrypoint: cargo
|
||||||
|
# command: ["run"]
|
||||||
volumes:
|
volumes:
|
||||||
- /path/to/hottub:/app
|
- /path/to/hottub:/app # REPLACE
|
||||||
environment:
|
environment:
|
||||||
- RUST_LOG=info
|
- RUST_LOG=info
|
||||||
- BURP_URL=http://127.0.0.1:8081
|
- BURP_URL=http://127.0.0.1:8081 # local burpsuite proxy for crawler analysis
|
||||||
|
- PROXY=0 # 1 for enable, else disabled
|
||||||
|
- DATABASE_URL=hottub.db # sqlite db to store hard to get videos for easy access
|
||||||
|
- FLARE_URL=http://flaresolverr:8191/v1 # flaresolverr to get around cloudflare 403 codes
|
||||||
|
- DOMAIN=hottub.spacemoehre.de # optional for the 302 forward on "/" to
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
ports:
|
ports:
|
||||||
- 6901:6901
|
- 80:18080
|
||||||
- 8080:18080
|
- 6901:6901 # vnc port to access burpsuite
|
||||||
|
- 8081:8080 # burpsuite port of http(s) proxy
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m" # Maximum size of each log file (e.g., 10MB)
|
||||||
|
max-file: "3" # Maximum number of log files to keep
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:18080/api/status | grep -q 200"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 1s
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 65536
|
||||||
|
hard: 65536
|
||||||
|
|
||||||
|
# flaresolverr to bypass cloudflare protections
|
||||||
|
flaresolverr:
|
||||||
|
container_name: flaresolverr
|
||||||
|
ports:
|
||||||
|
- 8191:8191
|
||||||
|
restart: unless-stopped
|
||||||
|
image: alexfozor/flaresolverr:pr-1300-experimental # master branches dont work as good as this one
|
||||||
|
environment:
|
||||||
|
- LOG_LEVEL=debug
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m" # Maximum size of each log file (e.g., 10MB)
|
||||||
|
max-file: "3" # Maximum number of log files to keep
|
||||||
|
|
||||||
|
|
||||||
networks:
|
|
||||||
traefik_default:
|
|
||||||
external: true
|
|
||||||
426
sf-symbols.md
Normal file
426
sf-symbols.md
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
# sf-symbols-online
|
||||||
|
|
||||||
|
This table is for GitHub's dark mode. For light mode visit [README.md](./README.md).
|
||||||
|
|
||||||
|
<!--prettier-ignore-start-->
|
||||||
|
| Glyph | Name | Glyph | Name | Glyph | Name | Glyph | Name |
|
||||||
|
|----|-------|----|-------|----|-------|----|-------|
|
||||||
|
| <img alt='square.and.arrow.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.arrow.up.png'> | square.and.arrow.up | <img alt='square.and.arrow.up.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.arrow.up.fill.png'> | square.and.arrow.up.fill | <img alt='square.and.arrow.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.arrow.down.png'> | square.and.arrow.down | <img alt='square.and.arrow.down.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.arrow.down.fill.png'> | square.and.arrow.down.fill | <img alt='square.and.arrow.up.on.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.arrow.up.on.square.png'> | square.and.arrow.up.on.square |
|
||||||
|
| <img alt='square.and.arrow.up.on.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.arrow.up.on.square.fill.png'> | square.and.arrow.up.on.square.fill | <img alt='square.and.arrow.down.on.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.arrow.down.on.square.png'> | square.and.arrow.down.on.square | <img alt='square.and.arrow.down.on.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.arrow.down.on.square.fill.png'> | square.and.arrow.down.on.square.fill | <img alt='pencil' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pencil.png'> | pencil |
|
||||||
|
| <img alt='pencil.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pencil.circle.png'> | pencil.circle | <img alt='pencil.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pencil.circle.fill.png'> | pencil.circle.fill | <img alt='pencil.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pencil.slash.png'> | pencil.slash | <img alt='square.and.pencil' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.pencil.png'> | square.and.pencil |
|
||||||
|
| <img alt='pencil.and.ellipsis.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pencil.and.ellipsis.rectangle.png'> | pencil.and.ellipsis.rectangle | <img alt='pencil.and.outline' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pencil.and.outline.png'> | pencil.and.outline | <img alt='trash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/trash.png'> | trash | <img alt='trash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/trash.fill.png'> | trash.fill |
|
||||||
|
| <img alt='trash.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/trash.circle.png'> | trash.circle | <img alt='trash.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/trash.circle.fill.png'> | trash.circle.fill | <img alt='trash.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/trash.slash.png'> | trash.slash | <img alt='trash.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/trash.slash.fill.png'> | trash.slash.fill |
|
||||||
|
| <img alt='folder' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/folder.png'> | folder | <img alt='folder.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/folder.fill.png'> | folder.fill | <img alt='folder.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/folder.circle.png'> | folder.circle | <img alt='folder.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/folder.circle.fill.png'> | folder.circle.fill |
|
||||||
|
| <img alt='folder.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/folder.badge.plus.png'> | folder.badge.plus | <img alt='folder.fill.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/folder.fill.badge.plus.png'> | folder.fill.badge.plus | <img alt='folder.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/folder.badge.minus.png'> | folder.badge.minus | <img alt='folder.fill.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/folder.fill.badge.minus.png'> | folder.fill.badge.minus |
|
||||||
|
| <img alt='folder.badge.person.crop' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/folder.badge.person.crop.png'> | folder.badge.person.crop | <img alt='folder.fill.badge.person.crop' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/folder.fill.badge.person.crop.png'> | folder.fill.badge.person.crop | <img alt='paperplane' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/paperplane.png'> | paperplane | <img alt='paperplane.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/paperplane.fill.png'> | paperplane.fill |
|
||||||
|
| <img alt='tray' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tray.png'> | tray | <img alt='tray.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tray.fill.png'> | tray.fill | <img alt='tray.and.arrow.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tray.and.arrow.up.png'> | tray.and.arrow.up | <img alt='tray.and.arrow.up.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tray.and.arrow.up.fill.png'> | tray.and.arrow.up.fill |
|
||||||
|
| <img alt='tray.and.arrow.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tray.and.arrow.down.png'> | tray.and.arrow.down | <img alt='tray.and.arrow.down.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tray.and.arrow.down.fill.png'> | tray.and.arrow.down.fill | <img alt='tray.2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tray.2.png'> | tray.2 | <img alt='tray.2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tray.2.fill.png'> | tray.2.fill |
|
||||||
|
| <img alt='tray.full' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tray.full.png'> | tray.full | <img alt='tray.full.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tray.full.fill.png'> | tray.full.fill | <img alt='archivebox' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/archivebox.png'> | archivebox | <img alt='archivebox.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/archivebox.fill.png'> | archivebox.fill |
|
||||||
|
| <img alt='bin.xmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bin.xmark.png'> | bin.xmark | <img alt='bin.xmark.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bin.xmark.fill.png'> | bin.xmark.fill | <img alt='arrow.up.bin' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.bin.png'> | arrow.up.bin | <img alt='arrow.up.bin.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.bin.fill.png'> | arrow.up.bin.fill |
|
||||||
|
| <img alt='doc' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.png'> | doc | <img alt='doc.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.fill.png'> | doc.fill | <img alt='doc.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.circle.png'> | doc.circle | <img alt='doc.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.circle.fill.png'> | doc.circle.fill |
|
||||||
|
| <img alt='arrow.up.doc' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.doc.png'> | arrow.up.doc | <img alt='arrow.up.doc.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.doc.fill.png'> | arrow.up.doc.fill | <img alt='arrow.down.doc' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.doc.png'> | arrow.down.doc | <img alt='arrow.down.doc.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.doc.fill.png'> | arrow.down.doc.fill |
|
||||||
|
| <img alt='doc.text' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.text.png'> | doc.text | <img alt='doc.text.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.text.fill.png'> | doc.text.fill | <img alt='doc.on.doc' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.on.doc.png'> | doc.on.doc | <img alt='doc.on.doc.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.on.doc.fill.png'> | doc.on.doc.fill |
|
||||||
|
| <img alt='doc.on.clipboard' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.on.clipboard.png'> | doc.on.clipboard | <img alt='doc.on.clipboard.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.on.clipboard.fill.png'> | doc.on.clipboard.fill | <img alt='doc.richtext' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.richtext.png'> | doc.richtext | <img alt='doc.plaintext' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.plaintext.png'> | doc.plaintext |
|
||||||
|
| <img alt='doc.append' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.append.png'> | doc.append | <img alt='doc.text.magnifyingglass' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.text.magnifyingglass.png'> | doc.text.magnifyingglass | <img alt='calendar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/calendar.png'> | calendar | <img alt='calendar.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/calendar.circle.png'> | calendar.circle |
|
||||||
|
| <img alt='calendar.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/calendar.circle.fill.png'> | calendar.circle.fill | <img alt='calendar.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/calendar.badge.plus.png'> | calendar.badge.plus | <img alt='calendar.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/calendar.badge.minus.png'> | calendar.badge.minus | <img alt='arrowshape.turn.up.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowshape.turn.up.left.png'> | arrowshape.turn.up.left |
|
||||||
|
| <img alt='arrowshape.turn.up.left.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowshape.turn.up.left.fill.png'> | arrowshape.turn.up.left.fill | <img alt='arrowshape.turn.up.left.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowshape.turn.up.left.circle.png'> | arrowshape.turn.up.left.circle | <img alt='arrowshape.turn.up.left.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowshape.turn.up.left.circle.fill.png'> | arrowshape.turn.up.left.circle.fill | <img alt='arrowshape.turn.up.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowshape.turn.up.right.png'> | arrowshape.turn.up.right |
|
||||||
|
| <img alt='arrowshape.turn.up.right.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowshape.turn.up.right.fill.png'> | arrowshape.turn.up.right.fill | <img alt='arrowshape.turn.up.right.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowshape.turn.up.right.circle.png'> | arrowshape.turn.up.right.circle | <img alt='arrowshape.turn.up.right.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowshape.turn.up.right.circle.fill.png'> | arrowshape.turn.up.right.circle.fill | <img alt='arrowshape.turn.up.left.2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowshape.turn.up.left.2.png'> | arrowshape.turn.up.left.2 |
|
||||||
|
| <img alt='arrowshape.turn.up.left.2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowshape.turn.up.left.2.fill.png'> | arrowshape.turn.up.left.2.fill | <img alt='book' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/book.png'> | book | <img alt='book.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/book.fill.png'> | book.fill | <img alt='book.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/book.circle.png'> | book.circle |
|
||||||
|
| <img alt='book.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/book.circle.fill.png'> | book.circle.fill | <img alt='bookmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bookmark.png'> | bookmark | <img alt='bookmark.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bookmark.fill.png'> | bookmark.fill | <img alt='rosette' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rosette.png'> | rosette |
|
||||||
|
| <img alt='paperclip' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/paperclip.png'> | paperclip | <img alt='paperclip.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/paperclip.circle.png'> | paperclip.circle | <img alt='paperclip.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/paperclip.circle.fill.png'> | paperclip.circle.fill | <img alt='rectangle.and.paperclip' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.and.paperclip.png'> | rectangle.and.paperclip |
|
||||||
|
| <img alt='link' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/link.png'> | link | <img alt='link.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/link.circle.png'> | link.circle | <img alt='link.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/link.circle.fill.png'> | link.circle.fill | <img alt='personalhotspot' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/personalhotspot.png'> | personalhotspot |
|
||||||
|
| <img alt='pencil.tip' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pencil.tip.png'> | pencil.tip | <img alt='pencil.tip.crop.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pencil.tip.crop.circle.png'> | pencil.tip.crop.circle | <img alt='pencil.tip.crop.circle.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pencil.tip.crop.circle.badge.plus.png'> | pencil.tip.crop.circle.badge.plus | <img alt='pencil.tip.crop.circle.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pencil.tip.crop.circle.badge.minus.png'> | pencil.tip.crop.circle.badge.minus |
|
||||||
|
| <img alt='person' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.png'> | person | <img alt='person.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.fill.png'> | person.fill | <img alt='person.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.circle.png'> | person.circle | <img alt='person.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.circle.fill.png'> | person.circle.fill |
|
||||||
|
| <img alt='person.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.badge.plus.png'> | person.badge.plus | <img alt='person.badge.plus.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.badge.plus.fill.png'> | person.badge.plus.fill | <img alt='person.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.badge.minus.png'> | person.badge.minus | <img alt='person.badge.minus.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.badge.minus.fill.png'> | person.badge.minus.fill |
|
||||||
|
| <img alt='person.2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.2.png'> | person.2 | <img alt='person.2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.2.fill.png'> | person.2.fill | <img alt='person.3' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.3.png'> | person.3 | <img alt='person.3.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.3.fill.png'> | person.3.fill |
|
||||||
|
| <img alt='person.crop.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.png'> | person.crop.circle | <img alt='person.crop.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.fill.png'> | person.crop.circle.fill | <img alt='person.crop.circle.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.badge.plus.png'> | person.crop.circle.badge.plus | <img alt='person.crop.circle.fill.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.fill.badge.plus.png'> | person.crop.circle.fill.badge.plus |
|
||||||
|
| <img alt='person.crop.circle.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.badge.minus.png'> | person.crop.circle.badge.minus | <img alt='person.crop.circle.fill.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.fill.badge.minus.png'> | person.crop.circle.fill.badge.minus | <img alt='person.crop.circle.badge.checkmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.badge.checkmark.png'> | person.crop.circle.badge.checkmark | <img alt='person.crop.circle.fill.badge.checkmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.fill.badge.checkmark.png'> | person.crop.circle.fill.badge.checkmark |
|
||||||
|
| <img alt='person.crop.circle.badge.xmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.badge.xmark.png'> | person.crop.circle.badge.xmark | <img alt='person.crop.circle.fill.badge.xmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.fill.badge.xmark.png'> | person.crop.circle.fill.badge.xmark | <img alt='person.crop.circle.badge.exclam' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.badge.exclam.png'> | person.crop.circle.badge.exclam | <img alt='person.crop.circle.fill.badge.exclam' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.circle.fill.badge.exclam.png'> | person.crop.circle.fill.badge.exclam |
|
||||||
|
| <img alt='person.crop.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.square.png'> | person.crop.square | <img alt='person.crop.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.square.fill.png'> | person.crop.square.fill | <img alt='command' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/command.png'> | command | <img alt='option' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/option.png'> | option |
|
||||||
|
| <img alt='alt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/alt.png'> | alt | <img alt='delete.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/delete.right.png'> | delete.right | <img alt='delete.right.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/delete.right.fill.png'> | delete.right.fill | <img alt='clear' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/clear.png'> | clear |
|
||||||
|
| <img alt='clear.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/clear.fill.png'> | clear.fill | <img alt='delete.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/delete.left.png'> | delete.left | <img alt='delete.left.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/delete.left.fill.png'> | delete.left.fill | <img alt='shift' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/shift.png'> | shift |
|
||||||
|
| <img alt='shift.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/shift.fill.png'> | shift.fill | <img alt='capslock' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/capslock.png'> | capslock | <img alt='capslock.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/capslock.fill.png'> | capslock.fill | <img alt='escape' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/escape.png'> | escape |
|
||||||
|
| <img alt='circle.bottomthird.split' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.bottomthird.split.png'> | circle.bottomthird.split | <img alt='power' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/power.png'> | power | <img alt='globe' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/globe.png'> | globe | <img alt='sun.min' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sun.min.png'> | sun.min |
|
||||||
|
| <img alt='sun.min.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sun.min.fill.png'> | sun.min.fill | <img alt='sun.max' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sun.max.png'> | sun.max | <img alt='sun.max.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sun.max.fill.png'> | sun.max.fill | <img alt='sunrise' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sunrise.png'> | sunrise |
|
||||||
|
| <img alt='sunrise.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sunrise.fill.png'> | sunrise.fill | <img alt='sunset' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sunset.png'> | sunset | <img alt='sunset.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sunset.fill.png'> | sunset.fill | <img alt='sun.dust' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sun.dust.png'> | sun.dust |
|
||||||
|
| <img alt='sun.dust.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sun.dust.fill.png'> | sun.dust.fill | <img alt='sun.haze' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sun.haze.png'> | sun.haze | <img alt='sun.haze.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sun.haze.fill.png'> | sun.haze.fill | <img alt='moon' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/moon.png'> | moon |
|
||||||
|
| <img alt='moon.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/moon.fill.png'> | moon.fill | <img alt='moon.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/moon.circle.png'> | moon.circle | <img alt='moon.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/moon.circle.fill.png'> | moon.circle.fill | <img alt='zzz' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/zzz.png'> | zzz |
|
||||||
|
| <img alt='moon.zzz' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/moon.zzz.png'> | moon.zzz | <img alt='moon.zzz.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/moon.zzz.fill.png'> | moon.zzz.fill | <img alt='sparkles' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sparkles.png'> | sparkles | <img alt='moon.stars' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/moon.stars.png'> | moon.stars |
|
||||||
|
| <img alt='moon.stars.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/moon.stars.fill.png'> | moon.stars.fill | <img alt='cloud' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.png'> | cloud | <img alt='cloud.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.fill.png'> | cloud.fill | <img alt='cloud.drizzle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.drizzle.png'> | cloud.drizzle |
|
||||||
|
| <img alt='cloud.drizzle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.drizzle.fill.png'> | cloud.drizzle.fill | <img alt='cloud.rain' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.rain.png'> | cloud.rain | <img alt='cloud.rain.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.rain.fill.png'> | cloud.rain.fill | <img alt='cloud.heavyrain' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.heavyrain.png'> | cloud.heavyrain |
|
||||||
|
| <img alt='cloud.heavyrain.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.heavyrain.fill.png'> | cloud.heavyrain.fill | <img alt='cloud.fog' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.fog.png'> | cloud.fog | <img alt='cloud.fog.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.fog.fill.png'> | cloud.fog.fill | <img alt='cloud.hail' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.hail.png'> | cloud.hail |
|
||||||
|
| <img alt='cloud.hail.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.hail.fill.png'> | cloud.hail.fill | <img alt='cloud.snow' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.snow.png'> | cloud.snow | <img alt='cloud.snow.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.snow.fill.png'> | cloud.snow.fill | <img alt='cloud.sleet' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.sleet.png'> | cloud.sleet |
|
||||||
|
| <img alt='cloud.sleet.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.sleet.fill.png'> | cloud.sleet.fill | <img alt='cloud.bolt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.bolt.png'> | cloud.bolt | <img alt='cloud.bolt.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.bolt.fill.png'> | cloud.bolt.fill | <img alt='cloud.sun' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.sun.png'> | cloud.sun |
|
||||||
|
| <img alt='cloud.sun.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.sun.fill.png'> | cloud.sun.fill | <img alt='cloud.sun.rain' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.sun.rain.png'> | cloud.sun.rain | <img alt='cloud.sun.rain.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.sun.rain.fill.png'> | cloud.sun.rain.fill | <img alt='cloud.sun.bolt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.sun.bolt.png'> | cloud.sun.bolt |
|
||||||
|
| <img alt='cloud.sun.bolt.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.sun.bolt.fill.png'> | cloud.sun.bolt.fill | <img alt='cloud.moon' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.moon.png'> | cloud.moon | <img alt='cloud.moon.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.moon.fill.png'> | cloud.moon.fill | <img alt='cloud.moon.rain' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.moon.rain.png'> | cloud.moon.rain |
|
||||||
|
| <img alt='cloud.moon.rain.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.moon.rain.fill.png'> | cloud.moon.rain.fill | <img alt='cloud.bolt.rain' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.bolt.rain.png'> | cloud.bolt.rain | <img alt='cloud.bolt.rain.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.bolt.rain.fill.png'> | cloud.bolt.rain.fill | <img alt='cloud.moon.bolt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.moon.bolt.png'> | cloud.moon.bolt |
|
||||||
|
| <img alt='cloud.moon.bolt.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cloud.moon.bolt.fill.png'> | cloud.moon.bolt.fill | <img alt='smoke' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/smoke.png'> | smoke | <img alt='smoke.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/smoke.fill.png'> | smoke.fill | <img alt='wind' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wind.png'> | wind |
|
||||||
|
| <img alt='snow' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/snow.png'> | snow | <img alt='wind.snow' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wind.snow.png'> | wind.snow | <img alt='tornado' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tornado.png'> | tornado | <img alt='tropicalstorm' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tropicalstorm.png'> | tropicalstorm |
|
||||||
|
| <img alt='hurricane' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hurricane.png'> | hurricane | <img alt='thermometer.sun' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/thermometer.sun.png'> | thermometer.sun | <img alt='thermometer.snowflake' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/thermometer.snowflake.png'> | thermometer.snowflake | <img alt='thermometer' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/thermometer.png'> | thermometer |
|
||||||
|
| <img alt='umbrella' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/umbrella.png'> | umbrella | <img alt='umbrella.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/umbrella.fill.png'> | umbrella.fill | <img alt='flame' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flame.png'> | flame | <img alt='flame.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flame.fill.png'> | flame.fill |
|
||||||
|
| <img alt='light.min' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/light.min.png'> | light.min | <img alt='light.max' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/light.max.png'> | light.max | <img alt='rays' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rays.png'> | rays | <img alt='cursor.rays' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cursor.rays.png'> | cursor.rays |
|
||||||
|
| <img alt='slowmo' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/slowmo.png'> | slowmo | <img alt='timelapse' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/timelapse.png'> | timelapse | <img alt='keyboard' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/keyboard.png'> | keyboard | <img alt='keyboard.chevron.compact.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/keyboard.chevron.compact.down.png'> | keyboard.chevron.compact.down |
|
||||||
|
| <img alt='rectangle.3.offgrid' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.3.offgrid.png'> | rectangle.3.offgrid | <img alt='rectangle.3.offgrid.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.3.offgrid.fill.png'> | rectangle.3.offgrid.fill | <img alt='square.grid.3x2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.grid.3x2.png'> | square.grid.3x2 | <img alt='square.grid.3x2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.grid.3x2.fill.png'> | square.grid.3x2.fill |
|
||||||
|
| <img alt='rectangle.grid.3x2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.grid.3x2.png'> | rectangle.grid.3x2 | <img alt='rectangle.grid.3x2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.grid.3x2.fill.png'> | rectangle.grid.3x2.fill | <img alt='square.grid.2x2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.grid.2x2.png'> | square.grid.2x2 | <img alt='square.grid.2x2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.grid.2x2.fill.png'> | square.grid.2x2.fill |
|
||||||
|
| <img alt='rectangle.grid.2x2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.grid.2x2.png'> | rectangle.grid.2x2 | <img alt='rectangle.grid.2x2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.grid.2x2.fill.png'> | rectangle.grid.2x2.fill | <img alt='square.grid.4x3.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.grid.4x3.fill.png'> | square.grid.4x3.fill | <img alt='rectangle.grid.1x2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.grid.1x2.png'> | rectangle.grid.1x2 |
|
||||||
|
| <img alt='rectangle.grid.1x2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.grid.1x2.fill.png'> | rectangle.grid.1x2.fill | <img alt='circle.grid.2x2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.grid.2x2.png'> | circle.grid.2x2 | <img alt='circle.grid.2x2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.grid.2x2.fill.png'> | circle.grid.2x2.fill | <img alt='circle.grid.3x3' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.grid.3x3.png'> | circle.grid.3x3 |
|
||||||
|
| <img alt='circle.grid.3x3.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.grid.3x3.fill.png'> | circle.grid.3x3.fill | <img alt='circle.grid.hex' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.grid.hex.png'> | circle.grid.hex | <img alt='circle.grid.hex.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.grid.hex.fill.png'> | circle.grid.hex.fill | <img alt='checkmark.seal' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.seal.png'> | checkmark.seal |
|
||||||
|
| <img alt='checkmark.seal.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.seal.fill.png'> | checkmark.seal.fill | <img alt='xmark.seal' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.seal.png'> | xmark.seal | <img alt='xmark.seal.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.seal.fill.png'> | xmark.seal.fill | <img alt='exclamationmark.triangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.triangle.png'> | exclamationmark.triangle |
|
||||||
|
| <img alt='exclamationmark.triangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.triangle.fill.png'> | exclamationmark.triangle.fill | <img alt='drop.triangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/drop.triangle.png'> | drop.triangle | <img alt='drop.triangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/drop.triangle.fill.png'> | drop.triangle.fill | <img alt='play' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/play.png'> | play |
|
||||||
|
| <img alt='play.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/play.fill.png'> | play.fill | <img alt='play.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/play.circle.png'> | play.circle | <img alt='play.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/play.circle.fill.png'> | play.circle.fill | <img alt='play.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/play.rectangle.png'> | play.rectangle |
|
||||||
|
| <img alt='play.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/play.rectangle.fill.png'> | play.rectangle.fill | <img alt='pause' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pause.png'> | pause | <img alt='pause.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pause.fill.png'> | pause.fill | <img alt='pause.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pause.circle.png'> | pause.circle |
|
||||||
|
| <img alt='pause.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pause.circle.fill.png'> | pause.circle.fill | <img alt='pause.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pause.rectangle.png'> | pause.rectangle | <img alt='pause.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pause.rectangle.fill.png'> | pause.rectangle.fill | <img alt='stop' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/stop.png'> | stop |
|
||||||
|
| <img alt='stop.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/stop.fill.png'> | stop.fill | <img alt='stop.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/stop.circle.png'> | stop.circle | <img alt='stop.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/stop.circle.fill.png'> | stop.circle.fill | <img alt='playpause' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/playpause.png'> | playpause |
|
||||||
|
| <img alt='playpause.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/playpause.fill.png'> | playpause.fill | <img alt='backward' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/backward.png'> | backward | <img alt='backward.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/backward.fill.png'> | backward.fill | <img alt='forward' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/forward.png'> | forward |
|
||||||
|
| <img alt='forward.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/forward.fill.png'> | forward.fill | <img alt='backward.end' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/backward.end.png'> | backward.end | <img alt='backward.end.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/backward.end.fill.png'> | backward.end.fill | <img alt='forward.end' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/forward.end.png'> | forward.end |
|
||||||
|
| <img alt='forward.end.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/forward.end.fill.png'> | forward.end.fill | <img alt='backward.end.alt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/backward.end.alt.png'> | backward.end.alt | <img alt='backward.end.alt.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/backward.end.alt.fill.png'> | backward.end.alt.fill | <img alt='forward.end.alt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/forward.end.alt.png'> | forward.end.alt |
|
||||||
|
| <img alt='forward.end.alt.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/forward.end.alt.fill.png'> | forward.end.alt.fill | <img alt='eject' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eject.png'> | eject | <img alt='eject.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eject.fill.png'> | eject.fill | <img alt='memories' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/memories.png'> | memories |
|
||||||
|
| <img alt='memories.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/memories.badge.plus.png'> | memories.badge.plus | <img alt='memories.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/memories.badge.minus.png'> | memories.badge.minus | <img alt='shuffle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/shuffle.png'> | shuffle | <img alt='repeat' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/repeat.png'> | repeat |
|
||||||
|
| <img alt='repeat.1' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/repeat.1.png'> | repeat.1 | <img alt='speaker' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.png'> | speaker | <img alt='speaker.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.fill.png'> | speaker.fill | <img alt='speaker.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.slash.png'> | speaker.slash |
|
||||||
|
| <img alt='speaker.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.slash.fill.png'> | speaker.slash.fill | <img alt='speaker.zzz' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.zzz.png'> | speaker.zzz | <img alt='speaker.zzz.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.zzz.fill.png'> | speaker.zzz.fill | <img alt='speaker.1' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.1.png'> | speaker.1 |
|
||||||
|
| <img alt='speaker.1.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.1.fill.png'> | speaker.1.fill | <img alt='speaker.2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.2.png'> | speaker.2 | <img alt='speaker.2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.2.fill.png'> | speaker.2.fill | <img alt='speaker.3' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.3.png'> | speaker.3 |
|
||||||
|
| <img alt='speaker.3.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speaker.3.fill.png'> | speaker.3.fill | <img alt='badge.plus.radiowaves.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/badge.plus.radiowaves.right.png'> | badge.plus.radiowaves.right | <img alt='music.note' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/music.note.png'> | music.note | <img alt='music.mic' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/music.mic.png'> | music.mic |
|
||||||
|
| <img alt='music.note.list' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/music.note.list.png'> | music.note.list | <img alt='goforward' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.png'> | goforward | <img alt='gobackward' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.png'> | gobackward | <img alt='goforward.10' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.10.png'> | goforward.10 |
|
||||||
|
| <img alt='gobackward.10' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.10.png'> | gobackward.10 | <img alt='goforward.15' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.15.png'> | goforward.15 | <img alt='gobackward.15' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.15.png'> | gobackward.15 | <img alt='goforward.30' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.30.png'> | goforward.30 |
|
||||||
|
| <img alt='gobackward.30' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.30.png'> | gobackward.30 | <img alt='goforward.45' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.45.png'> | goforward.45 | <img alt='gobackward.45' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.45.png'> | gobackward.45 | <img alt='goforward.60' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.60.png'> | goforward.60 |
|
||||||
|
| <img alt='gobackward.60' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.60.png'> | gobackward.60 | <img alt='goforward.75' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.75.png'> | goforward.75 | <img alt='gobackward.75' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.75.png'> | gobackward.75 | <img alt='goforward.90' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.90.png'> | goforward.90 |
|
||||||
|
| <img alt='gobackward.90' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.90.png'> | gobackward.90 | <img alt='goforward.10.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.10.ar.png'> | goforward.10.ar | <img alt='gobackward.10.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.10.ar.png'> | gobackward.10.ar | <img alt='goforward.15.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.15.ar.png'> | goforward.15.ar |
|
||||||
|
| <img alt='gobackward.15.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.15.ar.png'> | gobackward.15.ar | <img alt='goforward.30.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.30.ar.png'> | goforward.30.ar | <img alt='gobackward.30.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.30.ar.png'> | gobackward.30.ar | <img alt='goforward.45.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.45.ar.png'> | goforward.45.ar |
|
||||||
|
| <img alt='gobackward.45.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.45.ar.png'> | gobackward.45.ar | <img alt='goforward.60.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.60.ar.png'> | goforward.60.ar | <img alt='gobackward.60.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.60.ar.png'> | gobackward.60.ar | <img alt='goforward.75.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.75.ar.png'> | goforward.75.ar |
|
||||||
|
| <img alt='gobackward.75.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.75.ar.png'> | gobackward.75.ar | <img alt='goforward.90.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.90.ar.png'> | goforward.90.ar | <img alt='gobackward.90.ar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.90.ar.png'> | gobackward.90.ar | <img alt='goforward.10.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.10.hi.png'> | goforward.10.hi |
|
||||||
|
| <img alt='gobackward.10.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.10.hi.png'> | gobackward.10.hi | <img alt='goforward.15.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.15.hi.png'> | goforward.15.hi | <img alt='gobackward.15.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.15.hi.png'> | gobackward.15.hi | <img alt='goforward.30.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.30.hi.png'> | goforward.30.hi |
|
||||||
|
| <img alt='gobackward.30.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.30.hi.png'> | gobackward.30.hi | <img alt='goforward.45.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.45.hi.png'> | goforward.45.hi | <img alt='gobackward.45.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.45.hi.png'> | gobackward.45.hi | <img alt='goforward.60.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.60.hi.png'> | goforward.60.hi |
|
||||||
|
| <img alt='gobackward.60.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.60.hi.png'> | gobackward.60.hi | <img alt='goforward.75.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.75.hi.png'> | goforward.75.hi | <img alt='gobackward.75.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.75.hi.png'> | gobackward.75.hi | <img alt='goforward.90.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.90.hi.png'> | goforward.90.hi |
|
||||||
|
| <img alt='gobackward.90.hi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.90.hi.png'> | gobackward.90.hi | <img alt='goforward.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/goforward.plus.png'> | goforward.plus | <img alt='gobackward.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gobackward.minus.png'> | gobackward.minus | <img alt='magnifyingglass' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/magnifyingglass.png'> | magnifyingglass |
|
||||||
|
| <img alt='magnifyingglass.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/magnifyingglass.circle.png'> | magnifyingglass.circle | <img alt='magnifyingglass.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/magnifyingglass.circle.fill.png'> | magnifyingglass.circle.fill | <img alt='plus.magnifyingglass' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.magnifyingglass.png'> | plus.magnifyingglass | <img alt='minus.magnifyingglass' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/minus.magnifyingglass.png'> | minus.magnifyingglass |
|
||||||
|
| <img alt='1.magnifyingglass' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/1.magnifyingglass.png'> | 1.magnifyingglass | <img alt='mic' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mic.png'> | mic | <img alt='mic.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mic.fill.png'> | mic.fill | <img alt='mic.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mic.circle.png'> | mic.circle |
|
||||||
|
| <img alt='mic.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mic.circle.fill.png'> | mic.circle.fill | <img alt='mic.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mic.slash.png'> | mic.slash | <img alt='mic.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mic.slash.fill.png'> | mic.slash.fill | <img alt='suit.heart' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/suit.heart.png'> | suit.heart |
|
||||||
|
| <img alt='suit.heart.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/suit.heart.fill.png'> | suit.heart.fill | <img alt='suit.club' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/suit.club.png'> | suit.club | <img alt='suit.club.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/suit.club.fill.png'> | suit.club.fill | <img alt='suit.spade' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/suit.spade.png'> | suit.spade |
|
||||||
|
| <img alt='suit.spade.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/suit.spade.fill.png'> | suit.spade.fill | <img alt='suit.diamond' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/suit.diamond.png'> | suit.diamond | <img alt='suit.diamond.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/suit.diamond.fill.png'> | suit.diamond.fill | <img alt='heart' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/heart.png'> | heart |
|
||||||
|
| <img alt='heart.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/heart.fill.png'> | heart.fill | <img alt='heart.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/heart.circle.png'> | heart.circle | <img alt='heart.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/heart.circle.fill.png'> | heart.circle.fill | <img alt='heart.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/heart.slash.png'> | heart.slash |
|
||||||
|
| <img alt='heart.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/heart.slash.fill.png'> | heart.slash.fill | <img alt='heart.slash.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/heart.slash.circle.png'> | heart.slash.circle | <img alt='heart.slash.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/heart.slash.circle.fill.png'> | heart.slash.circle.fill | <img alt='rhombus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rhombus.png'> | rhombus |
|
||||||
|
| <img alt='rhombus.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rhombus.fill.png'> | rhombus.fill | <img alt='star' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/star.png'> | star | <img alt='star.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/star.fill.png'> | star.fill | <img alt='star.lefthalf.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/star.lefthalf.fill.png'> | star.lefthalf.fill |
|
||||||
|
| <img alt='star.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/star.circle.png'> | star.circle | <img alt='star.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/star.circle.fill.png'> | star.circle.fill | <img alt='star.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/star.slash.png'> | star.slash | <img alt='star.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/star.slash.fill.png'> | star.slash.fill |
|
||||||
|
| <img alt='flag' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flag.png'> | flag | <img alt='flag.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flag.fill.png'> | flag.fill | <img alt='flag.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flag.circle.png'> | flag.circle | <img alt='flag.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flag.circle.fill.png'> | flag.circle.fill |
|
||||||
|
| <img alt='flag.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flag.slash.png'> | flag.slash | <img alt='flag.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flag.slash.fill.png'> | flag.slash.fill | <img alt='location' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/location.png'> | location | <img alt='location.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/location.fill.png'> | location.fill |
|
||||||
|
| <img alt='location.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/location.slash.png'> | location.slash | <img alt='location.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/location.slash.fill.png'> | location.slash.fill | <img alt='location.north' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/location.north.png'> | location.north | <img alt='location.north.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/location.north.fill.png'> | location.north.fill |
|
||||||
|
| <img alt='location.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/location.circle.png'> | location.circle | <img alt='location.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/location.circle.fill.png'> | location.circle.fill | <img alt='location.north.line' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/location.north.line.png'> | location.north.line | <img alt='location.north.line.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/location.north.line.fill.png'> | location.north.line.fill |
|
||||||
|
| <img alt='bell' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bell.png'> | bell | <img alt='bell.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bell.fill.png'> | bell.fill | <img alt='bell.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bell.circle.png'> | bell.circle | <img alt='bell.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bell.circle.fill.png'> | bell.circle.fill |
|
||||||
|
| <img alt='bell.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bell.slash.png'> | bell.slash | <img alt='bell.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bell.slash.fill.png'> | bell.slash.fill | <img alt='tag' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tag.png'> | tag | <img alt='tag.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tag.fill.png'> | tag.fill |
|
||||||
|
| <img alt='tag.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tag.circle.png'> | tag.circle | <img alt='tag.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tag.circle.fill.png'> | tag.circle.fill | <img alt='bolt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.png'> | bolt | <img alt='bolt.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.fill.png'> | bolt.fill |
|
||||||
|
| <img alt='bolt.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.circle.png'> | bolt.circle | <img alt='bolt.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.circle.fill.png'> | bolt.circle.fill | <img alt='bolt.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.slash.png'> | bolt.slash | <img alt='bolt.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.slash.fill.png'> | bolt.slash.fill |
|
||||||
|
| <img alt='bolt.badge.a' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.badge.a.png'> | bolt.badge.a | <img alt='bolt.badge.a.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.badge.a.fill.png'> | bolt.badge.a.fill | <img alt='eye' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eye.png'> | eye | <img alt='eye.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eye.fill.png'> | eye.fill |
|
||||||
|
| <img alt='eye.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eye.slash.png'> | eye.slash | <img alt='eye.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eye.slash.fill.png'> | eye.slash.fill | <img alt='icloud' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/icloud.png'> | icloud | <img alt='icloud.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/icloud.fill.png'> | icloud.fill |
|
||||||
|
| <img alt='icloud.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/icloud.circle.png'> | icloud.circle | <img alt='icloud.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/icloud.circle.fill.png'> | icloud.circle.fill | <img alt='icloud.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/icloud.slash.png'> | icloud.slash | <img alt='icloud.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/icloud.slash.fill.png'> | icloud.slash.fill |
|
||||||
|
| <img alt='exclamationmark.icloud' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.icloud.png'> | exclamationmark.icloud | <img alt='exclamationmark.icloud.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.icloud.fill.png'> | exclamationmark.icloud.fill | <img alt='xmark.icloud' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.icloud.png'> | xmark.icloud | <img alt='xmark.icloud.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.icloud.fill.png'> | xmark.icloud.fill |
|
||||||
|
| <img alt='link.icloud' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/link.icloud.png'> | link.icloud | <img alt='link.icloud.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/link.icloud.fill.png'> | link.icloud.fill | <img alt='bolt.horizontal.icloud' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.horizontal.icloud.png'> | bolt.horizontal.icloud | <img alt='bolt.horizontal.icloud.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.horizontal.icloud.fill.png'> | bolt.horizontal.icloud.fill |
|
||||||
|
| <img alt='person.icloud' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.icloud.png'> | person.icloud | <img alt='person.icloud.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.icloud.fill.png'> | person.icloud.fill | <img alt='lock.icloud' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.icloud.png'> | lock.icloud | <img alt='lock.icloud.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.icloud.fill.png'> | lock.icloud.fill |
|
||||||
|
| <img alt='arrow.clockwise.icloud' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.clockwise.icloud.png'> | arrow.clockwise.icloud | <img alt='arrow.clockwise.icloud.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.clockwise.icloud.fill.png'> | arrow.clockwise.icloud.fill | <img alt='arrow.counterclockwise.icloud' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.counterclockwise.icloud.png'> | arrow.counterclockwise.icloud | <img alt='arrow.counterclockwise.icloud.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.counterclockwise.icloud.fill.png'> | arrow.counterclockwise.icloud.fill |
|
||||||
|
| <img alt='icloud.and.arrow.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/icloud.and.arrow.down.png'> | icloud.and.arrow.down | <img alt='icloud.and.arrow.down.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/icloud.and.arrow.down.fill.png'> | icloud.and.arrow.down.fill | <img alt='icloud.and.arrow.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/icloud.and.arrow.up.png'> | icloud.and.arrow.up | <img alt='icloud.and.arrow.up.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/icloud.and.arrow.up.fill.png'> | icloud.and.arrow.up.fill |
|
||||||
|
| <img alt='ant' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/ant.png'> | ant | <img alt='ant.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/ant.fill.png'> | ant.fill | <img alt='ant.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/ant.circle.png'> | ant.circle | <img alt='ant.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/ant.circle.fill.png'> | ant.circle.fill |
|
||||||
|
| <img alt='flashlight.off.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flashlight.off.fill.png'> | flashlight.off.fill | <img alt='flashlight.on.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flashlight.on.fill.png'> | flashlight.on.fill | <img alt='camera' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/camera.png'> | camera | <img alt='camera.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/camera.fill.png'> | camera.fill |
|
||||||
|
| <img alt='camera.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/camera.circle.png'> | camera.circle | <img alt='camera.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/camera.circle.fill.png'> | camera.circle.fill | <img alt='camera.rotate' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/camera.rotate.png'> | camera.rotate | <img alt='camera.rotate.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/camera.rotate.fill.png'> | camera.rotate.fill |
|
||||||
|
| <img alt='camera.on.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/camera.on.rectangle.png'> | camera.on.rectangle | <img alt='camera.on.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/camera.on.rectangle.fill.png'> | camera.on.rectangle.fill | <img alt='message' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/message.png'> | message | <img alt='message.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/message.fill.png'> | message.fill |
|
||||||
|
| <img alt='message.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/message.circle.png'> | message.circle | <img alt='message.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/message.circle.fill.png'> | message.circle.fill | <img alt='bubble.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bubble.right.png'> | bubble.right | <img alt='bubble.right.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bubble.right.fill.png'> | bubble.right.fill |
|
||||||
|
| <img alt='bubble.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bubble.left.png'> | bubble.left | <img alt='bubble.left.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bubble.left.fill.png'> | bubble.left.fill | <img alt='exclamationmark.bubble' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.bubble.png'> | exclamationmark.bubble | <img alt='exclamationmark.bubble.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.bubble.fill.png'> | exclamationmark.bubble.fill |
|
||||||
|
| <img alt='quote.bubble' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/quote.bubble.png'> | quote.bubble | <img alt='quote.bubble.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/quote.bubble.fill.png'> | quote.bubble.fill | <img alt='t.bubble' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/t.bubble.png'> | t.bubble | <img alt='t.bubble.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/t.bubble.fill.png'> | t.bubble.fill |
|
||||||
|
| <img alt='text.bubble' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.bubble.png'> | text.bubble | <img alt='text.bubble.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.bubble.fill.png'> | text.bubble.fill | <img alt='captions.bubble' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/captions.bubble.png'> | captions.bubble | <img alt='captions.bubble.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/captions.bubble.fill.png'> | captions.bubble.fill |
|
||||||
|
| <img alt='plus.bubble' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.bubble.png'> | plus.bubble | <img alt='plus.bubble.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.bubble.fill.png'> | plus.bubble.fill | <img alt='ellipses.bubble' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/ellipses.bubble.png'> | ellipses.bubble | <img alt='ellipses.bubble.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/ellipses.bubble.fill.png'> | ellipses.bubble.fill |
|
||||||
|
| <img alt='bubble.middle.bottom' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bubble.middle.bottom.png'> | bubble.middle.bottom | <img alt='bubble.middle.bottom.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bubble.middle.bottom.fill.png'> | bubble.middle.bottom.fill | <img alt='bubble.middle.top' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bubble.middle.top.png'> | bubble.middle.top | <img alt='bubble.middle.top.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bubble.middle.top.fill.png'> | bubble.middle.top.fill |
|
||||||
|
| <img alt='bubble.left.and.bubble.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bubble.left.and.bubble.right.png'> | bubble.left.and.bubble.right | <img alt='bubble.left.and.bubble.right.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bubble.left.and.bubble.right.fill.png'> | bubble.left.and.bubble.right.fill | <img alt='phone' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.png'> | phone | <img alt='phone.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.fill.png'> | phone.fill |
|
||||||
|
| <img alt='phone.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.circle.png'> | phone.circle | <img alt='phone.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.circle.fill.png'> | phone.circle.fill | <img alt='phone.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.badge.plus.png'> | phone.badge.plus | <img alt='phone.fill.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.fill.badge.plus.png'> | phone.fill.badge.plus |
|
||||||
|
| <img alt='phone.arrow.up.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.arrow.up.right.png'> | phone.arrow.up.right | <img alt='phone.fill.arrow.up.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.fill.arrow.up.right.png'> | phone.fill.arrow.up.right | <img alt='phone.arrow.down.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.arrow.down.left.png'> | phone.arrow.down.left | <img alt='phone.fill.arrow.down.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.fill.arrow.down.left.png'> | phone.fill.arrow.down.left |
|
||||||
|
| <img alt='phone.arrow.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.arrow.right.png'> | phone.arrow.right | <img alt='phone.fill.arrow.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.fill.arrow.right.png'> | phone.fill.arrow.right | <img alt='phone.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.down.png'> | phone.down | <img alt='phone.down.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.down.fill.png'> | phone.down.fill |
|
||||||
|
| <img alt='phone.down.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.down.circle.png'> | phone.down.circle | <img alt='phone.down.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/phone.down.circle.fill.png'> | phone.down.circle.fill | <img alt='teletype' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/teletype.png'> | teletype | <img alt='teletype.answer' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/teletype.answer.png'> | teletype.answer |
|
||||||
|
| <img alt='video' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/video.png'> | video | <img alt='video.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/video.fill.png'> | video.fill | <img alt='video.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/video.circle.png'> | video.circle | <img alt='video.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/video.circle.fill.png'> | video.circle.fill |
|
||||||
|
| <img alt='video.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/video.slash.png'> | video.slash | <img alt='video.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/video.slash.fill.png'> | video.slash.fill | <img alt='video.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/video.badge.plus.png'> | video.badge.plus | <img alt='video.badge.plus.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/video.badge.plus.fill.png'> | video.badge.plus.fill |
|
||||||
|
| <img alt='arrow.up.right.video' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.right.video.png'> | arrow.up.right.video | <img alt='arrow.up.right.video.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.right.video.fill.png'> | arrow.up.right.video.fill | <img alt='arrow.down.left.video' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.left.video.png'> | arrow.down.left.video | <img alt='arrow.down.left.video.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.left.video.fill.png'> | arrow.down.left.video.fill |
|
||||||
|
| <img alt='questionmark.video' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/questionmark.video.png'> | questionmark.video | <img alt='questionmark.video.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/questionmark.video.fill.png'> | questionmark.video.fill | <img alt='envelope' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/envelope.png'> | envelope | <img alt='envelope.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/envelope.fill.png'> | envelope.fill |
|
||||||
|
| <img alt='envelope.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/envelope.circle.png'> | envelope.circle | <img alt='envelope.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/envelope.circle.fill.png'> | envelope.circle.fill | <img alt='envelope.open' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/envelope.open.png'> | envelope.open | <img alt='envelope.open.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/envelope.open.fill.png'> | envelope.open.fill |
|
||||||
|
| <img alt='envelope.badge' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/envelope.badge.png'> | envelope.badge | <img alt='envelope.badge.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/envelope.badge.fill.png'> | envelope.badge.fill | <img alt='gear' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gear.png'> | gear | <img alt='signature' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/signature.png'> | signature |
|
||||||
|
| <img alt='scissors' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/scissors.png'> | scissors | <img alt='scissors.badge.ellipsis' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/scissors.badge.ellipsis.png'> | scissors.badge.ellipsis | <img alt='ellipsis' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/ellipsis.png'> | ellipsis | <img alt='ellipsis.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/ellipsis.circle.png'> | ellipsis.circle |
|
||||||
|
| <img alt='ellipsis.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/ellipsis.circle.fill.png'> | ellipsis.circle.fill | <img alt='bag' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bag.png'> | bag | <img alt='bag.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bag.fill.png'> | bag.fill | <img alt='bag.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bag.badge.plus.png'> | bag.badge.plus |
|
||||||
|
| <img alt='bag.fill.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bag.fill.badge.plus.png'> | bag.fill.badge.plus | <img alt='bag.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bag.badge.minus.png'> | bag.badge.minus | <img alt='bag.fill.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bag.fill.badge.minus.png'> | bag.fill.badge.minus | <img alt='cart' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cart.png'> | cart |
|
||||||
|
| <img alt='cart.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cart.fill.png'> | cart.fill | <img alt='cart.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cart.badge.plus.png'> | cart.badge.plus | <img alt='cart.fill.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cart.fill.badge.plus.png'> | cart.fill.badge.plus | <img alt='cart.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cart.badge.minus.png'> | cart.badge.minus |
|
||||||
|
| <img alt='cart.fill.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cart.fill.badge.minus.png'> | cart.fill.badge.minus | <img alt='creditcard' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/creditcard.png'> | creditcard | <img alt='creditcard.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/creditcard.fill.png'> | creditcard.fill | <img alt='wand.and.rays' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wand.and.rays.png'> | wand.and.rays |
|
||||||
|
| <img alt='wand.and.rays.inverse' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wand.and.rays.inverse.png'> | wand.and.rays.inverse | <img alt='wand.and.stars' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wand.and.stars.png'> | wand.and.stars | <img alt='wand.and.stars.inverse' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wand.and.stars.inverse.png'> | wand.and.stars.inverse | <img alt='crop' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/crop.png'> | crop |
|
||||||
|
| <img alt='crop.rotate' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/crop.rotate.png'> | crop.rotate | <img alt='dial' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dial.png'> | dial | <img alt='dial.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dial.fill.png'> | dial.fill | <img alt='nosign' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/nosign.png'> | nosign |
|
||||||
|
| <img alt='gauge' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gauge.png'> | gauge | <img alt='gauge.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gauge.badge.plus.png'> | gauge.badge.plus | <img alt='gauge.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gauge.badge.minus.png'> | gauge.badge.minus | <img alt='speedometer' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/speedometer.png'> | speedometer |
|
||||||
|
| <img alt='metronome' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/metronome.png'> | metronome | <img alt='hifispeaker' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hifispeaker.png'> | hifispeaker | <img alt='hifispeaker.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hifispeaker.fill.png'> | hifispeaker.fill | <img alt='tuningfork' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tuningfork.png'> | tuningfork |
|
||||||
|
| <img alt='paintbrush' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/paintbrush.png'> | paintbrush | <img alt='paintbrush.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/paintbrush.fill.png'> | paintbrush.fill | <img alt='bandage' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bandage.png'> | bandage | <img alt='bandage.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bandage.fill.png'> | bandage.fill |
|
||||||
|
| <img alt='wrench' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wrench.png'> | wrench | <img alt='wrench.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wrench.fill.png'> | wrench.fill | <img alt='hammer' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hammer.png'> | hammer | <img alt='hammer.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hammer.fill.png'> | hammer.fill |
|
||||||
|
| <img alt='eyedropper' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eyedropper.png'> | eyedropper | <img alt='eyedropper.halffull' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eyedropper.halffull.png'> | eyedropper.halffull | <img alt='eyedropper.full' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eyedropper.full.png'> | eyedropper.full | <img alt='printer' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/printer.png'> | printer |
|
||||||
|
| <img alt='printer.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/printer.fill.png'> | printer.fill | <img alt='briefcase' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/briefcase.png'> | briefcase | <img alt='briefcase.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/briefcase.fill.png'> | briefcase.fill | <img alt='house' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/house.png'> | house |
|
||||||
|
| <img alt='house.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/house.fill.png'> | house.fill | <img alt='music.house' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/music.house.png'> | music.house | <img alt='music.house.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/music.house.fill.png'> | music.house.fill | <img alt='lock' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.png'> | lock |
|
||||||
|
| <img alt='lock.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.fill.png'> | lock.fill | <img alt='lock.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.circle.png'> | lock.circle | <img alt='lock.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.circle.fill.png'> | lock.circle.fill | <img alt='lock.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.slash.png'> | lock.slash |
|
||||||
|
| <img alt='lock.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.slash.fill.png'> | lock.slash.fill | <img alt='lock.open' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.open.png'> | lock.open | <img alt='lock.open.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.open.fill.png'> | lock.open.fill | <img alt='lock.rotation' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.rotation.png'> | lock.rotation |
|
||||||
|
| <img alt='lock.rotation.open' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.rotation.open.png'> | lock.rotation.open | <img alt='wifi' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wifi.png'> | wifi | <img alt='wifi.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wifi.slash.png'> | wifi.slash | <img alt='wifi.exclamationmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wifi.exclamationmark.png'> | wifi.exclamationmark |
|
||||||
|
| <img alt='pin' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pin.png'> | pin | <img alt='pin.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pin.fill.png'> | pin.fill | <img alt='pin.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pin.circle.png'> | pin.circle | <img alt='pin.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pin.circle.fill.png'> | pin.circle.fill |
|
||||||
|
| <img alt='pin.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pin.slash.png'> | pin.slash | <img alt='pin.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pin.slash.fill.png'> | pin.slash.fill | <img alt='mappin' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mappin.png'> | mappin | <img alt='mappin.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mappin.circle.png'> | mappin.circle |
|
||||||
|
| <img alt='mappin.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mappin.circle.fill.png'> | mappin.circle.fill | <img alt='mappin.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mappin.slash.png'> | mappin.slash | <img alt='mappin.and.ellipse' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/mappin.and.ellipse.png'> | mappin.and.ellipse | <img alt='map' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/map.png'> | map |
|
||||||
|
| <img alt='map.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/map.fill.png'> | map.fill | <img alt='safari' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/safari.png'> | safari | <img alt='safari.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/safari.fill.png'> | safari.fill | <img alt='rotate.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rotate.left.png'> | rotate.left |
|
||||||
|
| <img alt='rotate.left.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rotate.left.fill.png'> | rotate.left.fill | <img alt='rotate.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rotate.right.png'> | rotate.right | <img alt='rotate.right.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rotate.right.fill.png'> | rotate.right.fill | <img alt='selection.pin.in.out' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/selection.pin.in.out.png'> | selection.pin.in.out |
|
||||||
|
| <img alt='tv' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tv.png'> | tv | <img alt='tv.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tv.fill.png'> | tv.fill | <img alt='tv.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tv.circle.png'> | tv.circle | <img alt='tv.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tv.circle.fill.png'> | tv.circle.fill |
|
||||||
|
| <img alt='tv.music.note' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tv.music.note.png'> | tv.music.note | <img alt='tv.music.note.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tv.music.note.fill.png'> | tv.music.note.fill | <img alt='desktopcomputer' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/desktopcomputer.png'> | desktopcomputer | <img alt='airplayvideo' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/airplayvideo.png'> | airplayvideo |
|
||||||
|
| <img alt='airplayaudio' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/airplayaudio.png'> | airplayaudio | <img alt='dot.radiowaves.left.and.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dot.radiowaves.left.and.right.png'> | dot.radiowaves.left.and.right | <img alt='dot.radiowaves.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dot.radiowaves.right.png'> | dot.radiowaves.right | <img alt='radiowaves.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/radiowaves.left.png'> | radiowaves.left |
|
||||||
|
| <img alt='radiowaves.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/radiowaves.right.png'> | radiowaves.right | <img alt='antenna.radiowaves.left.and.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/antenna.radiowaves.left.and.right.png'> | antenna.radiowaves.left.and.right | <img alt='guitars' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/guitars.png'> | guitars | <img alt='car' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/car.png'> | car |
|
||||||
|
| <img alt='car.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/car.fill.png'> | car.fill | <img alt='tram.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tram.fill.png'> | tram.fill | <img alt='bed.double' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bed.double.png'> | bed.double | <img alt='bed.double.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bed.double.fill.png'> | bed.double.fill |
|
||||||
|
| <img alt='hare' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hare.png'> | hare | <img alt='hare.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hare.fill.png'> | hare.fill | <img alt='tortoise' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tortoise.png'> | tortoise | <img alt='tortoise.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tortoise.fill.png'> | tortoise.fill |
|
||||||
|
| <img alt='film' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/film.png'> | film | <img alt='film.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/film.fill.png'> | film.fill | <img alt='sportscourt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sportscourt.png'> | sportscourt | <img alt='sportscourt.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sportscourt.fill.png'> | sportscourt.fill |
|
||||||
|
| <img alt='smiley' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/smiley.png'> | smiley | <img alt='smiley.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/smiley.fill.png'> | smiley.fill | <img alt='qrcode' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/qrcode.png'> | qrcode | <img alt='barcode' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/barcode.png'> | barcode |
|
||||||
|
| <img alt='viewfinder' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/viewfinder.png'> | viewfinder | <img alt='viewfinder.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/viewfinder.circle.png'> | viewfinder.circle | <img alt='viewfinder.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/viewfinder.circle.fill.png'> | viewfinder.circle.fill | <img alt='barcode.viewfinder' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/barcode.viewfinder.png'> | barcode.viewfinder |
|
||||||
|
| <img alt='qrcode.viewfinder' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/qrcode.viewfinder.png'> | qrcode.viewfinder | <img alt='camera.viewfinder' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/camera.viewfinder.png'> | camera.viewfinder | <img alt='faceid' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/faceid.png'> | faceid | <img alt='doc.text.viewfinder' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/doc.text.viewfinder.png'> | doc.text.viewfinder |
|
||||||
|
| <img alt='rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.png'> | rectangle | <img alt='rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.fill.png'> | rectangle.fill | <img alt='photo' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/photo.png'> | photo | <img alt='photo.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/photo.fill.png'> | photo.fill |
|
||||||
|
| <img alt='plus.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.rectangle.png'> | plus.rectangle | <img alt='plus.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.rectangle.fill.png'> | plus.rectangle.fill | <img alt='minus.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/minus.rectangle.png'> | minus.rectangle | <img alt='minus.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/minus.rectangle.fill.png'> | minus.rectangle.fill |
|
||||||
|
| <img alt='checkmark.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.rectangle.png'> | checkmark.rectangle | <img alt='checkmark.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.rectangle.fill.png'> | checkmark.rectangle.fill | <img alt='xmark.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.rectangle.png'> | xmark.rectangle | <img alt='xmark.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.rectangle.fill.png'> | xmark.rectangle.fill |
|
||||||
|
| <img alt='person.crop.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.rectangle.png'> | person.crop.rectangle | <img alt='person.crop.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.crop.rectangle.fill.png'> | person.crop.rectangle.fill | <img alt='rectangle.badge.checkmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.badge.checkmark.png'> | rectangle.badge.checkmark | <img alt='rectangle.fill.badge.checkmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.fill.badge.checkmark.png'> | rectangle.fill.badge.checkmark |
|
||||||
|
| <img alt='rectangle.badge.xmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.badge.xmark.png'> | rectangle.badge.xmark | <img alt='rectangle.fill.badge.xmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.fill.badge.xmark.png'> | rectangle.fill.badge.xmark | <img alt='sidebar.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sidebar.left.png'> | sidebar.left | <img alt='sidebar.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sidebar.right.png'> | sidebar.right |
|
||||||
|
| <img alt='macwindow' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/macwindow.png'> | macwindow | <img alt='uiwindow.split.2x1' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/uiwindow.split.2x1.png'> | uiwindow.split.2x1 | <img alt='rectangle.dock' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.dock.png'> | rectangle.dock | <img alt='rectangle.split.3x1' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.split.3x1.png'> | rectangle.split.3x1 |
|
||||||
|
| <img alt='rectangle.split.3x1.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.split.3x1.fill.png'> | rectangle.split.3x1.fill | <img alt='square.split.2x1' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.split.2x1.png'> | square.split.2x1 | <img alt='square.split.2x1.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.split.2x1.fill.png'> | square.split.2x1.fill | <img alt='square.split.1x2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.split.1x2.png'> | square.split.1x2 |
|
||||||
|
| <img alt='square.split.1x2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.split.1x2.fill.png'> | square.split.1x2.fill | <img alt='square.split.2x2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.split.2x2.png'> | square.split.2x2 | <img alt='square.split.2x2.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.split.2x2.fill.png'> | square.split.2x2.fill | <img alt='dot.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dot.square.png'> | dot.square |
|
||||||
|
| <img alt='dot.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dot.square.fill.png'> | dot.square.fill | <img alt='squares.below.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/squares.below.rectangle.png'> | squares.below.rectangle | <img alt='rectangle.split.3x3' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.split.3x3.png'> | rectangle.split.3x3 | <img alt='rectangle.split.3x3.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.split.3x3.fill.png'> | rectangle.split.3x3.fill |
|
||||||
|
| <img alt='table' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/table.png'> | table | <img alt='table.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/table.fill.png'> | table.fill | <img alt='table.badge.more' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/table.badge.more.png'> | table.badge.more | <img alt='table.badge.more.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/table.badge.more.fill.png'> | table.badge.more.fill |
|
||||||
|
| <img alt='rectangle.on.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.on.rectangle.png'> | rectangle.on.rectangle | <img alt='rectangle.fill.on.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.fill.on.rectangle.fill.png'> | rectangle.fill.on.rectangle.fill | <img alt='plus.rectangle.on.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.rectangle.on.rectangle.png'> | plus.rectangle.on.rectangle | <img alt='plus.rectangle.fill.on.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.rectangle.fill.on.rectangle.fill.png'> | plus.rectangle.fill.on.rectangle.fill |
|
||||||
|
| <img alt='photo.on.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/photo.on.rectangle.png'> | photo.on.rectangle | <img alt='photo.fill.on.rectangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/photo.fill.on.rectangle.fill.png'> | photo.fill.on.rectangle.fill | <img alt='rectangle.on.rectangle.angled' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.on.rectangle.angled.png'> | rectangle.on.rectangle.angled | <img alt='rectangle.fill.on.rectangle.angled.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.fill.on.rectangle.angled.fill.png'> | rectangle.fill.on.rectangle.angled.fill |
|
||||||
|
| <img alt='rectangle.stack' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.stack.png'> | rectangle.stack | <img alt='rectangle.stack.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.stack.fill.png'> | rectangle.stack.fill | <img alt='rectangle.stack.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.stack.badge.plus.png'> | rectangle.stack.badge.plus | <img alt='rectangle.stack.fill.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.stack.fill.badge.plus.png'> | rectangle.stack.fill.badge.plus |
|
||||||
|
| <img alt='rectangle.stack.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.stack.badge.minus.png'> | rectangle.stack.badge.minus | <img alt='rectangle.stack.fill.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.stack.fill.badge.minus.png'> | rectangle.stack.fill.badge.minus | <img alt='rectangle.stack.badge.person.crop' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.stack.badge.person.crop.png'> | rectangle.stack.badge.person.crop | <img alt='rectangle.stack.fill.badge.person.crop' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.stack.fill.badge.person.crop.png'> | rectangle.stack.fill.badge.person.crop |
|
||||||
|
| <img alt='rectangle.stack.person.crop' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.stack.person.crop.png'> | rectangle.stack.person.crop | <img alt='rectangle.stack.person.crop.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.stack.person.crop.fill.png'> | rectangle.stack.person.crop.fill | <img alt='person.2.square.stack' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.2.square.stack.png'> | person.2.square.stack | <img alt='person.2.square.stack.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/person.2.square.stack.fill.png'> | person.2.square.stack.fill |
|
||||||
|
| <img alt='square.on.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.on.square.png'> | square.on.square | <img alt='square.fill.on.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.fill.on.square.fill.png'> | square.fill.on.square.fill | <img alt='plus.square.on.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.square.on.square.png'> | plus.square.on.square | <img alt='plus.square.fill.on.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.square.fill.on.square.fill.png'> | plus.square.fill.on.square.fill |
|
||||||
|
| <img alt='square.on.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.on.circle.png'> | square.on.circle | <img alt='square.fill.on.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.fill.on.circle.fill.png'> | square.fill.on.circle.fill | <img alt='square.stack' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.stack.png'> | square.stack | <img alt='square.stack.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.stack.fill.png'> | square.stack.fill |
|
||||||
|
| <img alt='pano' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pano.png'> | pano | <img alt='pano.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pano.fill.png'> | pano.fill | <img alt='square.and.line.vertical.and.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.line.vertical.and.square.png'> | square.and.line.vertical.and.square | <img alt='square.fill.and.line.vertical.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.fill.and.line.vertical.square.fill.png'> | square.fill.and.line.vertical.square.fill |
|
||||||
|
| <img alt='square.fill.and.line.vertical.and.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.fill.and.line.vertical.and.square.png'> | square.fill.and.line.vertical.and.square | <img alt='square.and.line.vertical.and.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.and.line.vertical.and.square.fill.png'> | square.and.line.vertical.and.square.fill | <img alt='flowchart' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flowchart.png'> | flowchart | <img alt='flowchart.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flowchart.fill.png'> | flowchart.fill |
|
||||||
|
| <img alt='shield' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/shield.png'> | shield | <img alt='shield.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/shield.fill.png'> | shield.fill | <img alt='shield.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/shield.slash.png'> | shield.slash | <img alt='shield.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/shield.slash.fill.png'> | shield.slash.fill |
|
||||||
|
| <img alt='lock.shield' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.shield.png'> | lock.shield | <img alt='lock.shield.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lock.shield.fill.png'> | lock.shield.fill | <img alt='checkmark.shield' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.shield.png'> | checkmark.shield | <img alt='checkmark.shield.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.shield.fill.png'> | checkmark.shield.fill |
|
||||||
|
| <img alt='xmark.shield' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.shield.png'> | xmark.shield | <img alt='xmark.shield.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.shield.fill.png'> | xmark.shield.fill | <img alt='exclamationmark.shield' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.shield.png'> | exclamationmark.shield | <img alt='exclamationmark.shield.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.shield.fill.png'> | exclamationmark.shield.fill |
|
||||||
|
| <img alt='shield.lefthalf.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/shield.lefthalf.fill.png'> | shield.lefthalf.fill | <img alt='slider.horizontal.below.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/slider.horizontal.below.rectangle.png'> | slider.horizontal.below.rectangle | <img alt='hexagon' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hexagon.png'> | hexagon | <img alt='hexagon.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hexagon.fill.png'> | hexagon.fill |
|
||||||
|
| <img alt='cube' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cube.png'> | cube | <img alt='cube.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cube.fill.png'> | cube.fill | <img alt='cube.box' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cube.box.png'> | cube.box | <img alt='cube.box.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cube.box.fill.png'> | cube.box.fill |
|
||||||
|
| <img alt='arkit' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arkit.png'> | arkit | <img alt='square.stack.3d.down.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.stack.3d.down.right.png'> | square.stack.3d.down.right | <img alt='square.stack.3d.down.right.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.stack.3d.down.right.fill.png'> | square.stack.3d.down.right.fill | <img alt='square.stack.3d.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.stack.3d.up.png'> | square.stack.3d.up |
|
||||||
|
| <img alt='square.stack.3d.up.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.stack.3d.up.fill.png'> | square.stack.3d.up.fill | <img alt='square.stack.3d.up.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.stack.3d.up.slash.png'> | square.stack.3d.up.slash | <img alt='square.stack.3d.up.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.stack.3d.up.slash.fill.png'> | square.stack.3d.up.slash.fill | <img alt='square.stack.3d.down.dottedline' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.stack.3d.down.dottedline.png'> | square.stack.3d.down.dottedline |
|
||||||
|
| <img alt='livephoto' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/livephoto.png'> | livephoto | <img alt='livephoto.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/livephoto.slash.png'> | livephoto.slash | <img alt='livephoto.play' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/livephoto.play.png'> | livephoto.play | <img alt='scope' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/scope.png'> | scope |
|
||||||
|
| <img alt='helm' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/helm.png'> | helm | <img alt='clock' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/clock.png'> | clock | <img alt='clock.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/clock.fill.png'> | clock.fill | <img alt='alarm' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/alarm.png'> | alarm |
|
||||||
|
| <img alt='alarm.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/alarm.fill.png'> | alarm.fill | <img alt='stopwatch' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/stopwatch.png'> | stopwatch | <img alt='stopwatch.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/stopwatch.fill.png'> | stopwatch.fill | <img alt='timer' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/timer.png'> | timer |
|
||||||
|
| <img alt='gamecontroller' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gamecontroller.png'> | gamecontroller | <img alt='gamecontroller.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gamecontroller.fill.png'> | gamecontroller.fill | <img alt='ear' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/ear.png'> | ear | <img alt='hand.raised' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.raised.png'> | hand.raised |
|
||||||
|
| <img alt='hand.raised.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.raised.fill.png'> | hand.raised.fill | <img alt='hand.raised.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.raised.slash.png'> | hand.raised.slash | <img alt='hand.raised.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.raised.slash.fill.png'> | hand.raised.slash.fill | <img alt='hand.thumbsup' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.thumbsup.png'> | hand.thumbsup |
|
||||||
|
| <img alt='hand.thumbsup.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.thumbsup.fill.png'> | hand.thumbsup.fill | <img alt='hand.thumbsdown' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.thumbsdown.png'> | hand.thumbsdown | <img alt='hand.thumbsdown.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.thumbsdown.fill.png'> | hand.thumbsdown.fill | <img alt='hand.draw' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.draw.png'> | hand.draw |
|
||||||
|
| <img alt='hand.draw.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.draw.fill.png'> | hand.draw.fill | <img alt='hand.point.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.point.left.png'> | hand.point.left | <img alt='hand.point.left.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.point.left.fill.png'> | hand.point.left.fill | <img alt='hand.point.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.point.right.png'> | hand.point.right |
|
||||||
|
| <img alt='hand.point.right.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hand.point.right.fill.png'> | hand.point.right.fill | <img alt='rectangle.compress.vertical' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.compress.vertical.png'> | rectangle.compress.vertical | <img alt='rectangle.expand.vertical' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.expand.vertical.png'> | rectangle.expand.vertical | <img alt='rectangle.and.arrow.up.right.and.arrow.down.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.and.arrow.up.right.and.arrow.down.left.png'> | rectangle.and.arrow.up.right.and.arrow.down.left |
|
||||||
|
| <img alt='rectangle.and.arrow.up.right.and.arrow.down.left.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rectangle.and.arrow.up.right.and.arrow.down.left.slash.png'> | rectangle.and.arrow.up.right.and.arrow.down.left.slash | <img alt='chart.bar' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chart.bar.png'> | chart.bar | <img alt='chart.bar.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chart.bar.fill.png'> | chart.bar.fill | <img alt='chart.pie' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chart.pie.png'> | chart.pie |
|
||||||
|
| <img alt='chart.pie.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chart.pie.fill.png'> | chart.pie.fill | <img alt='burst' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/burst.png'> | burst | <img alt='burst.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/burst.fill.png'> | burst.fill | <img alt='waveform.path.ecg' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/waveform.path.ecg.png'> | waveform.path.ecg |
|
||||||
|
| <img alt='waveform.path' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/waveform.path.png'> | waveform.path | <img alt='waveform.path.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/waveform.path.badge.plus.png'> | waveform.path.badge.plus | <img alt='waveform.path.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/waveform.path.badge.minus.png'> | waveform.path.badge.minus | <img alt='waveform' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/waveform.png'> | waveform |
|
||||||
|
| <img alt='waveform.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/waveform.circle.png'> | waveform.circle | <img alt='waveform.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/waveform.circle.fill.png'> | waveform.circle.fill | <img alt='staroflife' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/staroflife.png'> | staroflife | <img alt='staroflife.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/staroflife.fill.png'> | staroflife.fill |
|
||||||
|
| <img alt='headphones' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/headphones.png'> | headphones | <img alt='gift' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gift.png'> | gift | <img alt='gift.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/gift.fill.png'> | gift.fill | <img alt='app' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/app.png'> | app |
|
||||||
|
| <img alt='app.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/app.fill.png'> | app.fill | <img alt='plus.app' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.app.png'> | plus.app | <img alt='plus.app.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.app.fill.png'> | plus.app.fill | <img alt='app.badge' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/app.badge.png'> | app.badge |
|
||||||
|
| <img alt='app.badge.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/app.badge.fill.png'> | app.badge.fill | <img alt='app.gift' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/app.gift.png'> | app.gift | <img alt='app.gift.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/app.gift.fill.png'> | app.gift.fill | <img alt='airplane' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/airplane.png'> | airplane |
|
||||||
|
| <img alt='studentdesk' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/studentdesk.png'> | studentdesk | <img alt='hourglass' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hourglass.png'> | hourglass | <img alt='hourglass.bottomhalf.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hourglass.bottomhalf.fill.png'> | hourglass.bottomhalf.fill | <img alt='hourglass.tophalf.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hourglass.tophalf.fill.png'> | hourglass.tophalf.fill |
|
||||||
|
| <img alt='paragraph' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/paragraph.png'> | paragraph | <img alt='purchased' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/purchased.png'> | purchased | <img alt='purchased.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/purchased.circle.png'> | purchased.circle | <img alt='purchased.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/purchased.circle.fill.png'> | purchased.circle.fill |
|
||||||
|
| <img alt='exclamationmark.octagon' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.octagon.png'> | exclamationmark.octagon | <img alt='exclamationmark.octagon.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.octagon.fill.png'> | exclamationmark.octagon.fill | <img alt='xmark.octagon' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.octagon.png'> | xmark.octagon | <img alt='xmark.octagon.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.octagon.fill.png'> | xmark.octagon.fill |
|
||||||
|
| <img alt='bolt.horizontal' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.horizontal.png'> | bolt.horizontal | <img alt='bolt.horizontal.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.horizontal.fill.png'> | bolt.horizontal.fill | <img alt='bolt.horizontal.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.horizontal.circle.png'> | bolt.horizontal.circle | <img alt='bolt.horizontal.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bolt.horizontal.circle.fill.png'> | bolt.horizontal.circle.fill |
|
||||||
|
| <img alt='perspective' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/perspective.png'> | perspective | <img alt='aspectratio' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/aspectratio.png'> | aspectratio | <img alt='aspectratio.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/aspectratio.fill.png'> | aspectratio.fill | <img alt='skew' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/skew.png'> | skew |
|
||||||
|
| <img alt='flip.horizontal' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flip.horizontal.png'> | flip.horizontal | <img alt='flip.horizontal.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/flip.horizontal.fill.png'> | flip.horizontal.fill | <img alt='grid' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/grid.png'> | grid | <img alt='grid.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/grid.circle.png'> | grid.circle |
|
||||||
|
| <img alt='grid.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/grid.circle.fill.png'> | grid.circle.fill | <img alt='burn' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/burn.png'> | burn | <img alt='scribble' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/scribble.png'> | scribble | <img alt='lasso' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lasso.png'> | lasso |
|
||||||
|
| <img alt='recordingtape' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/recordingtape.png'> | recordingtape | <img alt='eyeglasses' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eyeglasses.png'> | eyeglasses | <img alt='battery.100' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/battery.100.png'> | battery.100 | <img alt='battery.25' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/battery.25.png'> | battery.25 |
|
||||||
|
| <img alt='battery.0' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/battery.0.png'> | battery.0 | <img alt='lightbulb' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lightbulb.png'> | lightbulb | <img alt='lightbulb.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lightbulb.fill.png'> | lightbulb.fill | <img alt='lightbulb.slash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lightbulb.slash.png'> | lightbulb.slash |
|
||||||
|
| <img alt='lightbulb.slash.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lightbulb.slash.fill.png'> | lightbulb.slash.fill | <img alt='list.dash' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/list.dash.png'> | list.dash | <img alt='list.bullet' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/list.bullet.png'> | list.bullet | <img alt='list.bullet.indent' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/list.bullet.indent.png'> | list.bullet.indent |
|
||||||
|
| <img alt='list.number' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/list.number.png'> | list.number | <img alt='increase.indent' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/increase.indent.png'> | increase.indent | <img alt='decrease.indent' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/decrease.indent.png'> | decrease.indent | <img alt='decrease.quotelevel' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/decrease.quotelevel.png'> | decrease.quotelevel |
|
||||||
|
| <img alt='increase.quotelevel' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/increase.quotelevel.png'> | increase.quotelevel | <img alt='list.bullet.below.rectangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/list.bullet.below.rectangle.png'> | list.bullet.below.rectangle | <img alt='text.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.badge.plus.png'> | text.badge.plus | <img alt='text.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.badge.minus.png'> | text.badge.minus |
|
||||||
|
| <img alt='text.badge.checkmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.badge.checkmark.png'> | text.badge.checkmark | <img alt='text.badge.xmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.badge.xmark.png'> | text.badge.xmark | <img alt='text.badge.star' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.badge.star.png'> | text.badge.star | <img alt='text.insert' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.insert.png'> | text.insert |
|
||||||
|
| <img alt='text.append' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.append.png'> | text.append | <img alt='text.quote' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.quote.png'> | text.quote | <img alt='text.alignleft' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.alignleft.png'> | text.alignleft | <img alt='text.aligncenter' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.aligncenter.png'> | text.aligncenter |
|
||||||
|
| <img alt='text.alignright' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.alignright.png'> | text.alignright | <img alt='text.justify' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.justify.png'> | text.justify | <img alt='text.justifyleft' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.justifyleft.png'> | text.justifyleft | <img alt='text.justifyright' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.justifyright.png'> | text.justifyright |
|
||||||
|
| <img alt='slider.horizontal.3' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/slider.horizontal.3.png'> | slider.horizontal.3 | <img alt='line.horizontal.3' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/line.horizontal.3.png'> | line.horizontal.3 | <img alt='line.horizontal.3.decrease' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/line.horizontal.3.decrease.png'> | line.horizontal.3.decrease | <img alt='line.horizontal.3.decrease.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/line.horizontal.3.decrease.circle.png'> | line.horizontal.3.decrease.circle |
|
||||||
|
| <img alt='line.horizontal.3.decrease.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/line.horizontal.3.decrease.circle.fill.png'> | line.horizontal.3.decrease.circle.fill | <img alt='a' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/a.png'> | a | <img alt='textformat.size' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/textformat.size.png'> | textformat.size | <img alt='textformat.alt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/textformat.alt.png'> | textformat.alt |
|
||||||
|
| <img alt='textformat' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/textformat.png'> | textformat | <img alt='textformat.subscript' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/textformat.subscript.png'> | textformat.subscript | <img alt='textformat.superscript' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/textformat.superscript.png'> | textformat.superscript | <img alt='bold' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bold.png'> | bold |
|
||||||
|
| <img alt='italic' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/italic.png'> | italic | <img alt='underline' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/underline.png'> | underline | <img alt='strikethrough' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/strikethrough.png'> | strikethrough | <img alt='bold.italic.underline' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bold.italic.underline.png'> | bold.italic.underline |
|
||||||
|
| <img alt='bold.underline' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bold.underline.png'> | bold.underline | <img alt='view.2d' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/view.2d.png'> | view.2d | <img alt='view.3d' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/view.3d.png'> | view.3d | <img alt='text.cursor' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/text.cursor.png'> | text.cursor |
|
||||||
|
| <img alt='fx' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/fx.png'> | fx | <img alt='f.cursive' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/f.cursive.png'> | f.cursive | <img alt='f.cursive.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/f.cursive.circle.png'> | f.cursive.circle | <img alt='f.cursive.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/f.cursive.circle.fill.png'> | f.cursive.circle.fill |
|
||||||
|
| <img alt='sum' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sum.png'> | sum | <img alt='percent' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/percent.png'> | percent | <img alt='function' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/function.png'> | function | <img alt='textformat.abc' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/textformat.abc.png'> | textformat.abc |
|
||||||
|
| <img alt='textformat.abc.dottedunderline' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/textformat.abc.dottedunderline.png'> | textformat.abc.dottedunderline | <img alt='textformat.123' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/textformat.123.png'> | textformat.123 | <img alt='info' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/info.png'> | info | <img alt='info.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/info.circle.png'> | info.circle |
|
||||||
|
| <img alt='info.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/info.circle.fill.png'> | info.circle.fill | <img alt='textbox' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/textbox.png'> | textbox | <img alt='at' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/at.png'> | at | <img alt='at.badge.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/at.badge.plus.png'> | at.badge.plus |
|
||||||
|
| <img alt='at.badge.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/at.badge.minus.png'> | at.badge.minus | <img alt='questionmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/questionmark.png'> | questionmark | <img alt='questionmark.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/questionmark.circle.png'> | questionmark.circle | <img alt='questionmark.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/questionmark.circle.fill.png'> | questionmark.circle.fill |
|
||||||
|
| <img alt='questionmark.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/questionmark.square.png'> | questionmark.square | <img alt='questionmark.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/questionmark.square.fill.png'> | questionmark.square.fill | <img alt='questionmark.diamond' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/questionmark.diamond.png'> | questionmark.diamond | <img alt='questionmark.diamond.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/questionmark.diamond.fill.png'> | questionmark.diamond.fill |
|
||||||
|
| <img alt='exclamationmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.png'> | exclamationmark | <img alt='exclamationmark.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.circle.png'> | exclamationmark.circle | <img alt='exclamationmark.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.circle.fill.png'> | exclamationmark.circle.fill | <img alt='exclamationmark.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.square.png'> | exclamationmark.square |
|
||||||
|
| <img alt='exclamationmark.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/exclamationmark.square.fill.png'> | exclamationmark.square.fill | <img alt='plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.png'> | plus | <img alt='plus.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.circle.png'> | plus.circle | <img alt='plus.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.circle.fill.png'> | plus.circle.fill |
|
||||||
|
| <img alt='plus.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.square.png'> | plus.square | <img alt='plus.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.square.fill.png'> | plus.square.fill | <img alt='minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/minus.png'> | minus | <img alt='minus.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/minus.circle.png'> | minus.circle |
|
||||||
|
| <img alt='minus.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/minus.circle.fill.png'> | minus.circle.fill | <img alt='minus.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/minus.square.png'> | minus.square | <img alt='minus.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/minus.square.fill.png'> | minus.square.fill | <img alt='plusminus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plusminus.png'> | plusminus |
|
||||||
|
| <img alt='plusminus.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plusminus.circle.png'> | plusminus.circle | <img alt='plusminus.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plusminus.circle.fill.png'> | plusminus.circle.fill | <img alt='plus.slash.minus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/plus.slash.minus.png'> | plus.slash.minus | <img alt='minus.slash.plus' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/minus.slash.plus.png'> | minus.slash.plus |
|
||||||
|
| <img alt='multiply' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/multiply.png'> | multiply | <img alt='multiply.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/multiply.circle.png'> | multiply.circle | <img alt='multiply.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/multiply.circle.fill.png'> | multiply.circle.fill | <img alt='multiply.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/multiply.square.png'> | multiply.square |
|
||||||
|
| <img alt='multiply.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/multiply.square.fill.png'> | multiply.square.fill | <img alt='divide' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/divide.png'> | divide | <img alt='divide.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/divide.circle.png'> | divide.circle | <img alt='divide.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/divide.circle.fill.png'> | divide.circle.fill |
|
||||||
|
| <img alt='divide.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/divide.square.png'> | divide.square | <img alt='divide.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/divide.square.fill.png'> | divide.square.fill | <img alt='equal' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/equal.png'> | equal | <img alt='equal.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/equal.circle.png'> | equal.circle |
|
||||||
|
| <img alt='equal.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/equal.circle.fill.png'> | equal.circle.fill | <img alt='equal.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/equal.square.png'> | equal.square | <img alt='equal.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/equal.square.fill.png'> | equal.square.fill | <img alt='lessthan' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lessthan.png'> | lessthan |
|
||||||
|
| <img alt='lessthan.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lessthan.circle.png'> | lessthan.circle | <img alt='lessthan.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lessthan.circle.fill.png'> | lessthan.circle.fill | <img alt='lessthan.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lessthan.square.png'> | lessthan.square | <img alt='lessthan.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lessthan.square.fill.png'> | lessthan.square.fill |
|
||||||
|
| <img alt='greaterthan' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/greaterthan.png'> | greaterthan | <img alt='greaterthan.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/greaterthan.circle.png'> | greaterthan.circle | <img alt='greaterthan.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/greaterthan.circle.fill.png'> | greaterthan.circle.fill | <img alt='greaterthan.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/greaterthan.square.png'> | greaterthan.square |
|
||||||
|
| <img alt='greaterthan.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/greaterthan.square.fill.png'> | greaterthan.square.fill | <img alt='chevron.left.slash.chevron.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.left.slash.chevron.right.png'> | chevron.left.slash.chevron.right | <img alt='number' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/number.png'> | number | <img alt='number.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/number.circle.png'> | number.circle |
|
||||||
|
| <img alt='number.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/number.circle.fill.png'> | number.circle.fill | <img alt='number.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/number.square.png'> | number.square | <img alt='number.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/number.square.fill.png'> | number.square.fill | <img alt='x.squareroot' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/x.squareroot.png'> | x.squareroot |
|
||||||
|
| <img alt='xmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.png'> | xmark | <img alt='xmark.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.circle.png'> | xmark.circle | <img alt='xmark.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.circle.fill.png'> | xmark.circle.fill | <img alt='xmark.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.square.png'> | xmark.square |
|
||||||
|
| <img alt='xmark.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/xmark.square.fill.png'> | xmark.square.fill | <img alt='checkmark' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.png'> | checkmark | <img alt='checkmark.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.circle.png'> | checkmark.circle | <img alt='checkmark.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.circle.fill.png'> | checkmark.circle.fill |
|
||||||
|
| <img alt='checkmark.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.square.png'> | checkmark.square | <img alt='checkmark.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/checkmark.square.fill.png'> | checkmark.square.fill | <img alt='chevron.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.up.png'> | chevron.up | <img alt='chevron.up.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.up.circle.png'> | chevron.up.circle |
|
||||||
|
| <img alt='chevron.up.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.up.circle.fill.png'> | chevron.up.circle.fill | <img alt='chevron.up.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.up.square.png'> | chevron.up.square | <img alt='chevron.up.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.up.square.fill.png'> | chevron.up.square.fill | <img alt='chevron.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.down.png'> | chevron.down |
|
||||||
|
| <img alt='chevron.down.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.down.circle.png'> | chevron.down.circle | <img alt='chevron.down.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.down.circle.fill.png'> | chevron.down.circle.fill | <img alt='chevron.down.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.down.square.png'> | chevron.down.square | <img alt='chevron.down.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.down.square.fill.png'> | chevron.down.square.fill |
|
||||||
|
| <img alt='chevron.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.left.png'> | chevron.left | <img alt='chevron.left.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.left.circle.png'> | chevron.left.circle | <img alt='chevron.left.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.left.circle.fill.png'> | chevron.left.circle.fill | <img alt='chevron.left.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.left.square.png'> | chevron.left.square |
|
||||||
|
| <img alt='chevron.left.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.left.square.fill.png'> | chevron.left.square.fill | <img alt='chevron.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.right.png'> | chevron.right | <img alt='chevron.right.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.right.circle.png'> | chevron.right.circle | <img alt='chevron.right.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.right.circle.fill.png'> | chevron.right.circle.fill |
|
||||||
|
| <img alt='chevron.right.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.right.square.png'> | chevron.right.square | <img alt='chevron.right.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.right.square.fill.png'> | chevron.right.square.fill | <img alt='chevron.left.2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.left.2.png'> | chevron.left.2 | <img alt='chevron.right.2' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.right.2.png'> | chevron.right.2 |
|
||||||
|
| <img alt='control' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/control.png'> | control | <img alt='projective' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/projective.png'> | projective | <img alt='chevron.up.chevron.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.up.chevron.down.png'> | chevron.up.chevron.down | <img alt='chevron.compact.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.compact.up.png'> | chevron.compact.up |
|
||||||
|
| <img alt='chevron.compact.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.compact.down.png'> | chevron.compact.down | <img alt='chevron.compact.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.compact.left.png'> | chevron.compact.left | <img alt='chevron.compact.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/chevron.compact.right.png'> | chevron.compact.right | <img alt='arrow.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.png'> | arrow.up |
|
||||||
|
| <img alt='arrow.up.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.circle.png'> | arrow.up.circle | <img alt='arrow.up.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.circle.fill.png'> | arrow.up.circle.fill | <img alt='arrow.up.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.square.png'> | arrow.up.square | <img alt='arrow.up.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.square.fill.png'> | arrow.up.square.fill |
|
||||||
|
| <img alt='arrow.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.png'> | arrow.down | <img alt='arrow.down.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.circle.png'> | arrow.down.circle | <img alt='arrow.down.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.circle.fill.png'> | arrow.down.circle.fill | <img alt='arrow.down.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.square.png'> | arrow.down.square |
|
||||||
|
| <img alt='arrow.down.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.square.fill.png'> | arrow.down.square.fill | <img alt='arrow.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.png'> | arrow.left | <img alt='arrow.left.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.circle.png'> | arrow.left.circle | <img alt='arrow.left.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.circle.fill.png'> | arrow.left.circle.fill |
|
||||||
|
| <img alt='arrow.left.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.square.png'> | arrow.left.square | <img alt='arrow.left.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.square.fill.png'> | arrow.left.square.fill | <img alt='arrow.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.png'> | arrow.right | <img alt='arrow.right.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.circle.png'> | arrow.right.circle |
|
||||||
|
| <img alt='arrow.right.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.circle.fill.png'> | arrow.right.circle.fill | <img alt='arrow.right.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.square.png'> | arrow.right.square | <img alt='arrow.right.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.square.fill.png'> | arrow.right.square.fill | <img alt='arrow.up.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.left.png'> | arrow.up.left |
|
||||||
|
| <img alt='arrow.up.left.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.left.circle.png'> | arrow.up.left.circle | <img alt='arrow.up.left.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.left.circle.fill.png'> | arrow.up.left.circle.fill | <img alt='arrow.up.left.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.left.square.png'> | arrow.up.left.square | <img alt='arrow.up.left.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.left.square.fill.png'> | arrow.up.left.square.fill |
|
||||||
|
| <img alt='arrow.up.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.right.png'> | arrow.up.right | <img alt='arrow.up.right.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.right.circle.png'> | arrow.up.right.circle | <img alt='arrow.up.right.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.right.circle.fill.png'> | arrow.up.right.circle.fill | <img alt='arrow.up.right.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.right.square.png'> | arrow.up.right.square |
|
||||||
|
| <img alt='arrow.up.right.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.right.square.fill.png'> | arrow.up.right.square.fill | <img alt='arrow.down.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.left.png'> | arrow.down.left | <img alt='arrow.down.left.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.left.circle.png'> | arrow.down.left.circle | <img alt='arrow.down.left.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.left.circle.fill.png'> | arrow.down.left.circle.fill |
|
||||||
|
| <img alt='arrow.down.left.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.left.square.png'> | arrow.down.left.square | <img alt='arrow.down.left.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.left.square.fill.png'> | arrow.down.left.square.fill | <img alt='arrow.down.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.right.png'> | arrow.down.right | <img alt='arrow.down.right.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.right.circle.png'> | arrow.down.right.circle |
|
||||||
|
| <img alt='arrow.down.right.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.right.circle.fill.png'> | arrow.down.right.circle.fill | <img alt='arrow.down.right.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.right.square.png'> | arrow.down.right.square | <img alt='arrow.down.right.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.right.square.fill.png'> | arrow.down.right.square.fill | <img alt='arrow.up.arrow.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.arrow.down.png'> | arrow.up.arrow.down |
|
||||||
|
| <img alt='arrow.up.arrow.down.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.arrow.down.circle.png'> | arrow.up.arrow.down.circle | <img alt='arrow.up.arrow.down.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.arrow.down.circle.fill.png'> | arrow.up.arrow.down.circle.fill | <img alt='arrow.up.arrow.down.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.arrow.down.square.png'> | arrow.up.arrow.down.square | <img alt='arrow.up.arrow.down.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.arrow.down.square.fill.png'> | arrow.up.arrow.down.square.fill |
|
||||||
|
| <img alt='arrow.right.arrow.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.arrow.left.png'> | arrow.right.arrow.left | <img alt='arrow.right.arrow.left.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.arrow.left.circle.png'> | arrow.right.arrow.left.circle | <img alt='arrow.right.arrow.left.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.arrow.left.circle.fill.png'> | arrow.right.arrow.left.circle.fill | <img alt='arrow.right.arrow.left.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.arrow.left.square.png'> | arrow.right.arrow.left.square |
|
||||||
|
| <img alt='arrow.right.arrow.left.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.arrow.left.square.fill.png'> | arrow.right.arrow.left.square.fill | <img alt='arrow.turn.right.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.turn.right.up.png'> | arrow.turn.right.up | <img alt='arrow.turn.right.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.turn.right.down.png'> | arrow.turn.right.down | <img alt='arrow.turn.down.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.turn.down.left.png'> | arrow.turn.down.left |
|
||||||
|
| <img alt='arrow.turn.down.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.turn.down.right.png'> | arrow.turn.down.right | <img alt='arrow.turn.left.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.turn.left.up.png'> | arrow.turn.left.up | <img alt='arrow.turn.left.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.turn.left.down.png'> | arrow.turn.left.down | <img alt='arrow.turn.up.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.turn.up.left.png'> | arrow.turn.up.left |
|
||||||
|
| <img alt='arrow.turn.up.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.turn.up.right.png'> | arrow.turn.up.right | <img alt='arrow.uturn.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.up.png'> | arrow.uturn.up | <img alt='arrow.uturn.up.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.up.circle.png'> | arrow.uturn.up.circle | <img alt='arrow.uturn.up.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.up.circle.fill.png'> | arrow.uturn.up.circle.fill |
|
||||||
|
| <img alt='arrow.uturn.up.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.up.square.png'> | arrow.uturn.up.square | <img alt='arrow.uturn.up.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.up.square.fill.png'> | arrow.uturn.up.square.fill | <img alt='arrow.uturn.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.down.png'> | arrow.uturn.down | <img alt='arrow.uturn.down.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.down.circle.png'> | arrow.uturn.down.circle |
|
||||||
|
| <img alt='arrow.uturn.down.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.down.circle.fill.png'> | arrow.uturn.down.circle.fill | <img alt='arrow.uturn.down.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.down.square.png'> | arrow.uturn.down.square | <img alt='arrow.uturn.down.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.down.square.fill.png'> | arrow.uturn.down.square.fill | <img alt='arrow.uturn.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.left.png'> | arrow.uturn.left |
|
||||||
|
| <img alt='arrow.uturn.left.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.left.circle.png'> | arrow.uturn.left.circle | <img alt='arrow.uturn.left.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.left.circle.fill.png'> | arrow.uturn.left.circle.fill | <img alt='arrow.uturn.left.circle.badge.ellipsis' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.left.circle.badge.ellipsis.png'> | arrow.uturn.left.circle.badge.ellipsis | <img alt='arrow.uturn.left.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.left.square.png'> | arrow.uturn.left.square |
|
||||||
|
| <img alt='arrow.uturn.left.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.left.square.fill.png'> | arrow.uturn.left.square.fill | <img alt='arrow.uturn.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.right.png'> | arrow.uturn.right | <img alt='arrow.uturn.right.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.right.circle.png'> | arrow.uturn.right.circle | <img alt='arrow.uturn.right.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.right.circle.fill.png'> | arrow.uturn.right.circle.fill |
|
||||||
|
| <img alt='arrow.uturn.right.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.right.square.png'> | arrow.uturn.right.square | <img alt='arrow.uturn.right.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.uturn.right.square.fill.png'> | arrow.uturn.right.square.fill | <img alt='arrow.up.and.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.and.down.png'> | arrow.up.and.down | <img alt='arrow.up.and.down.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.and.down.circle.png'> | arrow.up.and.down.circle |
|
||||||
|
| <img alt='arrow.up.and.down.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.and.down.circle.fill.png'> | arrow.up.and.down.circle.fill | <img alt='arrow.up.and.down.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.and.down.square.png'> | arrow.up.and.down.square | <img alt='arrow.up.and.down.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.and.down.square.fill.png'> | arrow.up.and.down.square.fill | <img alt='arrow.left.and.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.and.right.png'> | arrow.left.and.right |
|
||||||
|
| <img alt='arrow.left.and.right.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.and.right.circle.png'> | arrow.left.and.right.circle | <img alt='arrow.left.and.right.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.and.right.circle.fill.png'> | arrow.left.and.right.circle.fill | <img alt='arrow.left.and.right.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.and.right.square.png'> | arrow.left.and.right.square | <img alt='arrow.left.and.right.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.and.right.square.fill.png'> | arrow.left.and.right.square.fill |
|
||||||
|
| <img alt='arrow.up.to.line.alt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.to.line.alt.png'> | arrow.up.to.line.alt | <img alt='arrow.up.to.line' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.to.line.png'> | arrow.up.to.line | <img alt='arrow.down.to.line.alt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.to.line.alt.png'> | arrow.down.to.line.alt | <img alt='arrow.down.to.line' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.to.line.png'> | arrow.down.to.line |
|
||||||
|
| <img alt='arrow.left.to.line.alt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.to.line.alt.png'> | arrow.left.to.line.alt | <img alt='arrow.left.to.line' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.left.to.line.png'> | arrow.left.to.line | <img alt='arrow.right.to.line.alt' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.to.line.alt.png'> | arrow.right.to.line.alt | <img alt='arrow.right.to.line' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.right.to.line.png'> | arrow.right.to.line |
|
||||||
|
| <img alt='return' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/return.png'> | return | <img alt='arrow.clockwise' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.clockwise.png'> | arrow.clockwise | <img alt='arrow.clockwise.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.clockwise.circle.png'> | arrow.clockwise.circle | <img alt='arrow.clockwise.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.clockwise.circle.fill.png'> | arrow.clockwise.circle.fill |
|
||||||
|
| <img alt='arrow.counterclockwise' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.counterclockwise.png'> | arrow.counterclockwise | <img alt='arrow.counterclockwise.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.counterclockwise.circle.png'> | arrow.counterclockwise.circle | <img alt='arrow.counterclockwise.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.counterclockwise.circle.fill.png'> | arrow.counterclockwise.circle.fill | <img alt='arrow.up.left.and.arrow.down.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.left.and.arrow.down.right.png'> | arrow.up.left.and.arrow.down.right |
|
||||||
|
| <img alt='arrow.down.right.and.arrow.up.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.down.right.and.arrow.up.left.png'> | arrow.down.right.and.arrow.up.left | <img alt='arrow.2.squarepath' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.2.squarepath.png'> | arrow.2.squarepath | <img alt='arrow.2.circlepath' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.2.circlepath.png'> | arrow.2.circlepath | <img alt='arrow.2.circlepath.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.2.circlepath.circle.png'> | arrow.2.circlepath.circle |
|
||||||
|
| <img alt='arrow.2.circlepath.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.2.circlepath.circle.fill.png'> | arrow.2.circlepath.circle.fill | <img alt='arrow.3.trianglepath' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.3.trianglepath.png'> | arrow.3.trianglepath | <img alt='leaf.arrow.circlepath' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/leaf.arrow.circlepath.png'> | leaf.arrow.circlepath | <img alt='arrow.up.right.diamond' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.right.diamond.png'> | arrow.up.right.diamond |
|
||||||
|
| <img alt='arrow.up.right.diamond.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.up.right.diamond.fill.png'> | arrow.up.right.diamond.fill | <img alt='arrow.merge' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.merge.png'> | arrow.merge | <img alt='arrow.swap' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.swap.png'> | arrow.swap | <img alt='arrow.branch' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrow.branch.png'> | arrow.branch |
|
||||||
|
| <img alt='arrowtriangle.up' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.up.png'> | arrowtriangle.up | <img alt='arrowtriangle.up.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.up.fill.png'> | arrowtriangle.up.fill | <img alt='arrowtriangle.up.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.up.circle.png'> | arrowtriangle.up.circle | <img alt='arrowtriangle.up.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.up.circle.fill.png'> | arrowtriangle.up.circle.fill |
|
||||||
|
| <img alt='arrowtriangle.up.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.up.square.png'> | arrowtriangle.up.square | <img alt='arrowtriangle.up.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.up.square.fill.png'> | arrowtriangle.up.square.fill | <img alt='arrowtriangle.down' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.down.png'> | arrowtriangle.down | <img alt='arrowtriangle.down.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.down.fill.png'> | arrowtriangle.down.fill |
|
||||||
|
| <img alt='arrowtriangle.down.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.down.circle.png'> | arrowtriangle.down.circle | <img alt='arrowtriangle.down.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.down.circle.fill.png'> | arrowtriangle.down.circle.fill | <img alt='arrowtriangle.down.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.down.square.png'> | arrowtriangle.down.square | <img alt='arrowtriangle.down.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.down.square.fill.png'> | arrowtriangle.down.square.fill |
|
||||||
|
| <img alt='arrowtriangle.left' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.left.png'> | arrowtriangle.left | <img alt='arrowtriangle.left.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.left.fill.png'> | arrowtriangle.left.fill | <img alt='arrowtriangle.left.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.left.circle.png'> | arrowtriangle.left.circle | <img alt='arrowtriangle.left.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.left.circle.fill.png'> | arrowtriangle.left.circle.fill |
|
||||||
|
| <img alt='arrowtriangle.left.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.left.square.png'> | arrowtriangle.left.square | <img alt='arrowtriangle.left.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.left.square.fill.png'> | arrowtriangle.left.square.fill | <img alt='arrowtriangle.right' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.right.png'> | arrowtriangle.right | <img alt='arrowtriangle.right.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.right.fill.png'> | arrowtriangle.right.fill |
|
||||||
|
| <img alt='arrowtriangle.right.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.right.circle.png'> | arrowtriangle.right.circle | <img alt='arrowtriangle.right.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.right.circle.fill.png'> | arrowtriangle.right.circle.fill | <img alt='arrowtriangle.right.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.right.square.png'> | arrowtriangle.right.square | <img alt='arrowtriangle.right.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/arrowtriangle.right.square.fill.png'> | arrowtriangle.right.square.fill |
|
||||||
|
| <img alt='triangle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/triangle.png'> | triangle | <img alt='triangle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/triangle.fill.png'> | triangle.fill | <img alt='triangle.lefthalf.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/triangle.lefthalf.fill.png'> | triangle.lefthalf.fill | <img alt='triangle.righthalf.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/triangle.righthalf.fill.png'> | triangle.righthalf.fill |
|
||||||
|
| <img alt='capsule' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/capsule.png'> | capsule | <img alt='capsule.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/capsule.fill.png'> | capsule.fill | <img alt='circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.png'> | circle | <img alt='circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.fill.png'> | circle.fill |
|
||||||
|
| <img alt='circle.lefthalf.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.lefthalf.fill.png'> | circle.lefthalf.fill | <img alt='circle.righthalf.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/circle.righthalf.fill.png'> | circle.righthalf.fill | <img alt='largecircle.fill.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/largecircle.fill.circle.png'> | largecircle.fill.circle | <img alt='smallcircle.fill.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/smallcircle.fill.circle.png'> | smallcircle.fill.circle |
|
||||||
|
| <img alt='smallcircle.fill.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/smallcircle.fill.circle.fill.png'> | smallcircle.fill.circle.fill | <img alt='smallcircle.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/smallcircle.circle.png'> | smallcircle.circle | <img alt='smallcircle.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/smallcircle.circle.fill.png'> | smallcircle.circle.fill | <img alt='slash.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/slash.circle.png'> | slash.circle |
|
||||||
|
| <img alt='slash.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/slash.circle.fill.png'> | slash.circle.fill | <img alt='asterisk.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/asterisk.circle.png'> | asterisk.circle | <img alt='asterisk.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/asterisk.circle.fill.png'> | asterisk.circle.fill | <img alt='a.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/a.circle.png'> | a.circle |
|
||||||
|
| <img alt='a.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/a.circle.fill.png'> | a.circle.fill | <img alt='b.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/b.circle.png'> | b.circle | <img alt='b.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/b.circle.fill.png'> | b.circle.fill | <img alt='c.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/c.circle.png'> | c.circle |
|
||||||
|
| <img alt='c.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/c.circle.fill.png'> | c.circle.fill | <img alt='d.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/d.circle.png'> | d.circle | <img alt='d.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/d.circle.fill.png'> | d.circle.fill | <img alt='e.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/e.circle.png'> | e.circle |
|
||||||
|
| <img alt='e.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/e.circle.fill.png'> | e.circle.fill | <img alt='f.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/f.circle.png'> | f.circle | <img alt='f.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/f.circle.fill.png'> | f.circle.fill | <img alt='g.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/g.circle.png'> | g.circle |
|
||||||
|
| <img alt='g.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/g.circle.fill.png'> | g.circle.fill | <img alt='h.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/h.circle.png'> | h.circle | <img alt='h.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/h.circle.fill.png'> | h.circle.fill | <img alt='i.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/i.circle.png'> | i.circle |
|
||||||
|
| <img alt='i.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/i.circle.fill.png'> | i.circle.fill | <img alt='j.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/j.circle.png'> | j.circle | <img alt='j.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/j.circle.fill.png'> | j.circle.fill | <img alt='k.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/k.circle.png'> | k.circle |
|
||||||
|
| <img alt='k.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/k.circle.fill.png'> | k.circle.fill | <img alt='l.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/l.circle.png'> | l.circle | <img alt='l.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/l.circle.fill.png'> | l.circle.fill | <img alt='m.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/m.circle.png'> | m.circle |
|
||||||
|
| <img alt='m.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/m.circle.fill.png'> | m.circle.fill | <img alt='n.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/n.circle.png'> | n.circle | <img alt='n.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/n.circle.fill.png'> | n.circle.fill | <img alt='o.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/o.circle.png'> | o.circle |
|
||||||
|
| <img alt='o.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/o.circle.fill.png'> | o.circle.fill | <img alt='p.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/p.circle.png'> | p.circle | <img alt='p.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/p.circle.fill.png'> | p.circle.fill | <img alt='q.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/q.circle.png'> | q.circle |
|
||||||
|
| <img alt='q.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/q.circle.fill.png'> | q.circle.fill | <img alt='r.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/r.circle.png'> | r.circle | <img alt='r.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/r.circle.fill.png'> | r.circle.fill | <img alt='s.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/s.circle.png'> | s.circle |
|
||||||
|
| <img alt='s.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/s.circle.fill.png'> | s.circle.fill | <img alt='t.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/t.circle.png'> | t.circle | <img alt='t.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/t.circle.fill.png'> | t.circle.fill | <img alt='u.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/u.circle.png'> | u.circle |
|
||||||
|
| <img alt='u.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/u.circle.fill.png'> | u.circle.fill | <img alt='v.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/v.circle.png'> | v.circle | <img alt='v.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/v.circle.fill.png'> | v.circle.fill | <img alt='w.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/w.circle.png'> | w.circle |
|
||||||
|
| <img alt='w.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/w.circle.fill.png'> | w.circle.fill | <img alt='x.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/x.circle.png'> | x.circle | <img alt='x.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/x.circle.fill.png'> | x.circle.fill | <img alt='y.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/y.circle.png'> | y.circle |
|
||||||
|
| <img alt='y.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/y.circle.fill.png'> | y.circle.fill | <img alt='z.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/z.circle.png'> | z.circle | <img alt='z.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/z.circle.fill.png'> | z.circle.fill | <img alt='dollarsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dollarsign.circle.png'> | dollarsign.circle |
|
||||||
|
| <img alt='dollarsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dollarsign.circle.fill.png'> | dollarsign.circle.fill | <img alt='centsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/centsign.circle.png'> | centsign.circle | <img alt='centsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/centsign.circle.fill.png'> | centsign.circle.fill | <img alt='yensign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/yensign.circle.png'> | yensign.circle |
|
||||||
|
| <img alt='yensign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/yensign.circle.fill.png'> | yensign.circle.fill | <img alt='sterlingsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sterlingsign.circle.png'> | sterlingsign.circle | <img alt='sterlingsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sterlingsign.circle.fill.png'> | sterlingsign.circle.fill | <img alt='francsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/francsign.circle.png'> | francsign.circle |
|
||||||
|
| <img alt='francsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/francsign.circle.fill.png'> | francsign.circle.fill | <img alt='florinsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/florinsign.circle.png'> | florinsign.circle | <img alt='florinsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/florinsign.circle.fill.png'> | florinsign.circle.fill | <img alt='turkishlirasign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/turkishlirasign.circle.png'> | turkishlirasign.circle |
|
||||||
|
| <img alt='turkishlirasign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/turkishlirasign.circle.fill.png'> | turkishlirasign.circle.fill | <img alt='rublesign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rublesign.circle.png'> | rublesign.circle | <img alt='rublesign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rublesign.circle.fill.png'> | rublesign.circle.fill | <img alt='eurosign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eurosign.circle.png'> | eurosign.circle |
|
||||||
|
| <img alt='eurosign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eurosign.circle.fill.png'> | eurosign.circle.fill | <img alt='dongsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dongsign.circle.png'> | dongsign.circle | <img alt='dongsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dongsign.circle.fill.png'> | dongsign.circle.fill | <img alt='indianrupeesign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/indianrupeesign.circle.png'> | indianrupeesign.circle |
|
||||||
|
| <img alt='indianrupeesign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/indianrupeesign.circle.fill.png'> | indianrupeesign.circle.fill | <img alt='tengesign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tengesign.circle.png'> | tengesign.circle | <img alt='tengesign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tengesign.circle.fill.png'> | tengesign.circle.fill | <img alt='pesetasign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pesetasign.circle.png'> | pesetasign.circle |
|
||||||
|
| <img alt='pesetasign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pesetasign.circle.fill.png'> | pesetasign.circle.fill | <img alt='pesosign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pesosign.circle.png'> | pesosign.circle | <img alt='pesosign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pesosign.circle.fill.png'> | pesosign.circle.fill | <img alt='kipsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/kipsign.circle.png'> | kipsign.circle |
|
||||||
|
| <img alt='kipsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/kipsign.circle.fill.png'> | kipsign.circle.fill | <img alt='wonsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wonsign.circle.png'> | wonsign.circle | <img alt='wonsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wonsign.circle.fill.png'> | wonsign.circle.fill | <img alt='lirasign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lirasign.circle.png'> | lirasign.circle |
|
||||||
|
| <img alt='lirasign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lirasign.circle.fill.png'> | lirasign.circle.fill | <img alt='australsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/australsign.circle.png'> | australsign.circle | <img alt='australsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/australsign.circle.fill.png'> | australsign.circle.fill | <img alt='hryvniasign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hryvniasign.circle.png'> | hryvniasign.circle |
|
||||||
|
| <img alt='hryvniasign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hryvniasign.circle.fill.png'> | hryvniasign.circle.fill | <img alt='nairasign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/nairasign.circle.png'> | nairasign.circle | <img alt='nairasign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/nairasign.circle.fill.png'> | nairasign.circle.fill | <img alt='guaranisign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/guaranisign.circle.png'> | guaranisign.circle |
|
||||||
|
| <img alt='guaranisign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/guaranisign.circle.fill.png'> | guaranisign.circle.fill | <img alt='coloncurrencysign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/coloncurrencysign.circle.png'> | coloncurrencysign.circle | <img alt='coloncurrencysign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/coloncurrencysign.circle.fill.png'> | coloncurrencysign.circle.fill | <img alt='cedisign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cedisign.circle.png'> | cedisign.circle |
|
||||||
|
| <img alt='cedisign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cedisign.circle.fill.png'> | cedisign.circle.fill | <img alt='cruzeirosign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cruzeirosign.circle.png'> | cruzeirosign.circle | <img alt='cruzeirosign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cruzeirosign.circle.fill.png'> | cruzeirosign.circle.fill | <img alt='tugriksign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tugriksign.circle.png'> | tugriksign.circle |
|
||||||
|
| <img alt='tugriksign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tugriksign.circle.fill.png'> | tugriksign.circle.fill | <img alt='millsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/millsign.circle.png'> | millsign.circle | <img alt='millsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/millsign.circle.fill.png'> | millsign.circle.fill | <img alt='sheqelsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sheqelsign.circle.png'> | sheqelsign.circle |
|
||||||
|
| <img alt='sheqelsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sheqelsign.circle.fill.png'> | sheqelsign.circle.fill | <img alt='manatsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/manatsign.circle.png'> | manatsign.circle | <img alt='manatsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/manatsign.circle.fill.png'> | manatsign.circle.fill | <img alt='rupeesign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rupeesign.circle.png'> | rupeesign.circle |
|
||||||
|
| <img alt='rupeesign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rupeesign.circle.fill.png'> | rupeesign.circle.fill | <img alt='bahtsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bahtsign.circle.png'> | bahtsign.circle | <img alt='bahtsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bahtsign.circle.fill.png'> | bahtsign.circle.fill | <img alt='larisign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/larisign.circle.png'> | larisign.circle |
|
||||||
|
| <img alt='larisign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/larisign.circle.fill.png'> | larisign.circle.fill | <img alt='bitcoinsign.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bitcoinsign.circle.png'> | bitcoinsign.circle | <img alt='bitcoinsign.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bitcoinsign.circle.fill.png'> | bitcoinsign.circle.fill | <img alt='0.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/0.circle.png'> | 0.circle |
|
||||||
|
| <img alt='0.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/0.circle.fill.png'> | 0.circle.fill | <img alt='1.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/1.circle.png'> | 1.circle | <img alt='1.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/1.circle.fill.png'> | 1.circle.fill | <img alt='2.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/2.circle.png'> | 2.circle |
|
||||||
|
| <img alt='2.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/2.circle.fill.png'> | 2.circle.fill | <img alt='3.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/3.circle.png'> | 3.circle | <img alt='3.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/3.circle.fill.png'> | 3.circle.fill | <img alt='4.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/4.circle.png'> | 4.circle |
|
||||||
|
| <img alt='4.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/4.circle.fill.png'> | 4.circle.fill | <img alt='4.alt.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/4.alt.circle.png'> | 4.alt.circle | <img alt='4.alt.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/4.alt.circle.fill.png'> | 4.alt.circle.fill | <img alt='5.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/5.circle.png'> | 5.circle |
|
||||||
|
| <img alt='5.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/5.circle.fill.png'> | 5.circle.fill | <img alt='6.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/6.circle.png'> | 6.circle | <img alt='6.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/6.circle.fill.png'> | 6.circle.fill | <img alt='6.alt.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/6.alt.circle.png'> | 6.alt.circle |
|
||||||
|
| <img alt='6.alt.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/6.alt.circle.fill.png'> | 6.alt.circle.fill | <img alt='7.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/7.circle.png'> | 7.circle | <img alt='7.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/7.circle.fill.png'> | 7.circle.fill | <img alt='8.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/8.circle.png'> | 8.circle |
|
||||||
|
| <img alt='8.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/8.circle.fill.png'> | 8.circle.fill | <img alt='9.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/9.circle.png'> | 9.circle | <img alt='9.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/9.circle.fill.png'> | 9.circle.fill | <img alt='9.alt.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/9.alt.circle.png'> | 9.alt.circle |
|
||||||
|
| <img alt='9.alt.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/9.alt.circle.fill.png'> | 9.alt.circle.fill | <img alt='00.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/00.circle.png'> | 00.circle | <img alt='00.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/00.circle.fill.png'> | 00.circle.fill | <img alt='01.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/01.circle.png'> | 01.circle |
|
||||||
|
| <img alt='01.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/01.circle.fill.png'> | 01.circle.fill | <img alt='02.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/02.circle.png'> | 02.circle | <img alt='02.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/02.circle.fill.png'> | 02.circle.fill | <img alt='03.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/03.circle.png'> | 03.circle |
|
||||||
|
| <img alt='03.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/03.circle.fill.png'> | 03.circle.fill | <img alt='04.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/04.circle.png'> | 04.circle | <img alt='04.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/04.circle.fill.png'> | 04.circle.fill | <img alt='05.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/05.circle.png'> | 05.circle |
|
||||||
|
| <img alt='05.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/05.circle.fill.png'> | 05.circle.fill | <img alt='06.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/06.circle.png'> | 06.circle | <img alt='06.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/06.circle.fill.png'> | 06.circle.fill | <img alt='07.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/07.circle.png'> | 07.circle |
|
||||||
|
| <img alt='07.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/07.circle.fill.png'> | 07.circle.fill | <img alt='08.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/08.circle.png'> | 08.circle | <img alt='08.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/08.circle.fill.png'> | 08.circle.fill | <img alt='09.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/09.circle.png'> | 09.circle |
|
||||||
|
| <img alt='09.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/09.circle.fill.png'> | 09.circle.fill | <img alt='10.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/10.circle.png'> | 10.circle | <img alt='10.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/10.circle.fill.png'> | 10.circle.fill | <img alt='11.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/11.circle.png'> | 11.circle |
|
||||||
|
| <img alt='11.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/11.circle.fill.png'> | 11.circle.fill | <img alt='12.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/12.circle.png'> | 12.circle | <img alt='12.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/12.circle.fill.png'> | 12.circle.fill | <img alt='13.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/13.circle.png'> | 13.circle |
|
||||||
|
| <img alt='13.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/13.circle.fill.png'> | 13.circle.fill | <img alt='14.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/14.circle.png'> | 14.circle | <img alt='14.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/14.circle.fill.png'> | 14.circle.fill | <img alt='15.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/15.circle.png'> | 15.circle |
|
||||||
|
| <img alt='15.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/15.circle.fill.png'> | 15.circle.fill | <img alt='16.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/16.circle.png'> | 16.circle | <img alt='16.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/16.circle.fill.png'> | 16.circle.fill | <img alt='17.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/17.circle.png'> | 17.circle |
|
||||||
|
| <img alt='17.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/17.circle.fill.png'> | 17.circle.fill | <img alt='18.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/18.circle.png'> | 18.circle | <img alt='18.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/18.circle.fill.png'> | 18.circle.fill | <img alt='19.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/19.circle.png'> | 19.circle |
|
||||||
|
| <img alt='19.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/19.circle.fill.png'> | 19.circle.fill | <img alt='20.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/20.circle.png'> | 20.circle | <img alt='20.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/20.circle.fill.png'> | 20.circle.fill | <img alt='21.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/21.circle.png'> | 21.circle |
|
||||||
|
| <img alt='21.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/21.circle.fill.png'> | 21.circle.fill | <img alt='22.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/22.circle.png'> | 22.circle | <img alt='22.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/22.circle.fill.png'> | 22.circle.fill | <img alt='23.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/23.circle.png'> | 23.circle |
|
||||||
|
| <img alt='23.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/23.circle.fill.png'> | 23.circle.fill | <img alt='24.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/24.circle.png'> | 24.circle | <img alt='24.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/24.circle.fill.png'> | 24.circle.fill | <img alt='25.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/25.circle.png'> | 25.circle |
|
||||||
|
| <img alt='25.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/25.circle.fill.png'> | 25.circle.fill | <img alt='26.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/26.circle.png'> | 26.circle | <img alt='26.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/26.circle.fill.png'> | 26.circle.fill | <img alt='27.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/27.circle.png'> | 27.circle |
|
||||||
|
| <img alt='27.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/27.circle.fill.png'> | 27.circle.fill | <img alt='28.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/28.circle.png'> | 28.circle | <img alt='28.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/28.circle.fill.png'> | 28.circle.fill | <img alt='29.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/29.circle.png'> | 29.circle |
|
||||||
|
| <img alt='29.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/29.circle.fill.png'> | 29.circle.fill | <img alt='30.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/30.circle.png'> | 30.circle | <img alt='30.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/30.circle.fill.png'> | 30.circle.fill | <img alt='31.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/31.circle.png'> | 31.circle |
|
||||||
|
| <img alt='31.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/31.circle.fill.png'> | 31.circle.fill | <img alt='32.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/32.circle.png'> | 32.circle | <img alt='32.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/32.circle.fill.png'> | 32.circle.fill | <img alt='33.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/33.circle.png'> | 33.circle |
|
||||||
|
| <img alt='33.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/33.circle.fill.png'> | 33.circle.fill | <img alt='34.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/34.circle.png'> | 34.circle | <img alt='34.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/34.circle.fill.png'> | 34.circle.fill | <img alt='35.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/35.circle.png'> | 35.circle |
|
||||||
|
| <img alt='35.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/35.circle.fill.png'> | 35.circle.fill | <img alt='36.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/36.circle.png'> | 36.circle | <img alt='36.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/36.circle.fill.png'> | 36.circle.fill | <img alt='37.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/37.circle.png'> | 37.circle |
|
||||||
|
| <img alt='37.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/37.circle.fill.png'> | 37.circle.fill | <img alt='38.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/38.circle.png'> | 38.circle | <img alt='38.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/38.circle.fill.png'> | 38.circle.fill | <img alt='39.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/39.circle.png'> | 39.circle |
|
||||||
|
| <img alt='39.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/39.circle.fill.png'> | 39.circle.fill | <img alt='40.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/40.circle.png'> | 40.circle | <img alt='40.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/40.circle.fill.png'> | 40.circle.fill | <img alt='41.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/41.circle.png'> | 41.circle |
|
||||||
|
| <img alt='41.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/41.circle.fill.png'> | 41.circle.fill | <img alt='42.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/42.circle.png'> | 42.circle | <img alt='42.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/42.circle.fill.png'> | 42.circle.fill | <img alt='43.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/43.circle.png'> | 43.circle |
|
||||||
|
| <img alt='43.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/43.circle.fill.png'> | 43.circle.fill | <img alt='44.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/44.circle.png'> | 44.circle | <img alt='44.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/44.circle.fill.png'> | 44.circle.fill | <img alt='45.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/45.circle.png'> | 45.circle |
|
||||||
|
| <img alt='45.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/45.circle.fill.png'> | 45.circle.fill | <img alt='46.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/46.circle.png'> | 46.circle | <img alt='46.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/46.circle.fill.png'> | 46.circle.fill | <img alt='47.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/47.circle.png'> | 47.circle |
|
||||||
|
| <img alt='47.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/47.circle.fill.png'> | 47.circle.fill | <img alt='48.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/48.circle.png'> | 48.circle | <img alt='48.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/48.circle.fill.png'> | 48.circle.fill | <img alt='49.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/49.circle.png'> | 49.circle |
|
||||||
|
| <img alt='49.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/49.circle.fill.png'> | 49.circle.fill | <img alt='50.circle' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/50.circle.png'> | 50.circle | <img alt='50.circle.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/50.circle.fill.png'> | 50.circle.fill | <img alt='square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.png'> | square |
|
||||||
|
| <img alt='square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.fill.png'> | square.fill | <img alt='square.lefthalf.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.lefthalf.fill.png'> | square.lefthalf.fill | <img alt='square.righthalf.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/square.righthalf.fill.png'> | square.righthalf.fill | <img alt='a.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/a.square.png'> | a.square |
|
||||||
|
| <img alt='a.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/a.square.fill.png'> | a.square.fill | <img alt='b.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/b.square.png'> | b.square | <img alt='b.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/b.square.fill.png'> | b.square.fill | <img alt='c.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/c.square.png'> | c.square |
|
||||||
|
| <img alt='c.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/c.square.fill.png'> | c.square.fill | <img alt='d.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/d.square.png'> | d.square | <img alt='d.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/d.square.fill.png'> | d.square.fill | <img alt='e.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/e.square.png'> | e.square |
|
||||||
|
| <img alt='e.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/e.square.fill.png'> | e.square.fill | <img alt='f.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/f.square.png'> | f.square | <img alt='f.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/f.square.fill.png'> | f.square.fill | <img alt='g.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/g.square.png'> | g.square |
|
||||||
|
| <img alt='g.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/g.square.fill.png'> | g.square.fill | <img alt='h.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/h.square.png'> | h.square | <img alt='h.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/h.square.fill.png'> | h.square.fill | <img alt='i.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/i.square.png'> | i.square |
|
||||||
|
| <img alt='i.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/i.square.fill.png'> | i.square.fill | <img alt='j.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/j.square.png'> | j.square | <img alt='j.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/j.square.fill.png'> | j.square.fill | <img alt='k.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/k.square.png'> | k.square |
|
||||||
|
| <img alt='k.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/k.square.fill.png'> | k.square.fill | <img alt='l.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/l.square.png'> | l.square | <img alt='l.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/l.square.fill.png'> | l.square.fill | <img alt='m.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/m.square.png'> | m.square |
|
||||||
|
| <img alt='m.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/m.square.fill.png'> | m.square.fill | <img alt='n.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/n.square.png'> | n.square | <img alt='n.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/n.square.fill.png'> | n.square.fill | <img alt='o.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/o.square.png'> | o.square |
|
||||||
|
| <img alt='o.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/o.square.fill.png'> | o.square.fill | <img alt='p.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/p.square.png'> | p.square | <img alt='p.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/p.square.fill.png'> | p.square.fill | <img alt='q.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/q.square.png'> | q.square |
|
||||||
|
| <img alt='q.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/q.square.fill.png'> | q.square.fill | <img alt='r.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/r.square.png'> | r.square | <img alt='r.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/r.square.fill.png'> | r.square.fill | <img alt='s.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/s.square.png'> | s.square |
|
||||||
|
| <img alt='s.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/s.square.fill.png'> | s.square.fill | <img alt='t.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/t.square.png'> | t.square | <img alt='t.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/t.square.fill.png'> | t.square.fill | <img alt='u.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/u.square.png'> | u.square |
|
||||||
|
| <img alt='u.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/u.square.fill.png'> | u.square.fill | <img alt='v.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/v.square.png'> | v.square | <img alt='v.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/v.square.fill.png'> | v.square.fill | <img alt='w.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/w.square.png'> | w.square |
|
||||||
|
| <img alt='w.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/w.square.fill.png'> | w.square.fill | <img alt='x.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/x.square.png'> | x.square | <img alt='x.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/x.square.fill.png'> | x.square.fill | <img alt='y.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/y.square.png'> | y.square |
|
||||||
|
| <img alt='y.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/y.square.fill.png'> | y.square.fill | <img alt='z.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/z.square.png'> | z.square | <img alt='z.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/z.square.fill.png'> | z.square.fill | <img alt='dollarsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dollarsign.square.png'> | dollarsign.square |
|
||||||
|
| <img alt='dollarsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dollarsign.square.fill.png'> | dollarsign.square.fill | <img alt='centsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/centsign.square.png'> | centsign.square | <img alt='centsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/centsign.square.fill.png'> | centsign.square.fill | <img alt='yensign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/yensign.square.png'> | yensign.square |
|
||||||
|
| <img alt='yensign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/yensign.square.fill.png'> | yensign.square.fill | <img alt='sterlingsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sterlingsign.square.png'> | sterlingsign.square | <img alt='sterlingsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sterlingsign.square.fill.png'> | sterlingsign.square.fill | <img alt='francsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/francsign.square.png'> | francsign.square |
|
||||||
|
| <img alt='francsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/francsign.square.fill.png'> | francsign.square.fill | <img alt='florinsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/florinsign.square.png'> | florinsign.square | <img alt='florinsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/florinsign.square.fill.png'> | florinsign.square.fill | <img alt='turkishlirasign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/turkishlirasign.square.png'> | turkishlirasign.square |
|
||||||
|
| <img alt='turkishlirasign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/turkishlirasign.square.fill.png'> | turkishlirasign.square.fill | <img alt='rublesign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rublesign.square.png'> | rublesign.square | <img alt='rublesign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rublesign.square.fill.png'> | rublesign.square.fill | <img alt='eurosign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eurosign.square.png'> | eurosign.square |
|
||||||
|
| <img alt='eurosign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/eurosign.square.fill.png'> | eurosign.square.fill | <img alt='dongsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dongsign.square.png'> | dongsign.square | <img alt='dongsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/dongsign.square.fill.png'> | dongsign.square.fill | <img alt='indianrupeesign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/indianrupeesign.square.png'> | indianrupeesign.square |
|
||||||
|
| <img alt='indianrupeesign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/indianrupeesign.square.fill.png'> | indianrupeesign.square.fill | <img alt='tengesign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tengesign.square.png'> | tengesign.square | <img alt='tengesign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tengesign.square.fill.png'> | tengesign.square.fill | <img alt='pesetasign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pesetasign.square.png'> | pesetasign.square |
|
||||||
|
| <img alt='pesetasign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pesetasign.square.fill.png'> | pesetasign.square.fill | <img alt='pesosign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pesosign.square.png'> | pesosign.square | <img alt='pesosign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/pesosign.square.fill.png'> | pesosign.square.fill | <img alt='kipsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/kipsign.square.png'> | kipsign.square |
|
||||||
|
| <img alt='kipsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/kipsign.square.fill.png'> | kipsign.square.fill | <img alt='wonsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wonsign.square.png'> | wonsign.square | <img alt='wonsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/wonsign.square.fill.png'> | wonsign.square.fill | <img alt='lirasign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lirasign.square.png'> | lirasign.square |
|
||||||
|
| <img alt='lirasign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/lirasign.square.fill.png'> | lirasign.square.fill | <img alt='australsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/australsign.square.png'> | australsign.square | <img alt='australsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/australsign.square.fill.png'> | australsign.square.fill | <img alt='hryvniasign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hryvniasign.square.png'> | hryvniasign.square |
|
||||||
|
| <img alt='hryvniasign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/hryvniasign.square.fill.png'> | hryvniasign.square.fill | <img alt='nairasign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/nairasign.square.png'> | nairasign.square | <img alt='nairasign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/nairasign.square.fill.png'> | nairasign.square.fill | <img alt='guaranisign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/guaranisign.square.png'> | guaranisign.square |
|
||||||
|
| <img alt='guaranisign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/guaranisign.square.fill.png'> | guaranisign.square.fill | <img alt='coloncurrencysign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/coloncurrencysign.square.png'> | coloncurrencysign.square | <img alt='coloncurrencysign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/coloncurrencysign.square.fill.png'> | coloncurrencysign.square.fill | <img alt='cedisign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cedisign.square.png'> | cedisign.square |
|
||||||
|
| <img alt='cedisign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cedisign.square.fill.png'> | cedisign.square.fill | <img alt='cruzeirosign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cruzeirosign.square.png'> | cruzeirosign.square | <img alt='cruzeirosign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/cruzeirosign.square.fill.png'> | cruzeirosign.square.fill | <img alt='tugriksign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tugriksign.square.png'> | tugriksign.square |
|
||||||
|
| <img alt='tugriksign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/tugriksign.square.fill.png'> | tugriksign.square.fill | <img alt='millsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/millsign.square.png'> | millsign.square | <img alt='millsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/millsign.square.fill.png'> | millsign.square.fill | <img alt='sheqelsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sheqelsign.square.png'> | sheqelsign.square |
|
||||||
|
| <img alt='sheqelsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/sheqelsign.square.fill.png'> | sheqelsign.square.fill | <img alt='manatsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/manatsign.square.png'> | manatsign.square | <img alt='manatsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/manatsign.square.fill.png'> | manatsign.square.fill | <img alt='rupeesign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rupeesign.square.png'> | rupeesign.square |
|
||||||
|
| <img alt='rupeesign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/rupeesign.square.fill.png'> | rupeesign.square.fill | <img alt='bahtsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bahtsign.square.png'> | bahtsign.square | <img alt='bahtsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bahtsign.square.fill.png'> | bahtsign.square.fill | <img alt='larisign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/larisign.square.png'> | larisign.square |
|
||||||
|
| <img alt='larisign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/larisign.square.fill.png'> | larisign.square.fill | <img alt='bitcoinsign.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bitcoinsign.square.png'> | bitcoinsign.square | <img alt='bitcoinsign.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/bitcoinsign.square.fill.png'> | bitcoinsign.square.fill | <img alt='0.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/0.square.png'> | 0.square |
|
||||||
|
| <img alt='0.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/0.square.fill.png'> | 0.square.fill | <img alt='1.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/1.square.png'> | 1.square | <img alt='1.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/1.square.fill.png'> | 1.square.fill | <img alt='2.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/2.square.png'> | 2.square |
|
||||||
|
| <img alt='2.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/2.square.fill.png'> | 2.square.fill | <img alt='3.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/3.square.png'> | 3.square | <img alt='3.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/3.square.fill.png'> | 3.square.fill | <img alt='4.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/4.square.png'> | 4.square |
|
||||||
|
| <img alt='4.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/4.square.fill.png'> | 4.square.fill | <img alt='4.alt.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/4.alt.square.png'> | 4.alt.square | <img alt='4.alt.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/4.alt.square.fill.png'> | 4.alt.square.fill | <img alt='5.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/5.square.png'> | 5.square |
|
||||||
|
| <img alt='5.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/5.square.fill.png'> | 5.square.fill | <img alt='6.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/6.square.png'> | 6.square | <img alt='6.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/6.square.fill.png'> | 6.square.fill | <img alt='6.alt.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/6.alt.square.png'> | 6.alt.square |
|
||||||
|
| <img alt='6.alt.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/6.alt.square.fill.png'> | 6.alt.square.fill | <img alt='7.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/7.square.png'> | 7.square | <img alt='7.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/7.square.fill.png'> | 7.square.fill | <img alt='8.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/8.square.png'> | 8.square |
|
||||||
|
| <img alt='8.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/8.square.fill.png'> | 8.square.fill | <img alt='9.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/9.square.png'> | 9.square | <img alt='9.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/9.square.fill.png'> | 9.square.fill | <img alt='9.alt.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/9.alt.square.png'> | 9.alt.square |
|
||||||
|
| <img alt='9.alt.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/9.alt.square.fill.png'> | 9.alt.square.fill | <img alt='00.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/00.square.png'> | 00.square | <img alt='00.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/00.square.fill.png'> | 00.square.fill | <img alt='01.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/01.square.png'> | 01.square |
|
||||||
|
| <img alt='01.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/01.square.fill.png'> | 01.square.fill | <img alt='02.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/02.square.png'> | 02.square | <img alt='02.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/02.square.fill.png'> | 02.square.fill | <img alt='03.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/03.square.png'> | 03.square |
|
||||||
|
| <img alt='03.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/03.square.fill.png'> | 03.square.fill | <img alt='04.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/04.square.png'> | 04.square | <img alt='04.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/04.square.fill.png'> | 04.square.fill | <img alt='05.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/05.square.png'> | 05.square |
|
||||||
|
| <img alt='05.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/05.square.fill.png'> | 05.square.fill | <img alt='06.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/06.square.png'> | 06.square | <img alt='06.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/06.square.fill.png'> | 06.square.fill | <img alt='07.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/07.square.png'> | 07.square |
|
||||||
|
| <img alt='07.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/07.square.fill.png'> | 07.square.fill | <img alt='08.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/08.square.png'> | 08.square | <img alt='08.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/08.square.fill.png'> | 08.square.fill | <img alt='09.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/09.square.png'> | 09.square |
|
||||||
|
| <img alt='09.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/09.square.fill.png'> | 09.square.fill | <img alt='10.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/10.square.png'> | 10.square | <img alt='10.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/10.square.fill.png'> | 10.square.fill | <img alt='11.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/11.square.png'> | 11.square |
|
||||||
|
| <img alt='11.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/11.square.fill.png'> | 11.square.fill | <img alt='12.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/12.square.png'> | 12.square | <img alt='12.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/12.square.fill.png'> | 12.square.fill | <img alt='13.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/13.square.png'> | 13.square |
|
||||||
|
| <img alt='13.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/13.square.fill.png'> | 13.square.fill | <img alt='14.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/14.square.png'> | 14.square | <img alt='14.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/14.square.fill.png'> | 14.square.fill | <img alt='15.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/15.square.png'> | 15.square |
|
||||||
|
| <img alt='15.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/15.square.fill.png'> | 15.square.fill | <img alt='16.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/16.square.png'> | 16.square | <img alt='16.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/16.square.fill.png'> | 16.square.fill | <img alt='17.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/17.square.png'> | 17.square |
|
||||||
|
| <img alt='17.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/17.square.fill.png'> | 17.square.fill | <img alt='18.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/18.square.png'> | 18.square | <img alt='18.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/18.square.fill.png'> | 18.square.fill | <img alt='19.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/19.square.png'> | 19.square |
|
||||||
|
| <img alt='19.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/19.square.fill.png'> | 19.square.fill | <img alt='20.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/20.square.png'> | 20.square | <img alt='20.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/20.square.fill.png'> | 20.square.fill | <img alt='21.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/21.square.png'> | 21.square |
|
||||||
|
| <img alt='21.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/21.square.fill.png'> | 21.square.fill | <img alt='22.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/22.square.png'> | 22.square | <img alt='22.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/22.square.fill.png'> | 22.square.fill | <img alt='23.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/23.square.png'> | 23.square |
|
||||||
|
| <img alt='23.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/23.square.fill.png'> | 23.square.fill | <img alt='24.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/24.square.png'> | 24.square | <img alt='24.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/24.square.fill.png'> | 24.square.fill | <img alt='25.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/25.square.png'> | 25.square |
|
||||||
|
| <img alt='25.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/25.square.fill.png'> | 25.square.fill | <img alt='26.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/26.square.png'> | 26.square | <img alt='26.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/26.square.fill.png'> | 26.square.fill | <img alt='27.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/27.square.png'> | 27.square |
|
||||||
|
| <img alt='27.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/27.square.fill.png'> | 27.square.fill | <img alt='28.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/28.square.png'> | 28.square | <img alt='28.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/28.square.fill.png'> | 28.square.fill | <img alt='29.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/29.square.png'> | 29.square |
|
||||||
|
| <img alt='29.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/29.square.fill.png'> | 29.square.fill | <img alt='30.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/30.square.png'> | 30.square | <img alt='30.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/30.square.fill.png'> | 30.square.fill | <img alt='31.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/31.square.png'> | 31.square |
|
||||||
|
| <img alt='31.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/31.square.fill.png'> | 31.square.fill | <img alt='32.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/32.square.png'> | 32.square | <img alt='32.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/32.square.fill.png'> | 32.square.fill | <img alt='33.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/33.square.png'> | 33.square |
|
||||||
|
| <img alt='33.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/33.square.fill.png'> | 33.square.fill | <img alt='34.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/34.square.png'> | 34.square | <img alt='34.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/34.square.fill.png'> | 34.square.fill | <img alt='35.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/35.square.png'> | 35.square |
|
||||||
|
| <img alt='35.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/35.square.fill.png'> | 35.square.fill | <img alt='36.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/36.square.png'> | 36.square | <img alt='36.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/36.square.fill.png'> | 36.square.fill | <img alt='37.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/37.square.png'> | 37.square |
|
||||||
|
| <img alt='37.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/37.square.fill.png'> | 37.square.fill | <img alt='38.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/38.square.png'> | 38.square | <img alt='38.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/38.square.fill.png'> | 38.square.fill | <img alt='39.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/39.square.png'> | 39.square |
|
||||||
|
| <img alt='39.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/39.square.fill.png'> | 39.square.fill | <img alt='40.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/40.square.png'> | 40.square | <img alt='40.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/40.square.fill.png'> | 40.square.fill | <img alt='41.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/41.square.png'> | 41.square |
|
||||||
|
| <img alt='41.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/41.square.fill.png'> | 41.square.fill | <img alt='42.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/42.square.png'> | 42.square | <img alt='42.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/42.square.fill.png'> | 42.square.fill | <img alt='43.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/43.square.png'> | 43.square |
|
||||||
|
| <img alt='43.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/43.square.fill.png'> | 43.square.fill | <img alt='44.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/44.square.png'> | 44.square | <img alt='44.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/44.square.fill.png'> | 44.square.fill | <img alt='45.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/45.square.png'> | 45.square |
|
||||||
|
| <img alt='45.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/45.square.fill.png'> | 45.square.fill | <img alt='46.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/46.square.png'> | 46.square | <img alt='46.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/46.square.fill.png'> | 46.square.fill | <img alt='47.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/47.square.png'> | 47.square |
|
||||||
|
| <img alt='47.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/47.square.fill.png'> | 47.square.fill | <img alt='48.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/48.square.png'> | 48.square | <img alt='48.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/48.square.fill.png'> | 48.square.fill | <img alt='49.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/49.square.png'> | 49.square |
|
||||||
|
| <img alt='49.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/49.square.fill.png'> | 49.square.fill | <img alt='50.square' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/50.square.png'> | 50.square | <img alt='50.square.fill' src='https://raw.githubusercontent.com/andrewtavis/sf-symbols-online/refs/heads/master/glyphs_white/50.square.fill.png'> | 50.square.fill |
|
||||||
|
<!--prettier-ignore-end-->
|
||||||
894
src/api.rs
894
src/api.rs
File diff suppressed because it is too large
Load Diff
56
src/db.rs
56
src/db.rs
@@ -1,29 +1,69 @@
|
|||||||
use diesel::prelude::*;
|
|
||||||
use crate::models::DBVideo;
|
use crate::models::DBVideo;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
pub fn get_video(
|
||||||
pub fn get_video(conn: &mut SqliteConnection, video_id: String) -> Result<Option<String>, diesel::result::Error> {
|
conn: &mut SqliteConnection,
|
||||||
|
video_id: String,
|
||||||
|
) -> Result<Option<String>, diesel::result::Error> {
|
||||||
use crate::schema::videos::dsl::*;
|
use crate::schema::videos::dsl::*;
|
||||||
let result = videos
|
let result = videos
|
||||||
.filter(id.eq(video_id))
|
.filter(id.eq(video_id))
|
||||||
.first::<DBVideo>(conn)
|
.first::<DBVideo>(conn)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
match result{
|
match result {
|
||||||
Some(video) => Ok(Some(video.url)),
|
Some(video) => Ok(Some(video.url)),
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_video(conn: &mut SqliteConnection, new_id: &str, new_url: &str) -> Result<usize, diesel::result::Error> {
|
pub fn insert_video(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
new_id: &str,
|
||||||
|
new_url: &str,
|
||||||
|
) -> Result<usize, diesel::result::Error> {
|
||||||
use crate::schema::videos::dsl::*;
|
use crate::schema::videos::dsl::*;
|
||||||
diesel::insert_into(videos).values(DBVideo{
|
diesel::insert_into(videos)
|
||||||
|
.values(DBVideo {
|
||||||
id: new_id.to_string(),
|
id: new_id.to_string(),
|
||||||
url: new_url.to_string(),
|
url: new_url.to_string(),
|
||||||
}).execute(conn)
|
})
|
||||||
|
.execute(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_video(conn: &mut SqliteConnection, video_id: String) -> Result<usize, diesel::result::Error> {
|
pub fn delete_video(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
video_id: String,
|
||||||
|
) -> Result<usize, diesel::result::Error> {
|
||||||
use crate::schema::videos::dsl::*;
|
use crate::schema::videos::dsl::*;
|
||||||
diesel::delete(videos.filter(id.eq(video_id))).execute(conn)
|
diesel::delete(videos.filter(id.eq(video_id))).execute(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_table(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
table_name: &str,
|
||||||
|
) -> Result<bool, diesel::result::Error> {
|
||||||
|
use diesel::sql_query;
|
||||||
|
use diesel::sql_types::Text;
|
||||||
|
#[derive(QueryableByName)]
|
||||||
|
struct TableName {
|
||||||
|
#[diesel(sql_type = Text)]
|
||||||
|
#[diesel(column_name = name)]
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = "SELECT name FROM sqlite_master WHERE type='table' AND name = ?1";
|
||||||
|
let rows = sql_query(query)
|
||||||
|
.bind::<Text, _>(table_name)
|
||||||
|
.load::<TableName>(conn)?;
|
||||||
|
let exists = rows.first().map(|r| !r.name.is_empty()).unwrap_or(false);
|
||||||
|
Ok(exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_table(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
create_sql: &str,
|
||||||
|
) -> Result<(), diesel::result::Error> {
|
||||||
|
use diesel::sql_query;
|
||||||
|
sql_query(create_sql).execute(conn)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
42
src/main.rs
42
src/main.rs
@@ -2,12 +2,15 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
|
||||||
|
use std::{env, thread};
|
||||||
|
|
||||||
use diesel::{r2d2::{self, ConnectionManager}, SqliteConnection};
|
use diesel::{r2d2::{self, ConnectionManager}, SqliteConnection};
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use ntex_files as fs;
|
use ntex_files as fs;
|
||||||
use ntex::web;
|
use ntex::web;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
mod proxy;
|
||||||
mod db;
|
mod db;
|
||||||
mod models;
|
mod models;
|
||||||
mod providers;
|
mod providers;
|
||||||
@@ -15,9 +18,12 @@ mod schema;
|
|||||||
mod status;
|
mod status;
|
||||||
mod util;
|
mod util;
|
||||||
mod videos;
|
mod videos;
|
||||||
|
mod proxies;
|
||||||
|
|
||||||
type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
|
type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
|
||||||
|
|
||||||
|
// #[macro_use(c)]
|
||||||
|
// extern crate cute;
|
||||||
|
|
||||||
|
|
||||||
#[ntex::main]
|
#[ntex::main]
|
||||||
@@ -26,31 +32,55 @@ async fn main() -> std::io::Result<()> {
|
|||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
|
||||||
// Enable request logging
|
// Enable request logging
|
||||||
unsafe {
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
std::env::set_var("RUST_LOG", "info");
|
unsafe{
|
||||||
|
std::env::set_var("RUST_LOG", "warn");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
env_logger::init(); // You need this to actually see logs
|
env_logger::init(); // You need this to actually see logs
|
||||||
|
|
||||||
// set up database connection pool
|
// set up database connection pool
|
||||||
let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL");
|
let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL");
|
||||||
let manager = ConnectionManager::<SqliteConnection>::new(connspec);
|
let manager = ConnectionManager::<SqliteConnection>::new(connspec.clone());
|
||||||
let pool = r2d2::Pool::builder()
|
let pool = r2d2::Pool::builder()
|
||||||
.build(manager)
|
.build(manager)
|
||||||
.expect("Failed to create pool.");
|
.expect("Failed to create pool.");
|
||||||
|
|
||||||
let cache: util::cache::VideoCache = crate::util::cache::VideoCache::new();
|
let mut requester = util::requester::Requester::new();
|
||||||
|
requester.set_proxy(env::var("PROXY").unwrap_or("0".to_string()) != "0".to_string());
|
||||||
|
|
||||||
|
let cache: util::cache::VideoCache = crate::util::cache::VideoCache::new().max_size(100_000).to_owned();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
// Create a tiny runtime just for these async tasks
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("build tokio runtime");
|
||||||
|
|
||||||
|
rt.block_on(async move {
|
||||||
|
providers::init_providers_now();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
web::HttpServer::new(move || {
|
web::HttpServer::new(move || {
|
||||||
web::App::new()
|
web::App::new()
|
||||||
.state(pool.clone())
|
.state(pool.clone())
|
||||||
.state(cache.clone())
|
.state(cache.clone())
|
||||||
|
.state(requester.clone())
|
||||||
.wrap(web::middleware::Logger::default())
|
.wrap(web::middleware::Logger::default())
|
||||||
.service(web::scope("/api").configure(api::config))
|
.service(web::scope("/api").configure(api::config))
|
||||||
|
.service(web::scope("/proxy").configure(proxy::config))
|
||||||
.service(
|
.service(
|
||||||
web::resource("/")
|
web::resource("/")
|
||||||
.route(web::get().to(|| async {
|
.route(web::get().to(|req: web::HttpRequest| async move{
|
||||||
|
let host = match std::env::var("DOMAIN"){
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => req.connection_info().host().to_string()
|
||||||
|
};
|
||||||
|
let source_forward_header = format!("hottub://source?url={}", host);
|
||||||
web::HttpResponse::Found()
|
web::HttpResponse::Found()
|
||||||
.header("Location", "hottub://source?url=hottub.spacemoehre.de")
|
.header("Location", source_forward_header)
|
||||||
.finish()
|
.finish()
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use std::vec;
|
use std::fs;
|
||||||
|
use std::time::Duration;
|
||||||
|
use async_trait::async_trait;
|
||||||
use error_chain::error_chain;
|
use error_chain::error_chain;
|
||||||
use futures::future::join_all;
|
use futures::StreamExt;
|
||||||
use serde_json::error::Category;
|
use futures::stream::FuturesUnordered;
|
||||||
use wreq::Client;
|
use crate::api::{get_provider, ClientVersion};
|
||||||
use wreq_util::Emulation;
|
use crate::providers::{DynProvider, Provider};
|
||||||
use crate::api::get_provider;
|
use crate::status::Channel;
|
||||||
use crate::db;
|
|
||||||
use crate::providers::{AnyProvider, Provider};
|
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
use crate::util::interleave;
|
use crate::util::interleave;
|
||||||
use crate::videos::{self, ServerOptions, VideoItem};
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
use crate::DbPool;
|
use crate::DbPool;
|
||||||
|
|
||||||
error_chain! {
|
error_chain! {
|
||||||
@@ -31,6 +31,7 @@ impl AllProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Provider for AllProvider {
|
impl Provider for AllProvider {
|
||||||
async fn get_videos(
|
async fn get_videos(
|
||||||
&self,
|
&self,
|
||||||
@@ -42,31 +43,79 @@ impl Provider for AllProvider {
|
|||||||
per_page: String,
|
per_page: String,
|
||||||
options: ServerOptions,
|
options: ServerOptions,
|
||||||
) -> Vec<VideoItem> {
|
) -> Vec<VideoItem> {
|
||||||
let mut sites_str = options.clone().sites.unwrap();
|
let mut sites_str = options.clone().sites.unwrap_or_default();
|
||||||
if sites_str.is_empty() {
|
if sites_str.is_empty() {
|
||||||
sites_str = "perverzija,hanime,spankbang,pmvhaven,redtube,pornhub,rule34video".to_string();
|
let files = fs::read_dir("./src/providers").unwrap();
|
||||||
}
|
let providers = files.map(|entry| entry.unwrap().file_name())
|
||||||
let sites = sites_str
|
.filter(|name| name.to_str().unwrap().ends_with(".rs"))
|
||||||
.split(',')
|
.filter(|name| !name.to_str().unwrap().contains("mod.rs") && !name.to_str().unwrap().contains("all.rs"))
|
||||||
.map(|s| s.to_string()) // or s.to_owned()
|
.map(|name| name.to_str().unwrap().replace(".rs", ""))
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
let providers = sites.iter().map(|el| get_provider(el.as_str()).unwrap()).collect::<Vec<AnyProvider>>();
|
sites_str = providers.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
let futures = providers.iter().map(|provider| {
|
let providers: Vec<DynProvider> = sites_str
|
||||||
provider.get_videos(
|
.split(',')
|
||||||
cache.clone(),
|
.filter(|s| !s.is_empty())
|
||||||
pool.clone(),
|
.filter_map(|s| get_provider(s))
|
||||||
sort.clone(),
|
.collect();
|
||||||
query.clone(),
|
|
||||||
page.clone(),
|
|
||||||
per_page.clone(),
|
|
||||||
options.clone()
|
|
||||||
)
|
|
||||||
}).collect::<Vec<_>>();
|
|
||||||
let results:Vec<Vec<VideoItem>> = join_all(futures).await;
|
|
||||||
let video_items: Vec<VideoItem> = interleave(&results);
|
|
||||||
|
|
||||||
|
let mut futures = FuturesUnordered::new();
|
||||||
|
|
||||||
return video_items;
|
for provider in providers {
|
||||||
|
let cache = cache.clone();
|
||||||
|
let pool = pool.clone();
|
||||||
|
let sort = sort.clone();
|
||||||
|
let query = query.clone();
|
||||||
|
let page = page.clone();
|
||||||
|
let per_page = per_page.clone();
|
||||||
|
let options = options.clone();
|
||||||
|
|
||||||
|
// Spawn the task so it lives independently of this function
|
||||||
|
futures.push(tokio::spawn(async move {
|
||||||
|
provider.get_videos(cache, pool, sort, query, page, per_page, options).await
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut all_results = Vec::new();
|
||||||
|
let timeout_timer = tokio::time::sleep(Duration::from_secs(10));
|
||||||
|
tokio::pin!(timeout_timer);
|
||||||
|
|
||||||
|
// Collect what we can within 55 seconds
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
Some(result) = futures.next() => {
|
||||||
|
// Ignore errors (panics or task cancellations)
|
||||||
|
if let Ok(videos) = result {
|
||||||
|
all_results.push(videos);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ = &mut timeout_timer => {
|
||||||
|
// 55 seconds passed. Stop waiting and return what we have.
|
||||||
|
// The tasks remaining in 'futures' will continue running in the
|
||||||
|
// background because they were 'tokio::spawn'ed.
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
else => break, // All tasks finished before the timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interleave(&all_results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<Channel> {
|
||||||
|
let _ = clientversion;
|
||||||
|
Some(Channel {
|
||||||
|
id: "placeholder".to_string(),
|
||||||
|
name: "PLACEHOLDER".to_string(),
|
||||||
|
description: "PLACEHOLDER FOR PARENT CLASS".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://hottub.spacemoehre.de/favicon.ico".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories: vec![],
|
||||||
|
options: vec![],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
382
src/providers/beeg.rs
Normal file
382
src/providers/beeg.rs
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use crate::{status::*, util};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread;
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
Json(serde_json::Error);
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
Parse(msg: String) {
|
||||||
|
description("parse error")
|
||||||
|
display("parse error: {}", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BeegProvider {
|
||||||
|
sites: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
stars: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
categories: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BeegProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let provider = BeegProvider {
|
||||||
|
sites: Arc::new(RwLock::new(vec![FilterOption { id: "all".into(), title: "All".into() }])),
|
||||||
|
stars: Arc::new(RwLock::new(vec![FilterOption { id: "all".into(), title: "All".into() }])),
|
||||||
|
categories: Arc::new(RwLock::new(vec![FilterOption { id: "all".into(), title: "All".into() }])),
|
||||||
|
};
|
||||||
|
|
||||||
|
provider.spawn_initial_load();
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_initial_load(&self) {
|
||||||
|
let sites = Arc::clone(&self.sites);
|
||||||
|
let categories = Arc::clone(&self.categories);
|
||||||
|
let stars = Arc::clone(&self.stars);
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let rt = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
|
||||||
|
Ok(rt) => rt,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("beeg runtime init failed: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rt.block_on(async move {
|
||||||
|
if let Err(e) = Self::load_sites(sites).await {
|
||||||
|
eprintln!("beeg load_sites failed: {}", e);
|
||||||
|
}
|
||||||
|
if let Err(e) = Self::load_categories(categories).await {
|
||||||
|
eprintln!("beeg load_categories failed: {}", e);
|
||||||
|
}
|
||||||
|
if let Err(e) = Self::load_stars(stars).await {
|
||||||
|
eprintln!("beeg load_stars failed: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_tags() -> Result<Value> {
|
||||||
|
let mut requester = util::requester::Requester::new();
|
||||||
|
let text = match requester
|
||||||
|
.get("https://store.externulls.com/tag/facts/tags?get_original=true&slug=index", None)
|
||||||
|
.await {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("beeg fetch_tags failed: {}", e);
|
||||||
|
return Err(ErrorKind::Parse("failed to fetch tags".into()).into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(serde_json::from_str(&text)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_stars(stars: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
let json = Self::fetch_tags().await?;
|
||||||
|
let arr = json
|
||||||
|
.get("human")
|
||||||
|
.and_then(|v| v.as_array().map(|v| v.as_slice()))
|
||||||
|
.unwrap_or(&[]);
|
||||||
|
for s in arr {
|
||||||
|
if let (Some(name), Some(id)) = (
|
||||||
|
s.get("tg_name").and_then(|v| v.as_str()),
|
||||||
|
s.get("tg_slug").and_then(|v| v.as_str()),
|
||||||
|
) {
|
||||||
|
Self::push_unique(&stars, FilterOption { id: id.into(), title: name.into() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_categories(categories: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
let json = Self::fetch_tags().await?;
|
||||||
|
let arr = json
|
||||||
|
.get("other")
|
||||||
|
.and_then(|v| v.as_array().map(|v| v.as_slice()))
|
||||||
|
.unwrap_or(&[]);
|
||||||
|
for s in arr {
|
||||||
|
if let (Some(name), Some(id)) = (
|
||||||
|
s.get("tg_name").and_then(|v| v.as_str()),
|
||||||
|
s.get("tg_slug").and_then(|v| v.as_str()),
|
||||||
|
) {
|
||||||
|
Self::push_unique(
|
||||||
|
&categories,
|
||||||
|
FilterOption {
|
||||||
|
id: id.replace('{', "").replace('}', ""),
|
||||||
|
title: name.replace('{', "").replace('}', ""),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_sites(sites: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
let json = Self::fetch_tags().await?;
|
||||||
|
let arr = json
|
||||||
|
.get("productions")
|
||||||
|
.and_then(|v| v.as_array().map(|v| v.as_slice()))
|
||||||
|
.unwrap_or(&[]);
|
||||||
|
for s in arr {
|
||||||
|
if let (Some(name), Some(id)) = (
|
||||||
|
s.get("tg_name").and_then(|v| v.as_str()),
|
||||||
|
s.get("tg_slug").and_then(|v| v.as_str()),
|
||||||
|
) {
|
||||||
|
Self::push_unique(&sites, FilterOption { id: id.into(), title: name.into() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
|
||||||
|
if let Ok(mut vec) = target.write() {
|
||||||
|
if !vec.iter().any(|x| x.id == item.id) {
|
||||||
|
vec.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_channel(&self, _: ClientVersion) -> Channel {
|
||||||
|
Channel {
|
||||||
|
id: "beeg".into(),
|
||||||
|
name: "Beeg".into(),
|
||||||
|
description: "Watch your favorite Porn on Beeg.com".into(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=beeg.com".into(),
|
||||||
|
status: "active".into(),
|
||||||
|
categories: vec![],
|
||||||
|
options: vec![
|
||||||
|
ChannelOption {
|
||||||
|
id: "sites".into(),
|
||||||
|
title: "Sites".into(),
|
||||||
|
description: "Filter for different Sites".into(),
|
||||||
|
systemImage: "rectangle.stack".into(),
|
||||||
|
colorName: "green".into(),
|
||||||
|
options: self.sites.read().map(|v| v.clone()).unwrap_or_default(),
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
ChannelOption {
|
||||||
|
id: "categories".into(),
|
||||||
|
title: "Categories".into(),
|
||||||
|
description: "Filter for different Networks".into(),
|
||||||
|
systemImage: "list.dash".into(),
|
||||||
|
colorName: "purple".into(),
|
||||||
|
options: self.categories.read().map(|v| v.clone()).unwrap_or_default(),
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
ChannelOption {
|
||||||
|
id: "stars".into(),
|
||||||
|
title: "Stars".into(),
|
||||||
|
description: "Filter for different Pornstars".into(),
|
||||||
|
systemImage: "star.fill".into(),
|
||||||
|
colorName: "yellow".into(),
|
||||||
|
options: self.stars.read().map(|v| v.clone()).unwrap_or_default(),
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let mut slug = "";
|
||||||
|
if options.categories.is_some()
|
||||||
|
&& !options.categories.as_ref().unwrap().is_empty()
|
||||||
|
&& options.categories.as_ref().unwrap() != "all"
|
||||||
|
{
|
||||||
|
slug = options.categories.as_ref().unwrap();
|
||||||
|
}
|
||||||
|
if options.sites.is_some()
|
||||||
|
&& !options.sites.as_ref().unwrap().is_empty()
|
||||||
|
&& options.sites.as_ref().unwrap() != "all"
|
||||||
|
{
|
||||||
|
slug = options.sites.as_ref().unwrap();
|
||||||
|
}
|
||||||
|
if options.stars.is_some()
|
||||||
|
&& !options.stars.as_ref().unwrap().is_empty()
|
||||||
|
&& options.stars.as_ref().unwrap() != "all"
|
||||||
|
{
|
||||||
|
slug = options.stars.as_ref().unwrap();
|
||||||
|
}
|
||||||
|
let video_url = format!(
|
||||||
|
"https://store.externulls.com/facts/tag?limit=100&offset={}{}",
|
||||||
|
page - 1, match slug {
|
||||||
|
"" => "&id=27173".to_string(),
|
||||||
|
_ => format!("&slug={}", slug.replace(" ", "")),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let json: serde_json::Value = serde_json::from_str::<serde_json::Value>(&text).unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(json.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let video_url = format!(
|
||||||
|
"https://store.externulls.com/facts/tag?get_original=true&limit=100&offset={}&slug={}",
|
||||||
|
page - 1,
|
||||||
|
query.replace(" ", ""),
|
||||||
|
);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let json: serde_json::Value = serde_json::from_str::<serde_json::Value>(&text).unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(json.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, json: Value) -> Vec<VideoItem> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
let array = match json.as_array() {
|
||||||
|
Some(a) => a,
|
||||||
|
None => return items,
|
||||||
|
};
|
||||||
|
|
||||||
|
for video in array {
|
||||||
|
let file = match video.get("file") { Some(v) => v, None => continue };
|
||||||
|
let hls = match file.get("hls_resources") { Some(v) => v, None => continue };
|
||||||
|
let key = match hls.get("fl_cdn_multi").and_then(|v| v.as_str()) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = file.get("id").and_then(|v| v.as_i64()).unwrap_or(0).to_string();
|
||||||
|
let title = file
|
||||||
|
.get("data")
|
||||||
|
.and_then(|v| v.get(0))
|
||||||
|
.and_then(|v| v.get("cd_value"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| decode(s.as_bytes()).to_string().unwrap_or_default())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let duration = file
|
||||||
|
.get("fl_duration")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let views = video
|
||||||
|
.get("fc_facts")
|
||||||
|
.and_then(|v| v.get(0))
|
||||||
|
.and_then(|v| v.get("fc_st_views"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.and_then(|s| parse_abbreviated_number(s))
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let thumb = format!("https://thumbs.externulls.com/videos/{}/0.webp?size=480x270", id);
|
||||||
|
|
||||||
|
let mut item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
format!("https://video.externulls.com/{}", key),
|
||||||
|
"beeg".into(),
|
||||||
|
thumb,
|
||||||
|
duration as u32,
|
||||||
|
);
|
||||||
|
|
||||||
|
if views > 0 {
|
||||||
|
item = item.views(views);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for BeegProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
_: DbPool,
|
||||||
|
_: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
_: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let page = page.parse::<u8>().unwrap_or(1);
|
||||||
|
let result = match query {
|
||||||
|
Some(q) => self.query(cache, page, &q, options).await,
|
||||||
|
None => self.get(cache, page, options).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
result.unwrap_or_else(|e| {
|
||||||
|
eprintln!("beeg provider error: {}", e);
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<Channel> {
|
||||||
|
Some(self.build_channel(clientversion))
|
||||||
|
}
|
||||||
|
}
|
||||||
202
src/providers/freshporno.rs
Normal file
202
src/providers/freshporno.rs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::status::Channel;
|
||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FreshpornoProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FreshpornoProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
FreshpornoProvider {
|
||||||
|
url: "https://freshporno.net".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"popular" => "/most-popular",
|
||||||
|
"top-rated" => "/top-rated",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
let video_url = format!("{}{}/{}/", self.url, sort_string, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_lowercase().trim().replace(" ", "-");
|
||||||
|
let video_url = format!("{}/search/{}/{}/", self.url, search_string, page);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("class=\"pagination\"").collect::<Vec<&str>>()[0]
|
||||||
|
.split("class=\"page-content item\"")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0].to_string();
|
||||||
|
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
|
||||||
|
let thumb = video_segment.split("<img class=\"lazy-load").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-original=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let preview = video_segment
|
||||||
|
.split("data-preview=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"freshporno".to_string(),
|
||||||
|
thumb,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.preview(preview)
|
||||||
|
;
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for FreshpornoProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q,options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_channel(&self,clientversion:ClientVersion) -> Option<Channel> {
|
||||||
|
println!("Getting channel for placeholder with client version: {:?}",clientversion);
|
||||||
|
let _ = clientversion;
|
||||||
|
Some(Channel {
|
||||||
|
id:"placeholder".to_string(),name:"PLACEHOLDER".to_string(),description:"PLACEHOLDER FOR PARENT CLASS".to_string(),premium:false,favicon:"https://www.google.com/s2/favicons?sz=64&domain=missav.ws".to_string(),status:"active".to_string(),categories:vec![],options:vec![],nsfw:true,cacheDuration:None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
use std::vec;
|
use std::vec;
|
||||||
|
use async_trait::async_trait;
|
||||||
use error_chain::error_chain;
|
use error_chain::error_chain;
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use serde_json::error::Category;
|
|
||||||
use wreq::Client;
|
|
||||||
use wreq_util::Emulation;
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::providers::Provider;
|
use crate::providers::Provider;
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
@@ -126,13 +125,13 @@ impl HanimeProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_video_item(&self, hit: HanimeSearchResult, pool: DbPool) -> Result<VideoItem> {
|
async fn get_video_item(&self, hit: HanimeSearchResult, pool: DbPool, options: ServerOptions) -> Result<VideoItem> {
|
||||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||||
let db_result = db::get_video(&mut conn,format!("https://h.freeanimehentai.net/api/v8/video?id={}&", hit.slug.clone()));
|
let db_result = db::get_video(&mut conn,format!("https://h.freeanimehentai.net/api/v8/video?id={}&", hit.slug.clone()));
|
||||||
drop(conn);
|
drop(conn);
|
||||||
let id = hit.id.to_string();
|
let id = hit.id.to_string();
|
||||||
let title = hit.name;
|
let title = hit.name;
|
||||||
let thumb = hit.poster_url;
|
let thumb = hit.cover_url.replace("https://hanime-cdn.com", "https://hottub.spacemoehre.de/proxy/hanime-cdn");
|
||||||
let duration = (hit.duration_in_ms / 1000) as u32; // Convert ms to seconds
|
let duration = (hit.duration_in_ms / 1000) as u32; // Convert ms to seconds
|
||||||
let channel = "hanime".to_string(); // Placeholder, adjust as needed
|
let channel = "hanime".to_string(); // Placeholder, adjust as needed
|
||||||
match db_result {
|
match db_result {
|
||||||
@@ -142,6 +141,7 @@ impl HanimeProvider {
|
|||||||
.uploader(hit.brand)
|
.uploader(hit.brand)
|
||||||
.views(hit.views as u32)
|
.views(hit.views as u32)
|
||||||
.rating((hit.likes as f32 / (hit.likes + hit.dislikes)as f32) * 100 as f32)
|
.rating((hit.likes as f32 / (hit.likes + hit.dislikes)as f32) * 100 as f32)
|
||||||
|
.aspect_ratio(0.68)
|
||||||
.formats(vec![videos::VideoFormat::new(video_url.clone(), "1080".to_string(), "m3u8".to_string())]));
|
.formats(vec![videos::VideoFormat::new(video_url.clone(), "1080".to_string(), "m3u8".to_string())]));
|
||||||
}
|
}
|
||||||
Ok(None) => (),
|
Ok(None) => (),
|
||||||
@@ -150,22 +150,10 @@ impl HanimeProvider {
|
|||||||
// return Err(format!("Error fetching video from database: {}", e).into());
|
// return Err(format!("Error fetching video from database: {}", e).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = Client::builder()
|
|
||||||
.emulation(Emulation::Firefox136)
|
|
||||||
.build()?;
|
|
||||||
let url = format!("https://h.freeanimehentai.net/api/v8/video?id={}&", hit.slug);
|
let url = format!("https://h.freeanimehentai.net/api/v8/video?id={}&", hit.slug);
|
||||||
let response = client.get(url).send().await?;
|
|
||||||
|
|
||||||
let text = match response.status().is_success() {
|
let mut requester = options.requester.clone().unwrap();
|
||||||
true => {
|
let text = requester.get(&url, None).await.unwrap();
|
||||||
response.text().await?
|
|
||||||
},
|
|
||||||
false => {
|
|
||||||
print!("Failed to fetch video item: {}\n\n", response.status());
|
|
||||||
return Err(format!("Failed to fetch video item: {}", response.status()).into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let urls = text.split("\"servers\"").collect::<Vec<&str>>()[1];
|
let urls = text.split("\"servers\"").collect::<Vec<&str>>()[1];
|
||||||
let mut url_vec = vec![];
|
let mut url_vec = vec![];
|
||||||
@@ -188,7 +176,7 @@ impl HanimeProvider {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, cache: VideoCache, pool: DbPool, page: u8, query: String, sort:String) -> Result<Vec<VideoItem>> {
|
async fn get(&self, cache: VideoCache, pool: DbPool, page: u8, query: String, sort:String, options: ServerOptions) -> Result<Vec<VideoItem>> {
|
||||||
let index = format!("hanime:{}:{}:{}", query, page, sort);
|
let index = format!("hanime:{}:{}:{}", query, page, sort);
|
||||||
let order_by = match sort.contains("."){
|
let order_by = match sort.contains("."){
|
||||||
true => sort.split(".").collect::<Vec<&str>>()[0].to_string(),
|
true => sort.split(".").collect::<Vec<&str>>()[0].to_string(),
|
||||||
@@ -201,7 +189,7 @@ impl HanimeProvider {
|
|||||||
let old_items = match cache.get(&index) {
|
let old_items = match cache.get(&index) {
|
||||||
Some((time, items)) => {
|
Some((time, items)) => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 * 12 {
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 * 12 {
|
||||||
println!("Cache hit for URL: {}", index);
|
//println!("Cache hit for URL: {}", index);
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
@@ -218,12 +206,9 @@ impl HanimeProvider {
|
|||||||
.search_text(query.clone())
|
.search_text(query.clone())
|
||||||
.order_by(order_by)
|
.order_by(order_by)
|
||||||
.ordering(ordering);
|
.ordering(ordering);
|
||||||
let client = Client::builder()
|
|
||||||
.emulation(Emulation::Firefox136)
|
let mut requester = options.requester.clone().unwrap();
|
||||||
.build()?;
|
let response = requester.post_json("https://search.htv-services.com/search", &search, vec![]).await.unwrap();
|
||||||
let response = client.post("https://search.htv-services.com/search")
|
|
||||||
.json(&search)
|
|
||||||
.send().await?;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -237,7 +222,7 @@ impl HanimeProvider {
|
|||||||
let hits_json: Vec<HanimeSearchResult> = serde_json::from_str(hits.as_str())
|
let hits_json: Vec<HanimeSearchResult> = serde_json::from_str(hits.as_str())
|
||||||
.map_err(|e| format!("Failed to parse hits JSON: {}", e))?;
|
.map_err(|e| format!("Failed to parse hits JSON: {}", e))?;
|
||||||
// let timeout_duration = Duration::from_secs(120);
|
// let timeout_duration = Duration::from_secs(120);
|
||||||
let futures = hits_json.into_iter().map(|el| self.get_video_item(el.clone(), pool.clone()));
|
let futures = hits_json.into_iter().map(|el| self.get_video_item(el.clone(), pool.clone(), options.clone()));
|
||||||
let results: Vec<Result<VideoItem>> = join_all(futures).await;
|
let results: Vec<Result<VideoItem>> = join_all(futures).await;
|
||||||
let video_items: Vec<VideoItem> = results
|
let video_items: Vec<VideoItem> = results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -254,6 +239,7 @@ impl HanimeProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Provider for HanimeProvider {
|
impl Provider for HanimeProvider {
|
||||||
async fn get_videos(
|
async fn get_videos(
|
||||||
&self,
|
&self,
|
||||||
@@ -269,8 +255,8 @@ impl Provider for HanimeProvider {
|
|||||||
let _ = per_page;
|
let _ = per_page;
|
||||||
let _ = sort;
|
let _ = sort;
|
||||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
Some(q) => self.get(cache, pool, page.parse::<u8>().unwrap_or(1), q, sort).await,
|
Some(q) => self.get(cache, pool, page.parse::<u8>().unwrap_or(1), q, sort, options).await,
|
||||||
None => self.get(cache, pool, page.parse::<u8>().unwrap_or(1), "".to_string(), sort).await,
|
None => self.get(cache, pool, page.parse::<u8>().unwrap_or(1), "".to_string(), sort, options).await,
|
||||||
};
|
};
|
||||||
match videos {
|
match videos {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
|
|||||||
279
src/providers/homoxxx.rs
Normal file
279
src/providers/homoxxx.rs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::env;
|
||||||
|
use std::vec;
|
||||||
|
use wreq::{Client};
|
||||||
|
use wreq_util::Emulation;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HomoxxxProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl HomoxxxProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
HomoxxxProvider {
|
||||||
|
url: "https://homo.xxx".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"trending" => "/trending",
|
||||||
|
"popular" => "/popular",
|
||||||
|
_ => "/new",
|
||||||
|
};
|
||||||
|
let video_url = format!("{}{}/{}/", self.url, sort_string, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
||||||
|
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
||||||
|
|
||||||
|
let mut response = client.get(video_url.clone())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
if response.status().is_redirection(){
|
||||||
|
println!("Redirection detected, following to: {}", response.headers()["Location"].to_str().unwrap());
|
||||||
|
response = client.get(response.headers()["Location"].to_str().unwrap())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
}
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response.text().await?;
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
} else {
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let flare = Flaresolverr::new(flare_url);
|
||||||
|
let result = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: video_url.clone(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let video_items = match result {
|
||||||
|
Ok(res) => {
|
||||||
|
// println!("FlareSolverr response: {}", res);
|
||||||
|
self.get_video_items_from_html(res.solution.response)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error solving FlareSolverr: {}", e);
|
||||||
|
return Err("Failed to solve FlareSolverr".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_lowercase().trim().replace(" ", "-");
|
||||||
|
let mut video_url = format!("{}/search/{}/{}/", self.url, search_string, page);
|
||||||
|
|
||||||
|
if search_string.starts_with("@"){
|
||||||
|
let url_part = search_string.split("@").collect::<Vec<&str>>()[1].replace(":", "/");
|
||||||
|
video_url = format!("{}/{}/", self.url, url_part);
|
||||||
|
}
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
||||||
|
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
||||||
|
|
||||||
|
let mut response = client.get(video_url.clone())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
|
||||||
|
if response.status().is_redirection(){
|
||||||
|
|
||||||
|
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response.text().await?;
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
} else {
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let flare = Flaresolverr::new(flare_url);
|
||||||
|
let result = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: video_url.clone(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let video_items = match result {
|
||||||
|
Ok(res) => self.get_video_items_from_html(res.solution.response),
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error solving FlareSolverr: {}", e);
|
||||||
|
return Err("Failed to solve FlareSolverr".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("pagination").collect::<Vec<&str>>()[0]
|
||||||
|
.split("<div class=\"item \">")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0].to_string();
|
||||||
|
let preview_url = video_segment.split("data-preview-custom=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let raw_duration = video_segment
|
||||||
|
.split("<p class=\"duration_item\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let thumb = video_segment.split("thumb lazyload").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-src=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"homoxxx".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.preview(preview_url)
|
||||||
|
;
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for HomoxxxProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = options;
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q,)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
483
src/providers/hqporner.rs
Normal file
483
src/providers/hqporner.rs
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::status::*;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::discord::{format_error_chain, send_discord_error_report};
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::{thread, vec};
|
||||||
|
use titlecase::Titlecase;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
Json(serde_json::Error);
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
Parse(msg: String) {
|
||||||
|
description("parse error")
|
||||||
|
display("parse error: {}", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HqpornerProvider {
|
||||||
|
url: String,
|
||||||
|
stars: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
categories: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HqpornerProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let provider = HqpornerProvider {
|
||||||
|
url: "https://hqporner.com".to_string(),
|
||||||
|
stars: Arc::new(RwLock::new(vec![])),
|
||||||
|
categories: Arc::new(RwLock::new(vec![])),
|
||||||
|
};
|
||||||
|
provider.spawn_initial_load();
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_initial_load(&self) {
|
||||||
|
let url = self.url.clone();
|
||||||
|
let stars = Arc::clone(&self.stars);
|
||||||
|
let categories = Arc::clone(&self.categories);
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if let Ok(runtime) = rt {
|
||||||
|
runtime.block_on(async move {
|
||||||
|
if let Err(e) = Self::load_stars(&url, stars).await {
|
||||||
|
eprintln!("load_stars failed: {e}");
|
||||||
|
}
|
||||||
|
if let Err(e) = Self::load_categories(&url, categories).await {
|
||||||
|
eprintln!("load_categories failed: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_stars(base_url: &str, stars: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
let mut requester = Requester::new();
|
||||||
|
let text = requester
|
||||||
|
.get(&format!("{}/girls", base_url), None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("Request failed: {}", e)))?;
|
||||||
|
|
||||||
|
let stars_div = text
|
||||||
|
.split("<span>Girls</span>")
|
||||||
|
.last()
|
||||||
|
.and_then(|s| s.split("</ul>").next())
|
||||||
|
.ok_or_else(|| Error::from("Could not find stars div"))?;
|
||||||
|
|
||||||
|
for stars_element in stars_div.split("<li ").skip(1) {
|
||||||
|
let star_id = stars_element
|
||||||
|
.split("href=\"/actress/")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let star_name = stars_element
|
||||||
|
.split("<a ")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('>').nth(1))
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
if let (Some(id), Some(name)) = (star_id, star_name) {
|
||||||
|
Self::push_unique(&stars, FilterOption { id, title: name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_categories(
|
||||||
|
base_url: &str,
|
||||||
|
categories: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut requester = Requester::new();
|
||||||
|
let text = requester
|
||||||
|
.get(&format!("{}/categories", base_url), None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("Request failed: {}", e)))?;
|
||||||
|
|
||||||
|
let categories_div = text
|
||||||
|
.split("<span>Categories</span>")
|
||||||
|
.last()
|
||||||
|
.and_then(|s| s.split("</ul>").next())
|
||||||
|
.ok_or_else(|| Error::from("Could not find categories div"))?;
|
||||||
|
|
||||||
|
for categories_element in categories_div.split("<li ").skip(1) {
|
||||||
|
let category_id = categories_element
|
||||||
|
.split("href=\"/category/")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let category_name = categories_element
|
||||||
|
.split("<a ")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('>').nth(1))
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.map(|s| s.titlecase());
|
||||||
|
|
||||||
|
if let (Some(id), Some(name)) = (category_id, category_name) {
|
||||||
|
Self::push_unique(&categories, FilterOption { id, title: name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_channel(&self, _clientversion: ClientVersion) -> Channel {
|
||||||
|
Channel {
|
||||||
|
id: "hqporner".to_string(),
|
||||||
|
name: "HQPorner".to_string(),
|
||||||
|
description: "HD Porn Videos Tube".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=hqporner.com".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories: self
|
||||||
|
.categories
|
||||||
|
.read()
|
||||||
|
.map(|c| c.iter().map(|o| o.title.clone()).collect())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
options: vec![],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
|
||||||
|
if let Ok(mut vec) = target.write() {
|
||||||
|
if !vec.iter().any(|x| x.id == item.id) {
|
||||||
|
vec.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
_sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let video_url = format!("{}/hdporn/{}", self.url, page);
|
||||||
|
if let Some((time, items)) = cache.get(&video_url) {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 300 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().ok_or("No requester")?;
|
||||||
|
let text = requester
|
||||||
|
.get(&video_url, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("Request failed: {}", e)))?;
|
||||||
|
|
||||||
|
let video_items = self.get_video_items_from_html(text, &mut requester).await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.insert(video_url, video_items.clone());
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.trim().to_lowercase();
|
||||||
|
let mut video_url = format!("{}/?q={}&p={}", self.url, search_string, page);
|
||||||
|
|
||||||
|
if let Ok(stars) = self.stars.read() {
|
||||||
|
if let Some(star) = stars
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.title.to_lowercase() == search_string)
|
||||||
|
{
|
||||||
|
video_url = format!("{}/actress/{}/{}", self.url, star.id, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(cats) = self.categories.read() {
|
||||||
|
if let Some(cat) = cats
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.title.to_lowercase() == search_string)
|
||||||
|
{
|
||||||
|
video_url = format!("{}/category/{}/{}", self.url, cat.id, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((time, items)) = cache.get(&video_url) {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 300 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().ok_or("No requester")?;
|
||||||
|
let text = requester
|
||||||
|
.get(&video_url, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("Request failed: {}", e)))?;
|
||||||
|
|
||||||
|
let video_items = self.get_video_items_from_html(text, &mut requester).await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.insert(video_url, video_items.clone());
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_items_from_html(
|
||||||
|
&self,
|
||||||
|
html: String,
|
||||||
|
requester: &mut Requester,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() || html.contains("404 Not Found") {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw_videos: Vec<String> = html
|
||||||
|
.split("id=\"footer\"")
|
||||||
|
.next()
|
||||||
|
.and_then(|s| s.split("<section class=\"box features\">").nth(2))
|
||||||
|
.map(|s| {
|
||||||
|
s.split("<section class=\"box feature\">")
|
||||||
|
.skip(1)
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let futures = raw_videos
|
||||||
|
.into_iter()
|
||||||
|
.map(|el| self.get_video_item(el, requester.clone()));
|
||||||
|
|
||||||
|
join_all(futures)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.inspect(|r| {
|
||||||
|
if let Err(e) = r {
|
||||||
|
let msg = e.to_string();
|
||||||
|
let chain = format_error_chain(e);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = send_discord_error_report(
|
||||||
|
msg,
|
||||||
|
Some(chain),
|
||||||
|
Some("Hqporner Provider"),
|
||||||
|
None,
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter(|item| !item.formats.clone().unwrap().is_empty())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_item(&self, seg: String, mut requester: Requester) -> Result<VideoItem> {
|
||||||
|
let video_url = format!(
|
||||||
|
"{}{}",
|
||||||
|
self.url,
|
||||||
|
seg.split("<a href=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("url \n{seg}").into()))?
|
||||||
|
);
|
||||||
|
let title_raw = seg
|
||||||
|
.split("<h3 class=\"meta-data-title\">")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('>').nth(1))
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("title \n{seg}").into()))?;
|
||||||
|
let title = decode(title_raw.as_bytes())
|
||||||
|
.to_string()
|
||||||
|
.unwrap_or_else(|_| title_raw.to_string())
|
||||||
|
.titlecase();
|
||||||
|
|
||||||
|
let id = video_url
|
||||||
|
.split('/')
|
||||||
|
.nth(4)
|
||||||
|
.and_then(|s| s.split('.').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("id \n{seg}").into()))?
|
||||||
|
.to_string();
|
||||||
|
let thumb = format!(
|
||||||
|
"https:{}",
|
||||||
|
seg.split("onmouseleave='defaultImage(\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("thumb \n{seg}").into()))?
|
||||||
|
);
|
||||||
|
let raw_duration = seg
|
||||||
|
.split("<span class=\"icon fa-clock-o meta-data\">")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("s<").next())
|
||||||
|
.map(|s| s.replace("m ", ":"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let (tags, formats) = self.extract_media(&video_url, &mut requester).await?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
VideoItem::new(id, title, video_url, "hqporner".into(), thumb, duration)
|
||||||
|
.formats(formats)
|
||||||
|
.tags(tags),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_media(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
requester: &mut Requester,
|
||||||
|
) -> Result<(Vec<String>, Vec<VideoFormat>)> {
|
||||||
|
let mut formats = vec![];
|
||||||
|
let mut tags = vec![];
|
||||||
|
let resp = requester
|
||||||
|
.get_raw_with_headers(
|
||||||
|
url,
|
||||||
|
vec![("Referer".to_string(), "https://hqporner.com/".into())],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("Request failed: {}", e)))?;
|
||||||
|
let text = resp
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("Text conversion failed: {}", e)))?;
|
||||||
|
|
||||||
|
if text.contains("Why do I see it?") {
|
||||||
|
return Ok((tags, formats));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract Stars & Tags
|
||||||
|
if let Some(stars_block) = text
|
||||||
|
.split("icon fa-star-o")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("</li>").next())
|
||||||
|
{
|
||||||
|
for star_el in stars_block.split("href=\"/actress/").skip(1) {
|
||||||
|
let id = star_el.split('"').next().unwrap_or("").to_string();
|
||||||
|
let name = star_el
|
||||||
|
.split("\">")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
if !name.is_empty() {
|
||||||
|
tags.push(name.clone());
|
||||||
|
Self::push_unique(&self.stars, FilterOption { id, title: name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player / Video Extraction
|
||||||
|
let player_url = format!(
|
||||||
|
"https:{}",
|
||||||
|
text.split("url: '/blocks/altplayer.php?i=")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('\'').next())
|
||||||
|
.ok_or("No player link")?
|
||||||
|
);
|
||||||
|
let mut r = requester
|
||||||
|
.get_raw_with_headers(
|
||||||
|
&player_url,
|
||||||
|
vec![("Referer".to_string(), "https://hqporner.com/".into())],
|
||||||
|
).await;
|
||||||
|
|
||||||
|
if let Err(_e) = &r {
|
||||||
|
sleep(std::time::Duration::from_secs(1));
|
||||||
|
r = requester
|
||||||
|
.get_raw_with_headers(
|
||||||
|
&player_url,
|
||||||
|
vec![("Referer".to_string(), "https://hqporner.com/".into())],
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
let text2 = r
|
||||||
|
.unwrap()
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("Text conversion failed: {}", e)))?;
|
||||||
|
|
||||||
|
// Check for error response
|
||||||
|
if text2.starts_with("ERR:"){
|
||||||
|
return Ok((tags, formats));
|
||||||
|
}
|
||||||
|
|
||||||
|
let video_element = text2
|
||||||
|
.split("<video ")
|
||||||
|
.nth(2)
|
||||||
|
.and_then(|s| s.split("</video>").next())
|
||||||
|
.ok_or(format!("No video element\n{player_url}\n{text2}"))?;
|
||||||
|
for source in video_element.split("<source ").skip(1) {
|
||||||
|
let title = source
|
||||||
|
.split("title=\\\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("\\\"").next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let quality = title.split(' ').next().unwrap_or("HD").to_string();
|
||||||
|
let media_url = format!(
|
||||||
|
"https:{}",
|
||||||
|
source
|
||||||
|
.split("src=\\\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("\\\"").next())
|
||||||
|
.unwrap_or("")
|
||||||
|
);
|
||||||
|
|
||||||
|
formats.push(
|
||||||
|
VideoFormat::new(media_url, quality, "mp4".into())
|
||||||
|
.format_id(title.clone())
|
||||||
|
.format_note(title),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((tags, formats))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for HqpornerProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
_pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
_per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let page_num = page.parse::<u8>().unwrap_or(1);
|
||||||
|
let res = match query {
|
||||||
|
Some(q) => self.query(cache, page_num, &q, options).await,
|
||||||
|
None => self.get(cache, page_num, &sort, options).await,
|
||||||
|
};
|
||||||
|
res.unwrap_or_else(|e| {
|
||||||
|
eprintln!("Hqporner error: {e}");
|
||||||
|
let _ = send_discord_error_report(e.to_string(), Some(format_error_chain(&e)), None, None, file!(), line!(), module_path!());
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_channel(&self, v: ClientVersion) -> Option<Channel> {
|
||||||
|
Some(self.build_channel(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
433
src/providers/hypnotube.rs
Normal file
433
src/providers/hypnotube.rs
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::status::*;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::discord::{format_error_chain, send_discord_error_report};
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::{thread, vec};
|
||||||
|
use titlecase::Titlecase;
|
||||||
|
use wreq::Version;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
Json(serde_json::Error);
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
Parse(msg: String) {
|
||||||
|
description("parse error")
|
||||||
|
display("parse error: {}", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HypnotubeProvider {
|
||||||
|
url: String,
|
||||||
|
categories: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HypnotubeProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let provider = Self {
|
||||||
|
url: "https://hypnotube.com".to_string(),
|
||||||
|
categories: Arc::new(RwLock::new(vec![])),
|
||||||
|
};
|
||||||
|
provider.spawn_initial_load();
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_initial_load(&self) {
|
||||||
|
let url = self.url.clone();
|
||||||
|
let categories = Arc::clone(&self.categories);
|
||||||
|
|
||||||
|
thread::spawn(async move || {
|
||||||
|
let rt = match tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(rt) => rt,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("tokio runtime failed: {e}");
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(format_error_chain(&e)),
|
||||||
|
Some("HypnoTube Provider"),
|
||||||
|
Some("Failed to create tokio runtime"),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rt.block_on(async {
|
||||||
|
if let Err(e) = Self::load_categories(&url, Arc::clone(&categories)).await {
|
||||||
|
eprintln!("load_categories failed: {e}");
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(format_error_chain(&e)),
|
||||||
|
Some("HypnoTube Provider"),
|
||||||
|
Some("Failed to load categories during initial load"),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_categories(base: &str, cats: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
let mut requester = Requester::new();
|
||||||
|
let text = requester
|
||||||
|
.get(&format!("{base}/channels/"), Some(Version::HTTP_11))
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("{}", e)))?;
|
||||||
|
|
||||||
|
let block = text
|
||||||
|
.split(" title END ")
|
||||||
|
.last()
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("categories block".into()))?
|
||||||
|
.split(" main END ")
|
||||||
|
.next()
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
for el in block.split("<!-- item -->").skip(1) {
|
||||||
|
let id = el
|
||||||
|
.split("<a href=\"https://hypnotube.com/channels/")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("/\"").next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("category id: {el}").into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let title = el
|
||||||
|
.split("title=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("\"").next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("category title: {el}").into()))?
|
||||||
|
.titlecase();
|
||||||
|
|
||||||
|
Self::push_unique(&cats, FilterOption { id, title });
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
|
||||||
|
let _ = clientversion;
|
||||||
|
Channel {
|
||||||
|
id: "hypnotube".to_string(),
|
||||||
|
name: "Hypnotube".to_string(),
|
||||||
|
description: "free video hypno tube for the sissy hypnosis porn fetish".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=hypnotube.com".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories: self
|
||||||
|
.categories
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.title.clone())
|
||||||
|
.collect(),
|
||||||
|
options: vec![ChannelOption {
|
||||||
|
id: "sort".to_string(),
|
||||||
|
title: "Sort".to_string(),
|
||||||
|
description: "Sort the Videos".to_string(),
|
||||||
|
systemImage: "list.number".to_string(),
|
||||||
|
colorName: "blue".to_string(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "most recent".into(),
|
||||||
|
title: "Most Recent".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "most viewed".into(),
|
||||||
|
title: "Most Viewed".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "top rated".into(),
|
||||||
|
title: "Top Rated".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "longest".into(),
|
||||||
|
title: "Longest".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
}],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: Some(1800),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
|
||||||
|
if let Ok(mut vec) = target.write() {
|
||||||
|
if !vec.iter().any(|x| x.id == item.id) {
|
||||||
|
vec.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"top rated" => "top-rated",
|
||||||
|
"most viewed" => "most-viewed",
|
||||||
|
"longest" => "longest",
|
||||||
|
_ => "videos",
|
||||||
|
};
|
||||||
|
let video_url = format!("{}/{}/page{}.html", self.url, sort_string, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return items.clone();
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester
|
||||||
|
.get(&video_url, Some(Version::HTTP_11))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if text.contains("Sorry, no results were found.") {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone()).await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return old_items;
|
||||||
|
}
|
||||||
|
video_items
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let sort_string = match options.sort.as_deref().unwrap_or("") {
|
||||||
|
"top rated" => "rating",
|
||||||
|
"most viewed" => "views",
|
||||||
|
"longest" => "longest",
|
||||||
|
_ => "newest",
|
||||||
|
};
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/search/videos/{}/{}/page{}.html",
|
||||||
|
self.url,
|
||||||
|
query.trim().replace(" ", "%20"),
|
||||||
|
sort_string,
|
||||||
|
page
|
||||||
|
);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return items.clone();
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return items.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = match requester
|
||||||
|
.post(
|
||||||
|
format!("{}/searchgate.php", self.url).as_str(),
|
||||||
|
format!("q={}&type=videos", query.replace(" ", "+")).as_str(),
|
||||||
|
vec![("Content-Type", "application/x-www-form-urlencoded")],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
eprint!("Hypnotube search POST request failed: {}", e);
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// println!("Hypnotube search POST response status: {}", p.text().await.unwrap_or_default());
|
||||||
|
// let text = requester.get(&video_url, Some(Version::HTTP_11)).await.unwrap();
|
||||||
|
if text.contains("Sorry, no results were found.") {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone()).await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return old_items;
|
||||||
|
}
|
||||||
|
video_items
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() || html.contains("404 Not Found") {
|
||||||
|
eprint!("Hypnotube returned empty or 404 html");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = match html
|
||||||
|
.split("pagination-col col pagination")
|
||||||
|
.next()
|
||||||
|
.and_then(|s| s.split(" title END ").last())
|
||||||
|
{
|
||||||
|
Some(b) => b,
|
||||||
|
None => {
|
||||||
|
eprint!("Hypnotube Provider: Failed to get block from html");
|
||||||
|
let e = Error::from(ErrorKind::Parse("html".into()));
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(format_error_chain(&e)),
|
||||||
|
Some("Hypnotube Provider"),
|
||||||
|
Some(&format!("Failed to get block from html:\n```{html}\n```")),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut items = vec![];
|
||||||
|
for seg in block.split("<!-- item -->").skip(1) {
|
||||||
|
let video_url = match seg
|
||||||
|
.split(" href=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
{
|
||||||
|
Some(url) => url.to_string(),
|
||||||
|
None => {
|
||||||
|
eprint!("Hypnotube Provider: Failed to parse video url from segment");
|
||||||
|
let e = Error::from(ErrorKind::Parse("video url".into()));
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(format_error_chain(&e)),
|
||||||
|
Some("Hypnotube Provider"),
|
||||||
|
Some(&format!(
|
||||||
|
"Failed to parse video url from segment:\n```{seg}\n```"
|
||||||
|
)),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut title = seg
|
||||||
|
.split(" title=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
title = decode(title.clone().as_bytes())
|
||||||
|
.to_string()
|
||||||
|
.unwrap_or(title)
|
||||||
|
.titlecase();
|
||||||
|
let id = video_url
|
||||||
|
.split('/')
|
||||||
|
.nth(4)
|
||||||
|
.and_then(|s| s.split('.').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("video id".into()))
|
||||||
|
.unwrap_or_else(|_| &title.as_str());
|
||||||
|
let thumb = seg
|
||||||
|
.split("<img ")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("src=\"").nth(1))
|
||||||
|
.and_then(|s| s.split("\"").next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("thumb block".into()))
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let raw_duration = seg
|
||||||
|
.split("<span class=\"time\">")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let views = seg
|
||||||
|
.split("<span class=\"icon i-eye\"></span>")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("span class=\"sub-desc\">").nth(1))
|
||||||
|
.and_then(|s| s.split("<").next())
|
||||||
|
.unwrap_or("0")
|
||||||
|
.replace(",", "")
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id.to_owned(),
|
||||||
|
title,
|
||||||
|
video_url,
|
||||||
|
"hypnotube".into(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.views(views);
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for HypnotubeProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
_pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
_per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let page = page.parse::<u8>().unwrap_or(1);
|
||||||
|
|
||||||
|
let res = match query {
|
||||||
|
Some(q) => self.to_owned().query(cache, page, &q, options).await,
|
||||||
|
None => self.get(cache, page, &sort, options).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_channel(&self, v: ClientVersion) -> Option<Channel> {
|
||||||
|
Some(self.build_channel(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
413
src/providers/javtiful.rs
Normal file
413
src/providers/javtiful.rs
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::status::*;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::discord::{format_error_chain, send_discord_error_report};
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use htmlentity::entity::{decode, ICodedDataTrait};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::{vec};
|
||||||
|
use titlecase::Titlecase;
|
||||||
|
use wreq::Version;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
Json(serde_json::Error);
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
Parse(msg: String) {
|
||||||
|
description("parse error")
|
||||||
|
display("parse error: {}", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct JavtifulProvider {
|
||||||
|
url: String,
|
||||||
|
categories: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JavtifulProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let provider = Self {
|
||||||
|
url: "https://javtiful.com".to_string(),
|
||||||
|
categories: Arc::new(RwLock::new(vec![])),
|
||||||
|
};
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
|
||||||
|
let _ = clientversion;
|
||||||
|
Channel {
|
||||||
|
id: "javtiful".to_string(),
|
||||||
|
name: "Javtiful".to_string(),
|
||||||
|
description: "Watch Porn!".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=javtiful.com".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories: self
|
||||||
|
.categories
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.title.clone())
|
||||||
|
.collect(),
|
||||||
|
options: vec![ChannelOption {
|
||||||
|
id: "sort".to_string(),
|
||||||
|
title: "Sort".to_string(),
|
||||||
|
description: "Sort the Videos".to_string(),
|
||||||
|
systemImage: "list.number".to_string(),
|
||||||
|
colorName: "blue".to_string(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "newest".into(),
|
||||||
|
title: "Newest".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "top rated".into(),
|
||||||
|
title: "Top Rated".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "most viewed".into(),
|
||||||
|
title: "Most Viewed".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "top favorites".into(),
|
||||||
|
title: "Top Favorites".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
}],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: Some(1800),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
|
||||||
|
if let Ok(mut vec) = target.write() {
|
||||||
|
if !vec.iter().any(|x| x.id == item.id) {
|
||||||
|
vec.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"top rated" => "/sort=top_rated",
|
||||||
|
"most viewed" => "/sort=most_viewed",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/videos{}?page={}",
|
||||||
|
self.url, sort_string, page
|
||||||
|
);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, Some(Version::HTTP_2)).await.unwrap();
|
||||||
|
if page > 1 && !text.contains(&format!("<li class=\"page-item active\"><span class=\"page-link\">{}</span>", page)) {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
let video_items: Vec<VideoItem> = self
|
||||||
|
.get_video_items_from_html(text.clone(), &mut requester)
|
||||||
|
.await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match options.sort.as_deref().unwrap_or("") {
|
||||||
|
"top rated" => "/sort=top_rated",
|
||||||
|
"most viewed" => "/sort=most_viewed",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/search/videos{}?search_query={}&page={}",
|
||||||
|
self.url, sort_string, query.replace(" ","+"), page
|
||||||
|
);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, Some(Version::HTTP_2)).await.unwrap();
|
||||||
|
if page > 1 && !text.contains(&format!("<li class=\"page-item active\"><span class=\"page-link\">{}</span>", page)) {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
let video_items: Vec<VideoItem> = self
|
||||||
|
.get_video_items_from_html(text.clone(), &mut requester)
|
||||||
|
.await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_items_from_html(
|
||||||
|
&self,
|
||||||
|
html: String,
|
||||||
|
requester: &mut Requester,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() || html.contains("404 Not Found") {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = match html
|
||||||
|
.split("pagination ")
|
||||||
|
.next()
|
||||||
|
.and_then(|s| s.split("row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-4").nth(1))
|
||||||
|
{
|
||||||
|
Some(b) => b,
|
||||||
|
None => {
|
||||||
|
eprint!("Javtiful Provider: Failed to get block from html");
|
||||||
|
let e = Error::from(ErrorKind::Parse("html".into()));
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(format_error_chain(&e)),
|
||||||
|
Some("Javtiful Provider"),
|
||||||
|
Some(&format!("Failed to get block from html:\n```{html}\n```")),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
).await;
|
||||||
|
return vec![]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let futures = block
|
||||||
|
.split("card ")
|
||||||
|
.skip(1)
|
||||||
|
.filter(|seg| !seg.contains("SPONSOR"))
|
||||||
|
.map(|el| self.get_video_item(el.to_string(), requester.clone()));
|
||||||
|
|
||||||
|
join_all(futures)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.inspect(|r| {
|
||||||
|
if let Err(e) = r {
|
||||||
|
eprint!("Javtiful Provider: Failed to get video item:{}\n", e);
|
||||||
|
// Prepare data to move into the background task
|
||||||
|
let msg = e.to_string();
|
||||||
|
let chain = format_error_chain(&e);
|
||||||
|
|
||||||
|
// Spawn the report into the background - NO .await here
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = send_discord_error_report(
|
||||||
|
msg,
|
||||||
|
Some(chain),
|
||||||
|
Some("Javtiful Provider"),
|
||||||
|
Some("Failed to get video item"),
|
||||||
|
file!(), // Note: these might report the utility line
|
||||||
|
line!(), // better to hardcode or pass from outside
|
||||||
|
module_path!(),
|
||||||
|
).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_item(
|
||||||
|
&self,
|
||||||
|
seg: String,
|
||||||
|
mut requester: Requester,
|
||||||
|
) -> Result<VideoItem> {
|
||||||
|
let video_url = seg
|
||||||
|
.split(" href=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("video url\n\n{seg}".into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut title = seg
|
||||||
|
.split(" alt=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("video title\n\n{seg}").into()))?
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title).titlecase();
|
||||||
|
let id = video_url
|
||||||
|
.split('/')
|
||||||
|
.nth(5)
|
||||||
|
.and_then(|s| s.split('.').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("video id\n\n{seg}".into()))?
|
||||||
|
.to_string();
|
||||||
|
let thumb_block = seg
|
||||||
|
.split("<img ")
|
||||||
|
.nth(1)
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("thumb block\n\n{seg}".into()))?;
|
||||||
|
|
||||||
|
let thumb = thumb_block
|
||||||
|
.split("data-src=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let mut preview = seg
|
||||||
|
.split("data-trailer=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let raw_duration = seg
|
||||||
|
.split("label-duration\">")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
let (tags, formats, views) =
|
||||||
|
self.extract_media(&video_url, &mut requester).await?;
|
||||||
|
|
||||||
|
if preview.len() == 0 {
|
||||||
|
preview = format!("https://trailers.jav.si/preview/{id}.mp4");
|
||||||
|
}
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url,
|
||||||
|
"javtiful".into(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.formats(formats)
|
||||||
|
.tags(tags)
|
||||||
|
.preview(preview)
|
||||||
|
.views(views);
|
||||||
|
Ok(video_item)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_media(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
requester: &mut Requester,
|
||||||
|
) -> Result<(Vec<String>, Vec<VideoFormat>, u32)> {
|
||||||
|
let text = requester
|
||||||
|
.get(url, Some(Version::HTTP_2))
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("{}", e)))?;
|
||||||
|
let tags = text.split("related-actress").next()
|
||||||
|
.and_then(|s| s.split("video-comments").next())
|
||||||
|
.and_then(|s| s.split(">Tags<").nth(1))
|
||||||
|
.map(|tag_block| {
|
||||||
|
tag_block
|
||||||
|
.split("<a ")
|
||||||
|
.skip(1)
|
||||||
|
.filter_map(|tag_el| {
|
||||||
|
tag_el
|
||||||
|
.split('>')
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.map(|s| decode(s.as_bytes()).to_string().unwrap_or(s.to_string()).titlecase())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| vec![]);
|
||||||
|
for tag in &tags {
|
||||||
|
Self::push_unique(&self.categories, FilterOption {
|
||||||
|
id: tag.to_ascii_lowercase().replace(" ","+"),
|
||||||
|
title: tag.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let views = text.split(" Views ")
|
||||||
|
.next()
|
||||||
|
.and_then(|s| s.split(" ").last())
|
||||||
|
.and_then(|s| s.replace(".","")
|
||||||
|
.parse::<u32>().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let quality="1080p".to_string();
|
||||||
|
let video_url = url.replace("javtiful.com","hottub.spacemoehre.de/proxy/javtiful");
|
||||||
|
Ok((
|
||||||
|
tags,
|
||||||
|
vec![VideoFormat::new(video_url, quality, "video/mp4".into())],
|
||||||
|
views,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for JavtifulProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
_pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
_per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let page = page.parse::<u8>().unwrap_or(1);
|
||||||
|
|
||||||
|
let res = match query {
|
||||||
|
Some(q) => self.to_owned().query(cache, page, &q, options).await,
|
||||||
|
None => self.get(cache, page, &sort, options).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.unwrap_or_else(|e| {
|
||||||
|
eprintln!("javtiful error: {e}");
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_channel(&self, v: ClientVersion) -> Option<Channel> {
|
||||||
|
Some(self.build_channel(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
221
src/providers/missav.rs
Normal file
221
src/providers/missav.rs
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
use std::vec;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use diesel::r2d2;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{decode, ICodedDataTrait};
|
||||||
|
use futures::future::join_all;
|
||||||
|
use wreq::Version;
|
||||||
|
use crate::db;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::discord::{format_error_chain, send_discord_error_report};
|
||||||
|
use crate::videos::ServerOptions;
|
||||||
|
use crate::videos::{VideoItem};
|
||||||
|
use crate::DbPool;
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
JsonError(serde_json::Error);
|
||||||
|
Pool(r2d2::Error); // Assuming r2d2 or similar for pool
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
ParsingError(t: String) {
|
||||||
|
description("parsing error")
|
||||||
|
display("Parsing error: '{}'", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MissavProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MissavProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
MissavProvider {
|
||||||
|
url: "https://missav.ws".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, cache: VideoCache, pool: DbPool, page: u8, mut sort: String, options: ServerOptions) -> Result<Vec<VideoItem>> {
|
||||||
|
// Use ok_or to avoid unwrapping options
|
||||||
|
let language = options.language.as_ref().ok_or("Missing language")?;
|
||||||
|
let filter = options.filter.as_ref().ok_or("Missing filter")?;
|
||||||
|
let mut requester = options.requester.clone().ok_or("Missing requester")?;
|
||||||
|
|
||||||
|
if !sort.is_empty() {
|
||||||
|
sort = format!("&sort={}", sort);
|
||||||
|
}
|
||||||
|
let url_str = format!("{}/{}/{}?page={}{}", self.url, language, filter, page, sort);
|
||||||
|
|
||||||
|
if let Some((time, items)) = cache.get(&url_str) {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 3600 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = requester.get(&url_str, Some(Version::HTTP_2)).await.unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error fetching Missav URL {}: {}", url_str, e);
|
||||||
|
let _ = send_discord_error_report(e.to_string(), None, Some(&url_str), None, file!(), line!(), module_path!());
|
||||||
|
"".to_string()
|
||||||
|
});
|
||||||
|
let video_items = self.get_video_items_from_html(text, pool, requester).await;
|
||||||
|
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.insert(url_str, video_items.clone());
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(&self, cache: VideoCache, pool: DbPool, page: u8, query: &str, mut sort: String, options: ServerOptions) -> Result<Vec<VideoItem>> {
|
||||||
|
let language = options.language.as_ref().ok_or("Missing language")?;
|
||||||
|
let mut requester = options.requester.clone().ok_or("Missing requester")?;
|
||||||
|
|
||||||
|
let search_string = query.replace(" ", "%20");
|
||||||
|
if !sort.is_empty() {
|
||||||
|
sort = format!("&sort={}", sort);
|
||||||
|
}
|
||||||
|
let url_str = format!("{}/{}/search/{}?page={}{}", self.url, language, search_string, page, sort);
|
||||||
|
|
||||||
|
if let Some((time, items)) = cache.get(&url_str) {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 3600 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = requester.get(&url_str, Some(Version::HTTP_2)).await.unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error fetching Missav URL {}: {}", url_str, e);
|
||||||
|
let _ = send_discord_error_report(e.to_string(), None, Some(&url_str), None, file!(), line!(), module_path!());
|
||||||
|
"".to_string()
|
||||||
|
});
|
||||||
|
let video_items = self.get_video_items_from_html(text, pool, requester).await;
|
||||||
|
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.insert(url_str, video_items.clone());
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_items_from_html(&self, html: String, pool: DbPool, requester: Requester) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() { return vec![]; }
|
||||||
|
|
||||||
|
let segments: Vec<&str> = html.split("@mouseenter=\"setPreview(\'").collect();
|
||||||
|
if segments.len() < 2 { return vec![]; }
|
||||||
|
|
||||||
|
let mut urls = vec![];
|
||||||
|
for video_segment in &segments[1..] {
|
||||||
|
// Safer parsing: find start and end of href
|
||||||
|
if let Some(start) = video_segment.find("<a href=\"") {
|
||||||
|
let rest = &video_segment[start + 9..];
|
||||||
|
if let Some(end) = rest.find('\"') {
|
||||||
|
urls.push(rest[..end].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let futures = urls.into_iter().map(|url| self.get_video_item(url, pool.clone(), requester.clone()));
|
||||||
|
join_all(futures).await.into_iter().filter_map(Result::ok).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_item(&self, url_str: String, pool: DbPool, mut requester: Requester) -> Result<VideoItem> {
|
||||||
|
// 1. Database Check
|
||||||
|
{
|
||||||
|
let mut conn = pool.get().map_err(|e| Error::from(format!("Pool error: {}", e)))?;
|
||||||
|
if let Ok(Some(entry)) = db::get_video(&mut conn, url_str.clone()) {
|
||||||
|
if let Ok(video_item) = serde_json::from_str::<VideoItem>(entry.as_str()) {
|
||||||
|
return Ok(video_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch Page
|
||||||
|
let vid = requester.get(&url_str, Some(Version::HTTP_2)).await.unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error fetching Missav URL {}: {}", url_str, e);
|
||||||
|
let _ = send_discord_error_report(e.to_string(), None, Some(&url_str), None, file!(), line!(), module_path!());
|
||||||
|
"".to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper closure to extract content between two strings
|
||||||
|
let extract = |html: &str, start_tag: &str, end_tag: &str| -> Option<String> {
|
||||||
|
let start = html.find(start_tag)? + start_tag.len();
|
||||||
|
let rest = &html[start..];
|
||||||
|
let end = rest.find(end_tag)?;
|
||||||
|
Some(rest[..end].to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut title = extract(&vid, "<meta property=\"og:title\" content=\"", "\"")
|
||||||
|
.ok_or_else(|| ErrorKind::ParsingError(format!("title\n{:?}", vid)))?;
|
||||||
|
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
if url_str.contains("uncensored") {
|
||||||
|
title = format!("[Uncensored] {}", title);
|
||||||
|
}
|
||||||
|
|
||||||
|
let thumb = extract(&vid, "<meta property=\"og:image\" content=\"", "\"")
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let duration = extract(&vid, "<meta property=\"og:video:duration\" content=\"", "\"")
|
||||||
|
.and_then(|d| d.parse::<u32>().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let id = url_str.split('/').last().ok_or("No ID found")?.to_string();
|
||||||
|
|
||||||
|
// 3. Extract Tags (Generic approach to avoid repetitive code)
|
||||||
|
let mut tags = vec![];
|
||||||
|
for (label, prefix) in [("Actress:", "@actress"), ("Actor:", "@actor"), ("Maker:", "@maker"), ("Genre:", "@genre")] {
|
||||||
|
let marker = format!("<span>{}</span>", label);
|
||||||
|
if let Some(section) = extract(&vid, &marker, "</div>") {
|
||||||
|
for part in section.split("class=\"text-nord13 font-medium\">").skip(1) {
|
||||||
|
if let Some(val) = part.split('<').next() {
|
||||||
|
let clean = val.trim();
|
||||||
|
if !clean.is_empty() {
|
||||||
|
tags.push(format!("{}:{}", prefix, clean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Extract Video URL (The m3u8 logic)
|
||||||
|
let video_url = (|| {
|
||||||
|
let parts_str = vid.split("m3u8").nth(1)?.split("https").next()?;
|
||||||
|
let mut parts: Vec<&str> = parts_str.split('|').collect();
|
||||||
|
parts.reverse();
|
||||||
|
if parts.len() < 8 { return None; }
|
||||||
|
Some(format!("https://{}.{}/{}-{}-{}-{}-{}/playlist.m3u8",
|
||||||
|
parts[1], parts[2], parts[3], parts[4], parts[5], parts[6], parts[7]))
|
||||||
|
})().ok_or_else(|| ErrorKind::ParsingError(format!("video_url\n{:?}", vid).to_string()))?;
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(id, title, video_url, "missav".to_string(), thumb, duration)
|
||||||
|
.tags(tags)
|
||||||
|
.preview(format!("https://fourhoi.com/{}/preview.mp4", url_str.split('/').last().unwrap_or_default()));
|
||||||
|
|
||||||
|
// 5. Cache to DB
|
||||||
|
if let Ok(mut conn) = pool.get() {
|
||||||
|
let _ = db::insert_video(&mut conn, &url_str, &serde_json::to_string(&video_item).unwrap_or_default());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(video_item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for MissavProvider {
|
||||||
|
async fn get_videos(&self, cache: VideoCache, pool: DbPool, sort: String, query: Option<String>, page: String, _per_page: String, options: ServerOptions) -> Vec<VideoItem> {
|
||||||
|
let page_num = page.parse::<u8>().unwrap_or(1);
|
||||||
|
let result = match query {
|
||||||
|
Some(q) => self.query(cache, pool, page_num, &q, sort, options).await,
|
||||||
|
None => self.get(cache, pool, page_num, sort, options).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
result.unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error fetching videos: {}", e);
|
||||||
|
let _ = send_discord_error_report(e.to_string(), Some(format_error_chain(&e)), None, None, file!(), line!(), module_path!());
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use rustc_hash::FxHashMap as HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
providers::{
|
DbPool, api::ClientVersion, status::Channel, util::cache::VideoCache, videos::{ServerOptions, VideoItem}
|
||||||
all::AllProvider, hanime::HanimeProvider, perverzija::PerverzijaProvider, pmvhaven::PmvhavenProvider, pornhub::PornhubProvider, redtube::RedtubeProvider, rule34video::Rule34videoProvider, spankbang::SpankbangProvider
|
|
||||||
}, util::cache::VideoCache, videos::{ServerOptions, VideoItem}, DbPool
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod all;
|
pub mod all;
|
||||||
@@ -9,11 +12,65 @@ pub mod hanime;
|
|||||||
pub mod perverzija;
|
pub mod perverzija;
|
||||||
pub mod pmvhaven;
|
pub mod pmvhaven;
|
||||||
pub mod pornhub;
|
pub mod pornhub;
|
||||||
pub mod spankbang;
|
// pub mod spankbang;
|
||||||
pub mod rule34video;
|
pub mod homoxxx;
|
||||||
|
pub mod okporn;
|
||||||
|
pub mod okxxx;
|
||||||
|
pub mod perfectgirls;
|
||||||
|
pub mod pornhat;
|
||||||
pub mod redtube;
|
pub mod redtube;
|
||||||
|
pub mod rule34video;
|
||||||
|
// pub mod hentaimoon;
|
||||||
|
pub mod missav;
|
||||||
|
pub mod porn00;
|
||||||
|
pub mod sxyprn;
|
||||||
|
pub mod xxthots;
|
||||||
|
// pub mod noodlemagazine;
|
||||||
|
pub mod freshporno;
|
||||||
|
pub mod omgxxx;
|
||||||
|
pub mod paradisehill;
|
||||||
|
pub mod pornzog;
|
||||||
|
pub mod youjizz;
|
||||||
|
pub mod beeg;
|
||||||
|
pub mod tnaflix;
|
||||||
|
pub mod pornxp;
|
||||||
|
pub mod rule34gen;
|
||||||
|
pub mod xxdbx;
|
||||||
|
pub mod hqporner;
|
||||||
|
pub mod noodlemagazine;
|
||||||
|
pub mod pimpbunny;
|
||||||
|
pub mod javtiful;
|
||||||
|
pub mod hypnotube;
|
||||||
|
|
||||||
pub trait Provider {
|
// convenient alias
|
||||||
|
pub type DynProvider = Arc<dyn Provider>;
|
||||||
|
|
||||||
|
pub static ALL_PROVIDERS: Lazy<HashMap<&'static str, DynProvider>> = Lazy::new(|| {
|
||||||
|
let mut m = HashMap::default();
|
||||||
|
m.insert("omgxxx", Arc::new(omgxxx::OmgxxxProvider::new()) as DynProvider);
|
||||||
|
m.insert("beeg", Arc::new(beeg::BeegProvider::new()) as DynProvider);
|
||||||
|
m.insert("tnaflix", Arc::new(tnaflix::TnaflixProvider::new()) as DynProvider);
|
||||||
|
m.insert("pornxp", Arc::new(pornxp::PornxpProvider::new()) as DynProvider);
|
||||||
|
m.insert("rule34gen", Arc::new(rule34gen::Rule34genProvider::new()) as DynProvider);
|
||||||
|
m.insert("xxdbx", Arc::new(xxdbx::XxdbxProvider::new()) as DynProvider);
|
||||||
|
m.insert("hqporner", Arc::new(hqporner::HqpornerProvider::new()) as DynProvider);
|
||||||
|
m.insert("pmvhaven", Arc::new(pmvhaven::PmvhavenProvider::new()) as DynProvider);
|
||||||
|
m.insert("noodlemagazine", Arc::new(noodlemagazine::NoodlemagazineProvider::new()) as DynProvider);
|
||||||
|
m.insert("pimpbunny", Arc::new(pimpbunny::PimpbunnyProvider::new()) as DynProvider);
|
||||||
|
m.insert("javtiful", Arc::new(javtiful::JavtifulProvider::new()) as DynProvider);
|
||||||
|
m.insert("hypnotube", Arc::new(hypnotube::HypnotubeProvider::new()) as DynProvider);
|
||||||
|
// add more here as you migrate them
|
||||||
|
m
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn init_providers_now() {
|
||||||
|
// Idempotent & thread-safe: runs the Lazy init exactly once.
|
||||||
|
Lazy::force(&ALL_PROVIDERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Provider: Send + Sync {
|
||||||
async fn get_videos(
|
async fn get_videos(
|
||||||
&self,
|
&self,
|
||||||
cache: VideoCache,
|
cache: VideoCache,
|
||||||
@@ -24,76 +81,24 @@ pub trait Provider {
|
|||||||
per_page: String,
|
per_page: String,
|
||||||
options: ServerOptions,
|
options: ServerOptions,
|
||||||
) -> Vec<VideoItem>;
|
) -> Vec<VideoItem>;
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<Channel> {
|
||||||
pub enum AnyProvider {
|
|
||||||
All(AllProvider),
|
|
||||||
Perverzija(PerverzijaProvider),
|
|
||||||
Hanime(HanimeProvider),
|
|
||||||
Spankbang(SpankbangProvider),
|
|
||||||
Pornhub(PornhubProvider),
|
|
||||||
Pmvhaven(PmvhavenProvider),
|
|
||||||
Rule34video(Rule34videoProvider),
|
|
||||||
Redtube(RedtubeProvider), // Assuming Redtube is similar to Rule34video
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Provider for AnyProvider {
|
|
||||||
async fn get_videos(
|
|
||||||
&self,
|
|
||||||
cache: VideoCache,
|
|
||||||
pool: DbPool,
|
|
||||||
sort: String,
|
|
||||||
query: Option<String>,
|
|
||||||
page: String,
|
|
||||||
per_page: String,
|
|
||||||
options: ServerOptions
|
|
||||||
) -> Vec<VideoItem> {
|
|
||||||
println!(
|
println!(
|
||||||
"/api/videos: sort={:?}, query={:?}, page={:?}, provider={:?}",
|
"Getting channel for placeholder with client version: {:?}",
|
||||||
sort, query, page, self
|
clientversion
|
||||||
);
|
);
|
||||||
match self {
|
let _ = clientversion;
|
||||||
AnyProvider::Perverzija(p) => {
|
Some(Channel {
|
||||||
p.get_videos(
|
id: "placeholder".to_string(),
|
||||||
cache.clone(),
|
name: "PLACEHOLDER".to_string(),
|
||||||
pool.clone(),
|
description: "PLACEHOLDER FOR PARENT CLASS".to_string(),
|
||||||
sort.clone(),
|
premium: false,
|
||||||
query.clone(),
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=missav.ws".to_string(),
|
||||||
page.clone(),
|
status: "active".to_string(),
|
||||||
per_page.clone(),
|
categories: vec![],
|
||||||
options,
|
options: vec![],
|
||||||
)
|
nsfw: true,
|
||||||
.await
|
cacheDuration: None,
|
||||||
}
|
})
|
||||||
AnyProvider::Hanime(p) => {
|
|
||||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
AnyProvider::Spankbang(p) => {
|
|
||||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
AnyProvider::Pornhub(p) => {
|
|
||||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
AnyProvider::Pmvhaven(p) => {
|
|
||||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
AnyProvider::Rule34video(p) => {
|
|
||||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
AnyProvider::Redtube(p) => {
|
|
||||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
AnyProvider::All(p) => {
|
|
||||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
308
src/providers/noodlemagazine.rs
Normal file
308
src/providers/noodlemagazine.rs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::status::*;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use htmlentity::entity::{decode, ICodedDataTrait};
|
||||||
|
use wreq::Version;
|
||||||
|
use titlecase::Titlecase;
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
Parse(msg: String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NoodlemagazineProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoodlemagazineProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
url: "https://noodlemagazine.com".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_channel(&self, _clientversion: ClientVersion) -> Channel {
|
||||||
|
Channel {
|
||||||
|
id: "noodlemagazine".into(),
|
||||||
|
name: "Noodlemagazine".into(),
|
||||||
|
description: "The Best Search Engine of HD Videos".into(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=noodlemagazine.com".into(),
|
||||||
|
status: "active".into(),
|
||||||
|
categories: vec![],
|
||||||
|
options: vec![],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: Some(1800),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
_sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/popular/recent?sort_by=views&sort_order=desc&p={}",
|
||||||
|
self.url,
|
||||||
|
page.saturating_sub(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((t, i)) if t.elapsed().unwrap_or_default().as_secs() < 300 => return Ok(i.clone()),
|
||||||
|
Some((_, i)) => i.clone(),
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = match options.requester.clone() {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return Ok(old_items),
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = requester
|
||||||
|
.get(&video_url, Some(Version::HTTP_2))
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let items = self.get_video_items_from_html(text, requester).await;
|
||||||
|
|
||||||
|
if items.is_empty() {
|
||||||
|
Ok(old_items)
|
||||||
|
} else {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url, items.clone());
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let q = query.trim().replace(' ', "%20");
|
||||||
|
let video_url = format!("{}/video/{}?p={}", self.url, q, page.saturating_sub(1));
|
||||||
|
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((t, i)) if t.elapsed().unwrap_or_default().as_secs() < 300 => return Ok(i.clone()),
|
||||||
|
Some((_, i)) => i.clone(),
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = match options.requester.clone() {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return Ok(old_items),
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = requester
|
||||||
|
.get(&video_url, Some(Version::HTTP_2))
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let items = self.get_video_items_from_html(text, requester).await;
|
||||||
|
|
||||||
|
if items.is_empty() {
|
||||||
|
Ok(old_items)
|
||||||
|
} else {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url, items.clone());
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_items_from_html(
|
||||||
|
&self,
|
||||||
|
html: String,
|
||||||
|
requester: Requester,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() || html.contains("404 Not Found") {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let section = match html.split(">Show more</div>").next() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let list = match section
|
||||||
|
.split("<div class=\"list_videos\" id=\"list_videos\">")
|
||||||
|
.nth(1)
|
||||||
|
{
|
||||||
|
Some(l) => l,
|
||||||
|
None => return vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let raw_videos = list
|
||||||
|
.split("<div class=\"item\">")
|
||||||
|
.skip(1)
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let futures = raw_videos.map(|v| self.get_video_item(v, requester.clone()));
|
||||||
|
let results = join_all(futures).await;
|
||||||
|
|
||||||
|
results.into_iter().filter_map(Result::ok).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_item(
|
||||||
|
&self,
|
||||||
|
video_segment: String,
|
||||||
|
requester: Requester,
|
||||||
|
) -> Result<VideoItem> {
|
||||||
|
let href = video_segment
|
||||||
|
.split("<a href=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.ok_or_else(|| Error::from("missing href"))?;
|
||||||
|
|
||||||
|
let video_url = format!("{}{}", self.url, href);
|
||||||
|
|
||||||
|
let mut title = video_segment
|
||||||
|
.split("<div class=\"title\">")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
title = decode(title.as_bytes())
|
||||||
|
.to_string()
|
||||||
|
.unwrap_or(title)
|
||||||
|
.titlecase();
|
||||||
|
|
||||||
|
let id = video_url
|
||||||
|
.split('/')
|
||||||
|
.nth(4)
|
||||||
|
.and_then(|s| s.split('.').next())
|
||||||
|
.ok_or_else(|| Error::from("missing id"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let thumb = video_segment
|
||||||
|
.split("data-src=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let raw_duration = video_segment
|
||||||
|
.split("#clock-o\"></use></svg>")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.unwrap_or("0:00");
|
||||||
|
|
||||||
|
let duration = parse_time_to_seconds(raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let views = video_segment
|
||||||
|
.split("#eye\"></use></svg>")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.and_then(|v| parse_abbreviated_number(v.trim()))
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let formats = self
|
||||||
|
.extract_media(&video_url, requester)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| Error::from("media extraction failed"))?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url,
|
||||||
|
"noodlemagazine".into(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.views(views)
|
||||||
|
.formats(formats),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_media(
|
||||||
|
&self,
|
||||||
|
video_url: &String,
|
||||||
|
mut requester: Requester,
|
||||||
|
) -> Option<Vec<VideoFormat>> {
|
||||||
|
let text = requester
|
||||||
|
.get(video_url, Some(Version::HTTP_2))
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let json_str = text
|
||||||
|
.split("window.playlist = ")
|
||||||
|
.nth(1)?
|
||||||
|
.split(';')
|
||||||
|
.next()?;
|
||||||
|
|
||||||
|
let json: serde_json::Value = serde_json::from_str(json_str).ok()?;
|
||||||
|
let sources = json["sources"].as_array()?;
|
||||||
|
|
||||||
|
let mut formats = vec![];
|
||||||
|
|
||||||
|
for s in sources {
|
||||||
|
let file = s["file"].as_str()?.to_string();
|
||||||
|
let label = s["label"].as_str().unwrap_or("unknown").to_string();
|
||||||
|
|
||||||
|
formats.push(
|
||||||
|
VideoFormat::new(file, label.clone(), "video/mp4".into())
|
||||||
|
.format_id(label.clone())
|
||||||
|
.format_note(label.clone())
|
||||||
|
.http_header("Referer".into(), video_url.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(formats.into_iter().rev().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for NoodlemagazineProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = pool;
|
||||||
|
let _ = per_page;
|
||||||
|
|
||||||
|
let page = page.parse::<u8>().unwrap_or(1);
|
||||||
|
|
||||||
|
let res = match query {
|
||||||
|
Some(q) => self.query(cache, page, &q, options).await,
|
||||||
|
None => self.get(cache, page, &sort, options).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.unwrap_or_else(|e| {
|
||||||
|
eprintln!("Noodlemagazine error: {e}");
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<Channel> {
|
||||||
|
Some(self.build_channel(clientversion))
|
||||||
|
}
|
||||||
|
}
|
||||||
269
src/providers/okporn.rs
Normal file
269
src/providers/okporn.rs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::env;
|
||||||
|
use std::vec;
|
||||||
|
use wreq::{Client};
|
||||||
|
use wreq_util::Emulation;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OkpornProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl OkpornProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
OkpornProvider {
|
||||||
|
url: "https://ok.porn".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"trending" => "/trending",
|
||||||
|
"popular" => "/popular",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
let video_url = format!("{}{}/{}/", self.url, sort_string, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
||||||
|
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
||||||
|
|
||||||
|
let mut response = client.get(video_url.clone())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
if response.status().is_redirection(){
|
||||||
|
|
||||||
|
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
}
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response.text().await?;
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
} else {
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let flare = Flaresolverr::new(flare_url);
|
||||||
|
let result = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: video_url.clone(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let video_items = match result {
|
||||||
|
Ok(res) => {
|
||||||
|
// println!("FlareSolverr response: {}", res);
|
||||||
|
self.get_video_items_from_html(res.solution.response)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error solving FlareSolverr: {}", e);
|
||||||
|
return Err("Failed to solve FlareSolverr".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_lowercase().trim().replace(" ", "-");
|
||||||
|
let video_url = format!("{}/search/{}/{}/", self.url, search_string, page);
|
||||||
|
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
||||||
|
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
||||||
|
|
||||||
|
let mut response = client.get(video_url.clone())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
|
||||||
|
if response.status().is_redirection(){
|
||||||
|
|
||||||
|
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response.text().await?;
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
} else {
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let flare = Flaresolverr::new(flare_url);
|
||||||
|
let result = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: video_url.clone(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let video_items = match result {
|
||||||
|
Ok(res) => self.get_video_items_from_html(res.solution.response),
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error solving FlareSolverr: {}", e);
|
||||||
|
return Err("Failed to solve FlareSolverr".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html
|
||||||
|
.split("<div class=\"item ")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = format!("{}{}", self.url, video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]);
|
||||||
|
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let raw_duration = video_segment.split("<span class=\"duration_item\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let thumb = video_segment.split("<img class=\"thumb lazy-load\" src=\"").collect::<Vec<&str>>()[1].split("data-original=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"okporn".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
;
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for OkpornProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = options;
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q,)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
314
src/providers/okxxx.rs
Normal file
314
src/providers/okxxx.rs
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::env;
|
||||||
|
use std::vec;
|
||||||
|
use wreq::{Client};
|
||||||
|
use wreq_util::Emulation;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OkxxxProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl OkxxxProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
OkxxxProvider {
|
||||||
|
url: "https://ok.xxx".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"trending" => "/trending",
|
||||||
|
"popular" => "/popular",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
let video_url = format!("{}{}/{}/", self.url, sort_string, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
||||||
|
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
||||||
|
|
||||||
|
let mut response = client.get(video_url.clone())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
if response.status().is_redirection(){
|
||||||
|
println!("Redirection detected, following to: {}", response.headers()["Location"].to_str().unwrap());
|
||||||
|
response = client.get(response.headers()["Location"].to_str().unwrap())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
}
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response.text().await?;
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
} else {
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let flare = Flaresolverr::new(flare_url);
|
||||||
|
let result = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: video_url.clone(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let video_items = match result {
|
||||||
|
Ok(res) => {
|
||||||
|
// println!("FlareSolverr response: {}", res);
|
||||||
|
self.get_video_items_from_html(res.solution.response)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error solving FlareSolverr: {}", e);
|
||||||
|
return Err("Failed to solve FlareSolverr".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_lowercase().trim().replace(" ", "-");
|
||||||
|
let mut video_url = format!("{}/search/{}/{}/", self.url, search_string, page);
|
||||||
|
|
||||||
|
if search_string.starts_with("@"){
|
||||||
|
let url_part = search_string.split("@").collect::<Vec<&str>>()[1].replace(":", "/");
|
||||||
|
video_url = format!("{}/{}/", self.url, url_part);
|
||||||
|
}
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
||||||
|
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
||||||
|
|
||||||
|
let mut response = client.get(video_url.clone())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
|
||||||
|
if response.status().is_redirection(){
|
||||||
|
|
||||||
|
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response.text().await?;
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
} else {
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let flare = Flaresolverr::new(flare_url);
|
||||||
|
let result = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: video_url.clone(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let video_items = match result {
|
||||||
|
Ok(res) => self.get_video_items_from_html(res.solution.response),
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error solving FlareSolverr: {}", e);
|
||||||
|
return Err("Failed to solve FlareSolverr".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("<div class=\"pagination\"").collect::<Vec<&str>>()[0]
|
||||||
|
.split("item thumb-bl thumb-bl-video video_")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = format!("{}{}", self.url, video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]);
|
||||||
|
let preview_url = video_segment.split("data-preview-custom=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let raw_duration = video_segment.split("fa fa-clock-o").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<span>").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let thumb = format!("https:{}", video_segment.split(" class=\"thumb lazy-load\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-original=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string());
|
||||||
|
|
||||||
|
let mut tags = vec![];
|
||||||
|
if video_segment.contains("href=\"/sites/"){
|
||||||
|
let raw_tags = video_segment.split("href=\"/sites/").collect::<Vec<&str>>()[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.split("/\"").collect::<Vec<&str>>()[0].to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
for tag in raw_tags {
|
||||||
|
if !tag.is_empty() {
|
||||||
|
tags.push(format!("@sites:{}",tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if video_segment.contains("href=\"/models/"){
|
||||||
|
let raw_tags = video_segment.split("href=\"/models/").collect::<Vec<&str>>()[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.split("/\"").collect::<Vec<&str>>()[0].to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
for tag in raw_tags {
|
||||||
|
if !tag.is_empty() {
|
||||||
|
tags.push(format!("@models:{}",tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let views_part = video_segment.split("fa fa-eye").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<span>").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let views = parse_abbreviated_number(&views_part).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"okxxx".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.preview(preview_url)
|
||||||
|
.views(views)
|
||||||
|
.tags(tags)
|
||||||
|
;
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for OkxxxProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = options;
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q,)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
627
src/providers/omgxxx.rs
Normal file
627
src/providers/omgxxx.rs
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use crate::{status::*, util};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread;
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OmgxxxProvider {
|
||||||
|
url: String,
|
||||||
|
sites: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
networks: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
stars: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
}
|
||||||
|
impl OmgxxxProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let provider = OmgxxxProvider {
|
||||||
|
url: "https://www.omg.xxx".to_string(),
|
||||||
|
sites: Arc::new(RwLock::new(vec![FilterOption {
|
||||||
|
id: "all".to_string(),
|
||||||
|
title: "All".to_string(),
|
||||||
|
}])),
|
||||||
|
networks: Arc::new(RwLock::new(vec![FilterOption {
|
||||||
|
id: "all".to_string(),
|
||||||
|
title: "All".to_string(),
|
||||||
|
}])),
|
||||||
|
stars: Arc::new(RwLock::new(vec![FilterOption {
|
||||||
|
id: "all".to_string(),
|
||||||
|
title: "All".to_string(),
|
||||||
|
}])),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Kick off the background load but return immediately
|
||||||
|
provider.spawn_initial_load();
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_initial_load(&self) {
|
||||||
|
let url = self.url.clone();
|
||||||
|
let sites = Arc::clone(&self.sites);
|
||||||
|
let networks = Arc::clone(&self.networks);
|
||||||
|
let stars = Arc::clone(&self.stars);
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
// Create a tiny runtime just for these async tasks
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("build tokio runtime");
|
||||||
|
|
||||||
|
rt.block_on(async move {
|
||||||
|
// If you have a streaming sites loader, call it here too
|
||||||
|
if let Err(e) = Self::load_sites(&url, sites).await {
|
||||||
|
eprintln!("load_sites_into failed: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = Self::load_networks(&url, networks).await {
|
||||||
|
eprintln!("load_networks failed: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = Self::load_stars(&url, stars).await {
|
||||||
|
eprintln!("load_stars failed: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_stars(base_url: &str, stars: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
let mut requester = util::requester::Requester::new();
|
||||||
|
for page in [1..10].into_iter().flatten() {
|
||||||
|
let text = requester
|
||||||
|
.get(
|
||||||
|
format!("{}/models/total-videos/{}/?gender_id=0", &base_url, page).as_str(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if text.contains("404 Not Found") || text.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let stars_div = text
|
||||||
|
.split("<div class=\"list-models\">")
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.last()
|
||||||
|
.unwrap()
|
||||||
|
.split("custom_list_models_models_list_pagination")
|
||||||
|
.collect::<Vec<&str>>()[0];
|
||||||
|
for stars_element in stars_div.split("<a ").collect::<Vec<&str>>()[1..].to_vec() {
|
||||||
|
let star_url = stars_element.split("href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0];
|
||||||
|
let star_id = star_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let star_name = stars_element
|
||||||
|
.split("<strong class=\"title\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
Self::push_unique(
|
||||||
|
&stars,
|
||||||
|
FilterOption {
|
||||||
|
id: star_id,
|
||||||
|
title: star_name,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_sites(base_url: &str, sites: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
let mut requester = util::requester::Requester::new();
|
||||||
|
let mut page = 0;
|
||||||
|
loop {
|
||||||
|
page += 1;
|
||||||
|
let text = requester
|
||||||
|
.get(format!("{}/sites/{}/", &base_url, page).as_str(), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if text.contains("404 Not Found") || text.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let sites_div = text
|
||||||
|
.split("id=\"list_content_sources_sponsors_list_items\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("class=\"pagination\"")
|
||||||
|
.collect::<Vec<&str>>()[0];
|
||||||
|
for sites_element in
|
||||||
|
sites_div.split("class=\"headline\"").collect::<Vec<&str>>()[1..].to_vec()
|
||||||
|
{
|
||||||
|
let site_url = sites_element.split("href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0];
|
||||||
|
let site_id = site_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let site_name = sites_element.split("<h2>").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
Self::push_unique(
|
||||||
|
&sites,
|
||||||
|
FilterOption {
|
||||||
|
id: site_id,
|
||||||
|
title: site_name,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_networks(base_url: &str, networks: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
let mut requester = util::requester::Requester::new();
|
||||||
|
let text = requester.get(&base_url, None).await.unwrap();
|
||||||
|
let networks_div = text.split("class=\"sites__list\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("</div>")
|
||||||
|
.collect::<Vec<&str>>()[0];
|
||||||
|
for network_element in
|
||||||
|
networks_div.split("sites__item").collect::<Vec<&str>>()[1..].to_vec()
|
||||||
|
{
|
||||||
|
if network_element.contains("sites__all") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let network_url = network_element.split("href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0];
|
||||||
|
let network_id = network_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let network_name = network_element.split(">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
Self::push_unique(
|
||||||
|
&networks,
|
||||||
|
FilterOption {
|
||||||
|
id: network_id,
|
||||||
|
title: network_name,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push one item with minimal lock time and dedup by id
|
||||||
|
fn push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
|
||||||
|
if let Ok(mut vec) = target.write() {
|
||||||
|
if !vec.iter().any(|x| x.id == item.id) {
|
||||||
|
vec.push(item);
|
||||||
|
// Optional: keep it sorted for nicer UX
|
||||||
|
// vec.sort_by(|a,b| a.title.cmp(&b.title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
|
||||||
|
let _ = clientversion;
|
||||||
|
let sites: Vec<FilterOption> = self
|
||||||
|
.sites
|
||||||
|
.read()
|
||||||
|
.map(|g| g.clone()) // or: .map(|g| g.to_vec())
|
||||||
|
.unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new())
|
||||||
|
|
||||||
|
let networks: Vec<FilterOption> = self
|
||||||
|
.networks
|
||||||
|
.read()
|
||||||
|
.map(|g| g.clone()) // or: .map(|g| g.to_vec())
|
||||||
|
.unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new())
|
||||||
|
|
||||||
|
let stars: Vec<FilterOption> = self
|
||||||
|
.stars
|
||||||
|
.read()
|
||||||
|
.map(|g| g.clone()) // or: .map(|g| g.to_vec())
|
||||||
|
.unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new())
|
||||||
|
|
||||||
|
Channel {
|
||||||
|
id: "omgxxx".to_string(),
|
||||||
|
name: "OMG XXX".to_string(),
|
||||||
|
description: "OMG look at that Collection!".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=www.omg.xxx".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories: vec![],
|
||||||
|
options: vec![
|
||||||
|
ChannelOption {
|
||||||
|
id: "sort".to_string(),
|
||||||
|
title: "Sort".to_string(),
|
||||||
|
description: "Sort the Videos".to_string(),
|
||||||
|
systemImage: "list.number".to_string(),
|
||||||
|
colorName: "blue".to_string(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "latest-updates".into(),
|
||||||
|
title: "Latest".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "most-popular".into(),
|
||||||
|
title: "Most Viewed".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "top-rated".into(),
|
||||||
|
title: "Top Rated".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
ChannelOption {
|
||||||
|
id: "sites".to_string(),
|
||||||
|
title: "Sites".to_string(),
|
||||||
|
description: "Filter for different Sites".to_string(),
|
||||||
|
systemImage: "rectangle.stack".to_string(),
|
||||||
|
colorName: "green".to_string(),
|
||||||
|
options: sites,
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
ChannelOption {
|
||||||
|
id: "networks".to_string(),
|
||||||
|
title: "Networks".to_string(),
|
||||||
|
description: "Filter for different Networks".to_string(),
|
||||||
|
systemImage: "list.dash".to_string(),
|
||||||
|
colorName: "purple".to_string(),
|
||||||
|
options: networks,
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
ChannelOption {
|
||||||
|
id: "stars".to_string(),
|
||||||
|
title: "Stars".to_string(),
|
||||||
|
description: "Filter for different Pornstars".to_string(),
|
||||||
|
systemImage: "star.fill".to_string(),
|
||||||
|
colorName: "yellow".to_string(),
|
||||||
|
options: stars,
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let mut sort_string: String = match sort {
|
||||||
|
"top-rated" => "top-rated".to_string(),
|
||||||
|
"most-popular" => "most-popular".to_string(),
|
||||||
|
_ => "latest-updates".to_string(),
|
||||||
|
};
|
||||||
|
let alt_sort_string: String = match sort {
|
||||||
|
"top-rated" => "/top-rated".to_string(),
|
||||||
|
"most-popular" => "/most-popular".to_string(),
|
||||||
|
_ => "".to_string(),
|
||||||
|
};
|
||||||
|
if options.network.is_some()
|
||||||
|
&& !options.network.as_ref().unwrap().is_empty()
|
||||||
|
&& options.network.as_ref().unwrap() != "all"
|
||||||
|
{
|
||||||
|
sort_string = format!(
|
||||||
|
"networks/{}{}",
|
||||||
|
options.network.as_ref().unwrap(),
|
||||||
|
alt_sort_string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if options.sites.is_some()
|
||||||
|
&& !options.sites.as_ref().unwrap().is_empty()
|
||||||
|
&& options.sites.as_ref().unwrap() != "all"
|
||||||
|
{
|
||||||
|
sort_string = format!(
|
||||||
|
"sites/{}{}",
|
||||||
|
options.sites.as_ref().unwrap(),
|
||||||
|
alt_sort_string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if options.stars.is_some()
|
||||||
|
&& !options.stars.as_ref().unwrap().is_empty()
|
||||||
|
&& options.stars.as_ref().unwrap() != "all"
|
||||||
|
{
|
||||||
|
sort_string = format!(
|
||||||
|
"models/{}{}",
|
||||||
|
options.stars.as_ref().unwrap(),
|
||||||
|
alt_sort_string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let video_url = format!("{}/{}/{}/", self.url, sort_string, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let mut search_type = "search";
|
||||||
|
let mut search_string = query.to_string().to_ascii_lowercase().trim().to_string();
|
||||||
|
match self
|
||||||
|
.stars
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.title.to_ascii_lowercase() == search_string)
|
||||||
|
{
|
||||||
|
Some(star) => {
|
||||||
|
search_type = "models";
|
||||||
|
search_string = star.id.clone();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
match self
|
||||||
|
.sites
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.title.to_ascii_lowercase() == search_string)
|
||||||
|
{
|
||||||
|
Some(site) => {
|
||||||
|
search_type = "sites";
|
||||||
|
search_string = site.id.clone();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
let mut video_url = format!("{}/{}/{}/{}/", self.url, search_type, search_string, page);
|
||||||
|
video_url = video_url.replace(" ", "+");
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_site_id_from_name(&self, site_name: &str) -> Option<String> {
|
||||||
|
// site_name.to_lowercase().replace(" ", "")
|
||||||
|
for site in self.sites.read().unwrap().iter() {
|
||||||
|
if site
|
||||||
|
.title
|
||||||
|
.to_lowercase()
|
||||||
|
.replace(" ", "")
|
||||||
|
.replace(".com", "")
|
||||||
|
== site_name.to_lowercase().replace(" ", "")
|
||||||
|
{
|
||||||
|
return Some(site.id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
if !html.contains("class=\"item\"") {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
let raw_videos = html.split("videos_list_pagination").collect::<Vec<&str>>()[0]
|
||||||
|
.split(" class=\"pagination\" ")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.split("class=\"list-videos\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("class=\"item\"")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let mut title = video_segment.split(" title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
|
||||||
|
let thumb = match video_segment.split("img loading").collect::<Vec<&str>>()[1]
|
||||||
|
.contains("data-src=\"")
|
||||||
|
{
|
||||||
|
true => video_segment.split("img loading").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-src=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string(),
|
||||||
|
false => video_segment.split("img loading").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-original=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string(),
|
||||||
|
};
|
||||||
|
let raw_duration = video_segment
|
||||||
|
.split("<span class=\"duration\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.split(" ")
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.last()
|
||||||
|
.unwrap_or(&"")
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32;
|
||||||
|
let views = parse_abbreviated_number(
|
||||||
|
video_segment
|
||||||
|
.split("<div class=\"views\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string()
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let preview = video_segment
|
||||||
|
.split("data-preview=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let site_name = title
|
||||||
|
.split("]")
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.first()
|
||||||
|
.unwrap_or(&"")
|
||||||
|
.trim_start_matches("[");
|
||||||
|
let site_id = self
|
||||||
|
.get_site_id_from_name(site_name)
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let mut tags = match video_segment.contains("class=\"models\">") {
|
||||||
|
true => video_segment
|
||||||
|
.split("class=\"models\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("</div>")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.split("href=\"")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
Self::push_unique(
|
||||||
|
&self.stars,
|
||||||
|
FilterOption {
|
||||||
|
id: s.split("/").collect::<Vec<&str>>()[4].to_string(),
|
||||||
|
title: s.split(">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
s.split(">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.to_vec(),
|
||||||
|
false => vec![],
|
||||||
|
};
|
||||||
|
if !site_id.is_empty() {
|
||||||
|
Self::push_unique(
|
||||||
|
&self.sites,
|
||||||
|
FilterOption {
|
||||||
|
id: site_id,
|
||||||
|
title: site_name.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
tags.push(site_name.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"omgxxx".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.views(views)
|
||||||
|
.preview(preview)
|
||||||
|
.tags(tags);
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for OmgxxxProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<crate::status::Channel> {
|
||||||
|
Some(self.build_channel(clientversion))
|
||||||
|
}
|
||||||
|
}
|
||||||
240
src/providers/paradisehill.rs
Normal file
240
src/providers/paradisehill.rs
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
use crate::videos::VideoItem;
|
||||||
|
use crate::videos::{self, ServerOptions};
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::vec;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
JsonError(serde_json::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ParadisehillProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl ParadisehillProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
ParadisehillProvider {
|
||||||
|
url: "https://en.paradisehill.cc".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let url_str = format!("{}/all/?sort=created_at&page={}", self.url, page);
|
||||||
|
|
||||||
|
let old_items = match cache.get(&url_str) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = requester.get(&url_str, None).await.unwrap();
|
||||||
|
// Pass a reference to options if needed, or reconstruct as needed
|
||||||
|
let video_items: Vec<VideoItem> = self
|
||||||
|
.get_video_items_from_html(text.clone(), requester)
|
||||||
|
.await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&url_str);
|
||||||
|
cache.insert(url_str.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
// Extract needed fields from options at the start
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let search_string = query.replace(" ", "+");
|
||||||
|
let url_str = format!(
|
||||||
|
"{}/search/?pattern={}&page={}",
|
||||||
|
self.url, search_string, page
|
||||||
|
);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&url_str) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let text = requester.get(&url_str, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self
|
||||||
|
.get_video_items_from_html(text.clone(), requester)
|
||||||
|
.await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&url_str);
|
||||||
|
cache.insert(url_str.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_items_from_html(
|
||||||
|
&self,
|
||||||
|
html: String,
|
||||||
|
requester: Requester,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let raw_videos = html.split("item list-film-item").collect::<Vec<&str>>()[1..].to_vec();
|
||||||
|
let mut urls: Vec<String> = vec![];
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line.to_string().trim());
|
||||||
|
// }
|
||||||
|
|
||||||
|
let url_str = format!(
|
||||||
|
"{}{}",
|
||||||
|
self.url,
|
||||||
|
video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
urls.push(url_str.clone());
|
||||||
|
}
|
||||||
|
let futures = urls
|
||||||
|
.into_iter()
|
||||||
|
.map(|el| self.get_video_item(el.clone(), requester.clone()));
|
||||||
|
let results: Vec<Result<VideoItem>> = join_all(futures).await;
|
||||||
|
let video_items: Vec<VideoItem> = results.into_iter().filter_map(Result::ok).collect();
|
||||||
|
|
||||||
|
return video_items;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_item(&self, url_str: String, mut requester: Requester) -> Result<VideoItem> {
|
||||||
|
let vid = requester.get(&url_str, None).await.unwrap();
|
||||||
|
let mut title = vid
|
||||||
|
.split("<meta property=\"og:title\" content=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let thumb = format!(
|
||||||
|
"{}{}",
|
||||||
|
self.url,
|
||||||
|
vid.split("<meta property=\"og:image\" content=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let video_urls = vid.split("var videoList = ").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"src\":\"")
|
||||||
|
.collect::<Vec<&str>>()[1..].to_vec();
|
||||||
|
let mut formats = vec![];
|
||||||
|
for url in video_urls {
|
||||||
|
let video_url = url
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.replace("\\", "")
|
||||||
|
.to_string();
|
||||||
|
let format =
|
||||||
|
videos::VideoFormat::new(video_url.clone(), "1080".to_string(), "mp4".to_string())
|
||||||
|
// .protocol("https".to_string())
|
||||||
|
.format_id(video_url.split("/").last().unwrap().to_string())
|
||||||
|
.format_note(format!("{}", video_url.split("_").last().unwrap().replace(".mp4", "").to_string()))
|
||||||
|
;
|
||||||
|
formats.push(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
formats.reverse();
|
||||||
|
let id = url_str
|
||||||
|
.split("/")
|
||||||
|
.collect::<Vec<&str>>()[3]
|
||||||
|
.split("_")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
url_str.clone(),
|
||||||
|
"paradisehill".to_string(),
|
||||||
|
thumb,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.aspect_ratio(0.697674419 as f32)
|
||||||
|
.formats(formats);
|
||||||
|
|
||||||
|
return Ok(video_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for ParadisehillProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = pool;
|
||||||
|
let _ = sort;
|
||||||
|
let _ = per_page;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
317
src/providers/perfectgirls.rs
Normal file
317
src/providers/perfectgirls.rs
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::env;
|
||||||
|
use std::vec;
|
||||||
|
use wreq::{Client};
|
||||||
|
use wreq_util::Emulation;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PerfectgirlsProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl PerfectgirlsProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
PerfectgirlsProvider {
|
||||||
|
url: "https://www.perfectgirls.xxx".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"trending" => "/trending",
|
||||||
|
"popular" => "/popular",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
let video_url = format!("{}{}/{}/", self.url, sort_string, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
||||||
|
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
||||||
|
|
||||||
|
let mut response = client.get(video_url.clone())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
if response.status().is_redirection(){
|
||||||
|
println!("Redirection detected, following to: {}", response.headers()["Location"].to_str().unwrap());
|
||||||
|
response = client.get(response.headers()["Location"].to_str().unwrap())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
}
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response.text().await?;
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
} else {
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let flare = Flaresolverr::new(flare_url);
|
||||||
|
let result = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: video_url.clone(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let video_items = match result {
|
||||||
|
Ok(res) => {
|
||||||
|
// println!("FlareSolverr response: {}", res);
|
||||||
|
self.get_video_items_from_html(res.solution.response)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error solving FlareSolverr: {}", e);
|
||||||
|
return Err("Failed to solve FlareSolverr".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_lowercase().trim().replace(" ", "-");
|
||||||
|
let mut video_url = format!("{}/search/{}/{}/", self.url, search_string, page);
|
||||||
|
|
||||||
|
if search_string.starts_with("@"){
|
||||||
|
let url_part = search_string.split("@").collect::<Vec<&str>>()[1].replace(":", "/");
|
||||||
|
video_url = format!("{}/{}/", self.url, url_part);
|
||||||
|
}
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
||||||
|
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
||||||
|
|
||||||
|
let mut response = client.get(video_url.clone())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
|
||||||
|
if response.status().is_redirection(){
|
||||||
|
|
||||||
|
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||||
|
// .proxy(proxy.clone())
|
||||||
|
.send().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response.text().await?;
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
} else {
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let flare = Flaresolverr::new(flare_url);
|
||||||
|
let result = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: video_url.clone(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let video_items = match result {
|
||||||
|
Ok(res) => self.get_video_items_from_html(res.solution.response),
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error solving FlareSolverr: {}", e);
|
||||||
|
return Err("Failed to solve FlareSolverr".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("<div class=\"pagination\"").collect::<Vec<&str>>()[0]
|
||||||
|
.split("item thumb-bl thumb-bl-video video_")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = format!("{}{}", self.url, video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]);
|
||||||
|
let preview_url = video_segment.split("data-preview-custom=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let raw_duration = video_segment.split("fa fa-clock-o").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<span>").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let mut thumb = video_segment.split(" class=\"thumb lazy-load\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-original=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
if thumb.starts_with("//"){
|
||||||
|
thumb = format!("https:{}",thumb);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tags = vec![];
|
||||||
|
if video_segment.contains("href=\"/channels/"){
|
||||||
|
let raw_tags = video_segment.split("href=\"/channels/").collect::<Vec<&str>>()[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.split("/\"").collect::<Vec<&str>>()[0].to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
for tag in raw_tags {
|
||||||
|
if !tag.is_empty() {
|
||||||
|
tags.push(format!("@channels:{}",tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if video_segment.contains("href=\"/pornstars/"){
|
||||||
|
let raw_tags = video_segment.split("href=\"/pornstars/").collect::<Vec<&str>>()[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.split("/\"").collect::<Vec<&str>>()[0].to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
for tag in raw_tags {
|
||||||
|
if !tag.is_empty() {
|
||||||
|
tags.push(format!("@pornstars:{}",tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let views_part = video_segment.split("fa fa-eye").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<span>").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let views = parse_abbreviated_number(&views_part).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"perfectgirls".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.preview(preview_url)
|
||||||
|
.views(views)
|
||||||
|
.tags(tags)
|
||||||
|
;
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for PerfectgirlsProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = options;
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q,)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,20 @@
|
|||||||
use std::vec;
|
use crate::DbPool;
|
||||||
use std::env;
|
|
||||||
use error_chain::error_chain;
|
|
||||||
use htmlentity::entity::{decode, ICodedDataTrait};
|
|
||||||
use futures::future::join_all;
|
|
||||||
use serde::Serialize;
|
|
||||||
use wreq::Client;
|
|
||||||
use wreq_util::Emulation;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::providers::perverzija;
|
|
||||||
use crate::providers::Provider;
|
use crate::providers::Provider;
|
||||||
use crate::schema::videos::url;
|
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
|
||||||
use crate::util::time::parse_time_to_seconds;
|
use crate::util::time::parse_time_to_seconds;
|
||||||
use crate::videos::ServerOptions;
|
use crate::videos::ServerOptions;
|
||||||
use crate::videos::{self, VideoEmbed, VideoItem};
|
use crate::videos::{self, VideoEmbed, VideoItem};
|
||||||
use crate::DbPool;
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use wreq::Version;
|
||||||
|
use std::vec;
|
||||||
|
use wreq::Client;
|
||||||
|
use wreq_util::Emulation;
|
||||||
|
|
||||||
error_chain! {
|
error_chain! {
|
||||||
foreign_links {
|
foreign_links {
|
||||||
@@ -27,7 +24,6 @@ error_chain! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
struct PerverzijaDbEntry {
|
struct PerverzijaDbEntry {
|
||||||
url_string: String,
|
url_string: String,
|
||||||
@@ -44,8 +40,14 @@ impl PerverzijaProvider {
|
|||||||
url: "https://tube.perverzija.com/".to_string(),
|
url: "https://tube.perverzija.com/".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn get(&self, cache:VideoCache, pool:DbPool, page: u8, featured: String) -> Result<Vec<VideoItem>> {
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
page: u8,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let featured = options.featured.unwrap_or("".to_string());
|
||||||
let mut prefix_uri = "".to_string();
|
let mut prefix_uri = "".to_string();
|
||||||
if featured == "featured" {
|
if featured == "featured" {
|
||||||
prefix_uri = "featured-scenes/".to_string();
|
prefix_uri = "featured-scenes/".to_string();
|
||||||
@@ -58,10 +60,9 @@ impl PerverzijaProvider {
|
|||||||
let old_items = match cache.get(&url_str) {
|
let old_items = match cache.get(&url_str) {
|
||||||
Some((time, items)) => {
|
Some((time, items)) => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||||
println!("Cache hit for URL: {}", url_str);
|
//println!("Cache hit for URL: {}", url_str);
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
items.clone()
|
items.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,43 +71,9 @@ impl PerverzijaProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
let client = Client::builder()
|
let text = requester.get(&url_str, Some(Version::HTTP_2)).await.unwrap();
|
||||||
.emulation(Emulation::Firefox136)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let response = client.get(url_str.clone()).send().await?;
|
|
||||||
// print!("Response: {:?}\n", response);
|
|
||||||
if response.status().is_success() {
|
|
||||||
let text = response.text().await?;
|
|
||||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone(), pool);
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone(), pool);
|
||||||
if !video_items.is_empty() {
|
|
||||||
cache.remove(&url_str);
|
|
||||||
cache.insert(url_str.clone(), video_items.clone());
|
|
||||||
} else{
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
|
||||||
} else {
|
|
||||||
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
|
||||||
let flare = Flaresolverr::new(flare_url);
|
|
||||||
let result = flare
|
|
||||||
.solve(FlareSolverrRequest {
|
|
||||||
cmd: "request.get".to_string(),
|
|
||||||
url: url_str.clone(),
|
|
||||||
maxTimeout: 60000,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let video_items = match result {
|
|
||||||
Ok(res) => {
|
|
||||||
// println!("FlareSolverr response: {}", res);
|
|
||||||
self.get_video_items_from_html(res.solution.response, pool)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error solving FlareSolverr: {}", e);
|
|
||||||
return Err("Failed to solve FlareSolverr".into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !video_items.is_empty() {
|
if !video_items.is_empty() {
|
||||||
cache.remove(&url_str);
|
cache.remove(&url_str);
|
||||||
cache.insert(url_str.clone(), video_items.clone());
|
cache.insert(url_str.clone(), video_items.clone());
|
||||||
@@ -115,14 +82,17 @@ impl PerverzijaProvider {
|
|||||||
}
|
}
|
||||||
Ok(video_items)
|
Ok(video_items)
|
||||||
}
|
}
|
||||||
}
|
async fn query(
|
||||||
async fn query(&self, cache: VideoCache, pool:DbPool, page: u8, query: &str) -> Result<Vec<VideoItem>> {
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
let mut query_parse = true;
|
let mut query_parse = true;
|
||||||
let search_string = query.replace(" ", "+");
|
let search_string = query.replace(" ", "+");
|
||||||
let mut url_str = format!(
|
let mut url_str = format!("{}page/{}/?s={}", self.url, page, search_string);
|
||||||
"{}page/{}/?s={}",
|
|
||||||
self.url, page, search_string
|
|
||||||
);
|
|
||||||
if page == 1 {
|
if page == 1 {
|
||||||
url_str = format!("{}?s={}", self.url, search_string);
|
url_str = format!("{}?s={}", self.url, search_string);
|
||||||
}
|
}
|
||||||
@@ -142,62 +112,33 @@ impl PerverzijaProvider {
|
|||||||
Some((time, items)) => {
|
Some((time, items)) => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
let _ = cache.check().await;
|
let _ = cache.check().await;
|
||||||
return Ok(items.clone())
|
return Ok(items.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let client = Client::builder()
|
|
||||||
.emulation(Emulation::Firefox136)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let response = client.get(url_str.clone()).send().await?;
|
let mut requester = options.requester.clone().unwrap();
|
||||||
if response.status().is_success() {
|
let text = requester.get(&url_str, Some(Version::HTTP_2)).await.unwrap();
|
||||||
let text = response.text().await?;
|
let video_items: Vec<VideoItem> = match query_parse {
|
||||||
let video_items: Vec<VideoItem> = match query_parse{
|
true => {
|
||||||
true => {self.get_video_items_from_html_query(text.clone(), pool).await},
|
self.get_video_items_from_html_query(text.clone(), pool)
|
||||||
false => {self.get_video_items_from_html(text.clone(), pool)}
|
.await
|
||||||
|
}
|
||||||
|
false => self.get_video_items_from_html(text.clone(), pool),
|
||||||
};
|
};
|
||||||
if !video_items.is_empty() {
|
if !video_items.is_empty() {
|
||||||
cache.remove(&url_str);
|
cache.remove(&url_str);
|
||||||
cache.insert(url_str.clone(), video_items.clone());
|
cache.insert(url_str.clone(), video_items.clone());
|
||||||
} else{
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
|
||||||
} else {
|
} else {
|
||||||
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
|
||||||
let flare = Flaresolverr::new(flare_url);
|
|
||||||
let result = flare
|
|
||||||
.solve(FlareSolverrRequest {
|
|
||||||
cmd: "request.get".to_string(),
|
|
||||||
url: url_str.clone(),
|
|
||||||
maxTimeout: 60000,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let video_items = match result {
|
|
||||||
Ok(res) => {
|
|
||||||
self.get_video_items_from_html_query(res.solution.response, pool).await
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error solving FlareSolverr: {}", e);
|
|
||||||
return Err("Failed to solve FlareSolverr".into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !video_items.is_empty() {
|
|
||||||
cache.remove(&url_str);
|
|
||||||
cache.insert(url_str.clone(), video_items.clone());
|
|
||||||
} else{
|
|
||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
Ok(video_items)
|
Ok(video_items)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn get_video_items_from_html(&self, html: String, pool: DbPool) -> Vec<VideoItem> {
|
fn get_video_items_from_html(&self, html: String, pool: DbPool) -> Vec<VideoItem> {
|
||||||
if html.is_empty() {
|
if html.is_empty() {
|
||||||
@@ -230,8 +171,9 @@ impl PerverzijaProvider {
|
|||||||
let url_str = vid[1].split("iframe src="").collect::<Vec<&str>>()[1]
|
let url_str = vid[1].split("iframe src="").collect::<Vec<&str>>()[1]
|
||||||
.split(""")
|
.split(""")
|
||||||
.collect::<Vec<&str>>()[0]
|
.collect::<Vec<&str>>()[0]
|
||||||
.to_string().replace("index.php", "xs1.php");
|
.to_string()
|
||||||
if url_str.starts_with("https://streamtape.com/"){
|
.replace("index.php", "xs1.php");
|
||||||
|
if url_str.starts_with("https://streamtape.com/") {
|
||||||
continue; // Skip Streamtape links
|
continue; // Skip Streamtape links
|
||||||
}
|
}
|
||||||
let id = url_str.split("data=").collect::<Vec<&str>>()[1]
|
let id = url_str.split("data=").collect::<Vec<&str>>()[1]
|
||||||
@@ -247,16 +189,18 @@ impl PerverzijaProvider {
|
|||||||
};
|
};
|
||||||
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
if !vid[4].contains("srcset=") && vid[4].split("src=\"").collect::<Vec<&str>>().len() == 1{
|
if !vid[4].contains("srcset=")
|
||||||
for (index, line) in vid.iter().enumerate(){
|
&& vid[4].split("src=\"").collect::<Vec<&str>>().len() == 1
|
||||||
|
{
|
||||||
|
for (index, line) in vid.iter().enumerate() {
|
||||||
println!("Line {}: {}\n\n", index, line);
|
println!("Line {}: {}\n\n", index, line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut thumb = "".to_string();
|
let mut thumb = "".to_string();
|
||||||
for v in vid.clone(){
|
for v in vid.clone() {
|
||||||
let line = v.trim();
|
let line = v.trim();
|
||||||
if line.starts_with("<img "){
|
if line.starts_with("<img ") {
|
||||||
thumb = line.split(" src=\"").collect::<Vec<&str>>()[1]
|
thumb = line.split(" src=\"").collect::<Vec<&str>>()[1]
|
||||||
.split("\"")
|
.split("\"")
|
||||||
.collect::<Vec<&str>>()[0]
|
.collect::<Vec<&str>>()[0]
|
||||||
@@ -282,7 +226,7 @@ impl PerverzijaProvider {
|
|||||||
|
|
||||||
let studios_parts = vid[7].split("a href=\"").collect::<Vec<&str>>();
|
let studios_parts = vid[7].split("a href=\"").collect::<Vec<&str>>();
|
||||||
for studio in studios_parts.iter().skip(1) {
|
for studio in studios_parts.iter().skip(1) {
|
||||||
if studio.starts_with("https://tube.perverzija.com/studio/"){
|
if studio.starts_with("https://tube.perverzija.com/studio/") {
|
||||||
tags.push(
|
tags.push(
|
||||||
studio.split("/\"").collect::<Vec<&str>>()[0]
|
studio.split("/\"").collect::<Vec<&str>>()[0]
|
||||||
.replace("https://tube.perverzija.com/studio/", "@studio:")
|
.replace("https://tube.perverzija.com/studio/", "@studio:")
|
||||||
@@ -291,9 +235,11 @@ impl PerverzijaProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for tag in vid[0].split(" ").collect::<Vec<&str>>(){
|
for tag in vid[0].split(" ").collect::<Vec<&str>>() {
|
||||||
if tag.starts_with("stars-") {
|
if tag.starts_with("stars-") {
|
||||||
let tag_name = tag.split("stars-").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0]
|
let tag_name = tag.split("stars-").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
.to_string();
|
.to_string();
|
||||||
if !tag_name.is_empty() {
|
if !tag_name.is_empty() {
|
||||||
tags.push(format!("@stars:{}", tag_name));
|
tags.push(format!("@stars:{}", tag_name));
|
||||||
@@ -301,10 +247,9 @@ impl PerverzijaProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for tag in vid[0].split(" ").collect::<Vec<&str>>(){
|
for tag in vid[0].split(" ").collect::<Vec<&str>>() {
|
||||||
if tag.starts_with("tag-") {
|
if tag.starts_with("tag-") {
|
||||||
let tag_name = tag.split("tag-").collect::<Vec<&str>>()[1]
|
let tag_name = tag.split("tag-").collect::<Vec<&str>>()[1].to_string();
|
||||||
.to_string();
|
|
||||||
if !tag_name.is_empty() {
|
if !tag_name.is_empty() {
|
||||||
tags.push(tag_name.replace("-", " ").to_string());
|
tags.push(tag_name.replace("-", " ").to_string());
|
||||||
}
|
}
|
||||||
@@ -317,7 +262,8 @@ impl PerverzijaProvider {
|
|||||||
"perverzija".to_string(),
|
"perverzija".to_string(),
|
||||||
thumb,
|
thumb,
|
||||||
duration,
|
duration,
|
||||||
).tags(tags);
|
)
|
||||||
|
.tags(tags);
|
||||||
// .embed(embed.clone());
|
// .embed(embed.clone());
|
||||||
let mut format =
|
let mut format =
|
||||||
videos::VideoFormat::new(url_str.clone(), "1080".to_string(), "m3u8".to_string());
|
videos::VideoFormat::new(url_str.clone(), "1080".to_string(), "m3u8".to_string());
|
||||||
@@ -333,17 +279,13 @@ impl PerverzijaProvider {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_video_items_from_html_query(&self, html: String, pool:DbPool) -> Vec<VideoItem> {
|
async fn get_video_items_from_html_query(&self, html: String, pool: DbPool) -> Vec<VideoItem> {
|
||||||
let raw_videos = html
|
let raw_videos = html.split("video-item post").collect::<Vec<&str>>()[1..].to_vec();
|
||||||
.split("video-item post")
|
let futures = raw_videos
|
||||||
.collect::<Vec<&str>>()[1..]
|
|
||||||
.to_vec();
|
|
||||||
let futures = raw_videos.into_iter().map(|el| self.get_video_item(el, pool.clone()));
|
|
||||||
let results: Vec<Result<VideoItem>> = join_all(futures).await;
|
|
||||||
let items: Vec<VideoItem> = results
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(Result::ok)
|
.map(|el| self.get_video_item(el, pool.clone()));
|
||||||
.collect();
|
let results: Vec<Result<VideoItem>> = join_all(futures).await;
|
||||||
|
let items: Vec<VideoItem> = results.into_iter().filter_map(Result::ok).collect();
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -360,9 +302,9 @@ impl PerverzijaProvider {
|
|||||||
.to_string();
|
.to_string();
|
||||||
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
|
||||||
let thumb = match vid[6].split(" src=\"").collect::<Vec<&str>>().len(){
|
let thumb = match vid[6].split(" src=\"").collect::<Vec<&str>>().len() {
|
||||||
1=>{
|
1 => {
|
||||||
for (index,line) in vid.iter().enumerate() {
|
for (index, line) in vid.iter().enumerate() {
|
||||||
println!("Line {}: {}", index, line.to_string().trim());
|
println!("Line {}: {}", index, line.to_string().trim());
|
||||||
}
|
}
|
||||||
return Err("Failed to parse thumbnail URL".into());
|
return Err("Failed to parse thumbnail URL".into());
|
||||||
@@ -381,19 +323,19 @@ impl PerverzijaProvider {
|
|||||||
let referer_url = "https://xtremestream.xyz/".to_string();
|
let referer_url = "https://xtremestream.xyz/".to_string();
|
||||||
|
|
||||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||||
let db_result = db::get_video(&mut conn,lookup_url.clone());
|
let db_result = db::get_video(&mut conn, lookup_url.clone());
|
||||||
match db_result {
|
match db_result {
|
||||||
Ok(Some(entry)) => {
|
Ok(Some(entry)) => {
|
||||||
if entry.starts_with("{"){ // replace old urls with new json objects
|
if entry.starts_with("{") {
|
||||||
|
// replace old urls with new json objects
|
||||||
let entry = serde_json::from_str::<PerverzijaDbEntry>(entry.as_str())?;
|
let entry = serde_json::from_str::<PerverzijaDbEntry>(entry.as_str())?;
|
||||||
let url_str = entry.url_string;
|
let url_str = entry.url_string;
|
||||||
let tags = entry.tags_strings;
|
let tags = entry.tags_strings;
|
||||||
if url_str.starts_with("!"){
|
if url_str.starts_with("!") {
|
||||||
return Err("Video was removed".into());
|
return Err("Video was removed".into());
|
||||||
}
|
}
|
||||||
let mut id = url_str.split("data=").collect::<Vec<&str>>()[1]
|
let mut id = url_str.split("data=").collect::<Vec<&str>>()[1].to_string();
|
||||||
.to_string();
|
if id.contains("&") {
|
||||||
if id.contains("&"){
|
|
||||||
id = id.split("&").collect::<Vec<&str>>()[0].to_string()
|
id = id.split("&").collect::<Vec<&str>>()[0].to_string()
|
||||||
}
|
}
|
||||||
let mut video_item = VideoItem::new(
|
let mut video_item = VideoItem::new(
|
||||||
@@ -404,24 +346,24 @@ impl PerverzijaProvider {
|
|||||||
thumb,
|
thumb,
|
||||||
duration,
|
duration,
|
||||||
)
|
)
|
||||||
.tags(tags)
|
.tags(tags);
|
||||||
;
|
let mut format = videos::VideoFormat::new(
|
||||||
let mut format =
|
url_str.clone(),
|
||||||
videos::VideoFormat::new(url_str.clone(), "1080".to_string(), "m3u8".to_string());
|
"1080".to_string(),
|
||||||
|
"m3u8".to_string(),
|
||||||
|
);
|
||||||
format.add_http_header("Referer".to_string(), referer_url.clone());
|
format.add_http_header("Referer".to_string(), referer_url.clone());
|
||||||
if let Some(formats) = video_item.formats.as_mut() {
|
if let Some(formats) = video_item.formats.as_mut() {
|
||||||
formats.push(format);
|
formats.push(format);
|
||||||
} else {
|
} else {
|
||||||
video_item.formats = Some(vec![format]);
|
video_item.formats = Some(vec![format]);
|
||||||
}
|
}
|
||||||
return Ok(video_item)
|
return Ok(video_item);
|
||||||
}
|
} else {
|
||||||
else{
|
let _ = db::delete_video(&mut conn, lookup_url.clone());
|
||||||
let _ = db::delete_video(&mut conn,lookup_url.clone());
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {}
|
||||||
},
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error fetching video from database: {}", e);
|
println!("Error fetching video from database: {}", e);
|
||||||
// return Err(format!("Error fetching video from database: {}", e).into());
|
// return Err(format!("Error fetching video from database: {}", e).into());
|
||||||
@@ -429,13 +371,10 @@ impl PerverzijaProvider {
|
|||||||
}
|
}
|
||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
||||||
|
let client = Client::builder().emulation(Emulation::Firefox136).build()?;
|
||||||
let client = Client::builder()
|
|
||||||
.emulation(Emulation::Firefox136)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let response = client.get(lookup_url.clone()).send().await?;
|
let response = client.get(lookup_url.clone()).send().await?;
|
||||||
let text = match response.status().is_success(){
|
let text = match response.status().is_success() {
|
||||||
true => response.text().await?,
|
true => response.text().await?,
|
||||||
false => {
|
false => {
|
||||||
println!("Failed to fetch video details");
|
println!("Failed to fetch video details");
|
||||||
@@ -443,20 +382,26 @@ impl PerverzijaProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
let mut url_str = text.split("<iframe src=\"").collect::<Vec<&str>>()[1]
|
let mut url_str = text.split("<iframe src=\"").collect::<Vec<&str>>()[1]
|
||||||
.split("\"")
|
.split("\"")
|
||||||
.collect::<Vec<&str>>()[0]
|
.collect::<Vec<&str>>()[0]
|
||||||
.to_string().replace("index.php","xs1.php");
|
.to_string()
|
||||||
if !url_str.contains("xtremestream.xyz"){
|
.replace("index.php", "xs1.php");
|
||||||
|
if !url_str.contains("xtremestream.xyz") {
|
||||||
url_str = "!".to_string()
|
url_str = "!".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tags: Vec<String> = Vec::new(); // Placeholder for tags, adjust as needed
|
let mut tags: Vec<String> = Vec::new(); // Placeholder for tags, adjust as needed
|
||||||
|
|
||||||
let studios_parts = text.split("<strong>Studio: </strong>").collect::<Vec<&str>>()[1].split("</div>").collect::<Vec<&str>>()[0].split("<a href=\"").collect::<Vec<&str>>();
|
let studios_parts = text
|
||||||
|
.split("<strong>Studio: </strong>")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("</div>")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.split("<a href=\"")
|
||||||
|
.collect::<Vec<&str>>();
|
||||||
for studio in studios_parts.iter().skip(1) {
|
for studio in studios_parts.iter().skip(1) {
|
||||||
if studio.starts_with("https://tube.perverzija.com/studio/"){
|
if studio.starts_with("https://tube.perverzija.com/studio/") {
|
||||||
tags.push(
|
tags.push(
|
||||||
studio.split("/\"").collect::<Vec<&str>>()[0]
|
studio.split("/\"").collect::<Vec<&str>>()[0]
|
||||||
.replace("https://tube.perverzija.com/studio/", "@studio:")
|
.replace("https://tube.perverzija.com/studio/", "@studio:")
|
||||||
@@ -464,10 +409,16 @@ impl PerverzijaProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if text.contains("<strong>Stars: </strong>"){
|
if text.contains("<strong>Stars: </strong>") {
|
||||||
let stars_parts: Vec<&str> = text.split("<strong>Stars: </strong>").collect::<Vec<&str>>()[1].split("</div>").collect::<Vec<&str>>()[0].split("<a href=\"").collect::<Vec<&str>>();
|
let stars_parts: Vec<&str> = text
|
||||||
|
.split("<strong>Stars: </strong>")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("</div>")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.split("<a href=\"")
|
||||||
|
.collect::<Vec<&str>>();
|
||||||
for star in stars_parts.iter().skip(1) {
|
for star in stars_parts.iter().skip(1) {
|
||||||
if star.starts_with("https://tube.perverzija.com/stars/"){
|
if star.starts_with("https://tube.perverzija.com/stars/") {
|
||||||
tags.push(
|
tags.push(
|
||||||
star.split("/\"").collect::<Vec<&str>>()[0]
|
star.split("/\"").collect::<Vec<&str>>()[0]
|
||||||
.replace("https://tube.perverzija.com/stars/", "@stars:")
|
.replace("https://tube.perverzija.com/stars/", "@stars:")
|
||||||
@@ -477,9 +428,13 @@ impl PerverzijaProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tags_parts: Vec<&str> = text.split("<strong>Tags: </strong>").collect::<Vec<&str>>()[1].split("</div>").collect::<Vec<&str>>()[0].split("<a href=\"").collect::<Vec<&str>>();
|
let tags_parts: Vec<&str> = text.split("<strong>Tags: </strong>").collect::<Vec<&str>>()[1]
|
||||||
|
.split("</div>")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.split("<a href=\"")
|
||||||
|
.collect::<Vec<&str>>();
|
||||||
for star in tags_parts.iter().skip(1) {
|
for star in tags_parts.iter().skip(1) {
|
||||||
if star.starts_with("https://tube.perverzija.com/stars/"){
|
if star.starts_with("https://tube.perverzija.com/stars/") {
|
||||||
tags.push(
|
tags.push(
|
||||||
star.split("/\"").collect::<Vec<&str>>()[0]
|
star.split("/\"").collect::<Vec<&str>>()[0]
|
||||||
.replace("https://tube.perverzija.com/stars/", "@stars:")
|
.replace("https://tube.perverzija.com/stars/", "@stars:")
|
||||||
@@ -493,18 +448,23 @@ impl PerverzijaProvider {
|
|||||||
tags_strings: tags.clone(),
|
tags_strings: tags.clone(),
|
||||||
};
|
};
|
||||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||||
let insert_result = db::insert_video(&mut conn, &lookup_url, &serde_json::to_string(&perverzija_db_entry)?);
|
let insert_result = db::insert_video(
|
||||||
match insert_result{
|
&mut conn,
|
||||||
|
&lookup_url,
|
||||||
|
&serde_json::to_string(&perverzija_db_entry)?,
|
||||||
|
);
|
||||||
|
match insert_result {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {println!("{:?}", e); }
|
Err(e) => {
|
||||||
|
println!("{:?}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
drop(conn);
|
drop(conn);
|
||||||
if !url_str.contains("xtremestream.xyz"){
|
if !url_str.contains("xtremestream.xyz") {
|
||||||
return Err("Video URL does not contain xtremestream.xyz".into());
|
return Err("Video URL does not contain xtremestream.xyz".into());
|
||||||
}
|
}
|
||||||
let mut id = url_str.split("data=").collect::<Vec<&str>>()[1]
|
let mut id = url_str.split("data=").collect::<Vec<&str>>()[1].to_string();
|
||||||
.to_string();
|
if id.contains("&") {
|
||||||
if id.contains("&"){
|
|
||||||
id = id.split("&").collect::<Vec<&str>>()[0].to_string()
|
id = id.split("&").collect::<Vec<&str>>()[0].to_string()
|
||||||
}
|
}
|
||||||
// if !vid[6].contains(" src=\""){
|
// if !vid[6].contains(" src=\""){
|
||||||
@@ -516,9 +476,6 @@ impl PerverzijaProvider {
|
|||||||
// println!("Line {}: {}", index, line.to_string().trim());
|
// println!("Line {}: {}", index, line.to_string().trim());
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let mut video_item = VideoItem::new(
|
let mut video_item = VideoItem::new(
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
@@ -541,6 +498,7 @@ impl PerverzijaProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Provider for PerverzijaProvider {
|
impl Provider for PerverzijaProvider {
|
||||||
async fn get_videos(
|
async fn get_videos(
|
||||||
&self,
|
&self,
|
||||||
@@ -555,8 +513,14 @@ impl Provider for PerverzijaProvider {
|
|||||||
let _ = per_page;
|
let _ = per_page;
|
||||||
let _ = sort;
|
let _ = sort;
|
||||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
Some(q) => self.query(cache, pool, page.parse::<u8>().unwrap_or(1), &q).await,
|
Some(q) => {
|
||||||
None => self.get(cache, pool, page.parse::<u8>().unwrap_or(1), options.featured.unwrap()).await,
|
self.query(cache, pool, page.parse::<u8>().unwrap_or(1), &q, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, pool, page.parse::<u8>().unwrap_or(1), options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
};
|
};
|
||||||
match videos {
|
match videos {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
|
|||||||
537
src/providers/pimpbunny.rs
Normal file
537
src/providers/pimpbunny.rs
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::status::*;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::discord::{format_error_chain, send_discord_error_report};
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use htmlentity::entity::{decode, ICodedDataTrait};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::{thread, vec};
|
||||||
|
use titlecase::Titlecase;
|
||||||
|
use wreq::Version;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
Json(serde_json::Error);
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
Parse(msg: String) {
|
||||||
|
description("parse error")
|
||||||
|
display("parse error: {}", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PimpbunnyProvider {
|
||||||
|
url: String,
|
||||||
|
stars: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
categories: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PimpbunnyProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let provider = Self {
|
||||||
|
url: "https://pimpbunny.com".to_string(),
|
||||||
|
stars: Arc::new(RwLock::new(vec![])),
|
||||||
|
categories: Arc::new(RwLock::new(vec![])),
|
||||||
|
};
|
||||||
|
provider.spawn_initial_load();
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
|
||||||
|
let _ = clientversion;
|
||||||
|
Channel {
|
||||||
|
id: "pimpbunny".to_string(),
|
||||||
|
name: "Pimpbunny".to_string(),
|
||||||
|
description: "Watch Porn!".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=pimpbunny.com".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories: self
|
||||||
|
.categories
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.title.clone())
|
||||||
|
.collect(),
|
||||||
|
options: vec![ChannelOption {
|
||||||
|
id: "sort".to_string(),
|
||||||
|
title: "Sort".to_string(),
|
||||||
|
description: "Sort the Videos".to_string(),
|
||||||
|
systemImage: "list.number".to_string(),
|
||||||
|
colorName: "blue".to_string(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "featured".into(),
|
||||||
|
title: "Featured".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "most recent".into(),
|
||||||
|
title: "Most Recent".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "most viewed".into(),
|
||||||
|
title: "Most Viewed".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "best rated".into(),
|
||||||
|
title: "Best Rated".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
}],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_initial_load(&self) {
|
||||||
|
let url = self.url.clone();
|
||||||
|
let stars = Arc::clone(&self.stars);
|
||||||
|
let categories = Arc::clone(&self.categories);
|
||||||
|
|
||||||
|
thread::spawn(async move || {
|
||||||
|
let rt = match tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(rt) => rt,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("tokio runtime failed: {e}");
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(format_error_chain(&e)),
|
||||||
|
Some("Pimpbunny Provider"),
|
||||||
|
Some("Failed to create tokio runtime"),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rt.block_on(async {
|
||||||
|
if let Err(e) = Self::load_stars(&url, Arc::clone(&stars)).await {
|
||||||
|
eprintln!("load_stars failed: {e}");
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(format_error_chain(&e)),
|
||||||
|
Some("Pimpbunny Provider"),
|
||||||
|
Some("Failed to load stars during initial load"),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
if let Err(e) = Self::load_categories(&url, Arc::clone(&categories)).await {
|
||||||
|
eprintln!("load_categories failed: {e}");
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(format_error_chain(&e)),
|
||||||
|
Some("Pimpbunny Provider"),
|
||||||
|
Some("Failed to load categories during initial load"),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
|
||||||
|
if let Ok(mut vec) = target.write() {
|
||||||
|
if !vec.iter().any(|x| x.id == item.id) {
|
||||||
|
vec.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_stars(base: &str, stars: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
let mut requester = Requester::new();
|
||||||
|
let text = requester
|
||||||
|
.get(
|
||||||
|
&format!("{base}/onlyfans-models/?models_per_page=20"),
|
||||||
|
Some(Version::HTTP_2),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("{}", e)))?;
|
||||||
|
|
||||||
|
let block = text
|
||||||
|
.split("vt_list_models_with_advertising_custom_models_list_items")
|
||||||
|
.last()
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("missing stars block".into()))?
|
||||||
|
.split("pb-page-description")
|
||||||
|
.next()
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
for el in block.split("<div class=\"col\">").skip(1) {
|
||||||
|
if el.contains("pb-promoted-link") || !el.contains("href=\"https://pimpbunny.com/onlyfans-models/") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = el
|
||||||
|
.split("href=\"https://pimpbunny.com/onlyfans-models/")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("/\"").next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("star id: {el}").into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let title = el
|
||||||
|
.split("ui-card-title")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("star title: {el}").into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Self::push_unique(&stars, FilterOption { id, title });
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_categories(base: &str, cats: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
let mut requester = Requester::new();
|
||||||
|
let text = requester
|
||||||
|
.get(
|
||||||
|
&format!("{base}/categories/?items_per_page=120"),
|
||||||
|
Some(Version::HTTP_2),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("{}", e)))?;
|
||||||
|
|
||||||
|
let block = text
|
||||||
|
.split("list_categories_categories_list_items")
|
||||||
|
.last()
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("categories block".into()))?
|
||||||
|
.split("pb-pagination-wrapper")
|
||||||
|
.next()
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
for el in block.split("<div class=\"col\">").skip(1) {
|
||||||
|
let id = el
|
||||||
|
.split("href=\"https://pimpbunny.com/categories/")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("/\"").next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("category id: {el}").into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let title = el
|
||||||
|
.split("ui-heading-h3")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse(format!("category title: {el}").into()))?
|
||||||
|
.titlecase();
|
||||||
|
|
||||||
|
Self::push_unique(&cats, FilterOption { id, title });
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"best rated" => "&sort_by=rating",
|
||||||
|
"most viewed" => "&sort_by=video_viewed",
|
||||||
|
_ => "&sort_by=post_date",
|
||||||
|
};
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/videos/{}/?videos_per_page=20{}",
|
||||||
|
self.url, page, sort_string
|
||||||
|
);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, Some(Version::HTTP_11)).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self
|
||||||
|
.get_video_items_from_html(text.clone(), &mut requester)
|
||||||
|
.await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.trim().to_string();
|
||||||
|
|
||||||
|
let mut video_url = format!(
|
||||||
|
"{}/search/{}/?mode=async&function=get_block&block_id=list_videos_videos_list_search_result&videos_per_page=20&from_videos={}",
|
||||||
|
self.url, search_string.replace(" ","-"), page
|
||||||
|
);
|
||||||
|
|
||||||
|
let sort_string = match options.sort.as_deref().unwrap_or("") {
|
||||||
|
"best rated" => "&sort_by=rating",
|
||||||
|
"most viewed" => "&sort_by=video_viewed",
|
||||||
|
_ => "&sort_by=post_date",
|
||||||
|
};
|
||||||
|
if let Some(star) = self
|
||||||
|
.stars
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.title.to_ascii_lowercase() == search_string.to_ascii_lowercase())
|
||||||
|
{
|
||||||
|
video_url = format!(
|
||||||
|
"{}/onlyfans-models/{}/{}/?videos_per_page=20{}",
|
||||||
|
self.url, star.id, page, sort_string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(cat) = self
|
||||||
|
.categories
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.title.to_ascii_lowercase() == search_string.to_ascii_lowercase())
|
||||||
|
{
|
||||||
|
video_url = format!(
|
||||||
|
"{}/categories/{}/{}/?videos_per_page=20{}",
|
||||||
|
self.url, cat.id, page, sort_string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, Some(Version::HTTP_2)).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self
|
||||||
|
.get_video_items_from_html(text.clone(), &mut requester)
|
||||||
|
.await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_items_from_html(
|
||||||
|
&self,
|
||||||
|
html: String,
|
||||||
|
requester: &mut Requester,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() || html.contains("404 Not Found") {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = match html
|
||||||
|
.split("-pagination-wrapper")
|
||||||
|
.next()
|
||||||
|
.and_then(|s| s.split("video_list").nth(2))
|
||||||
|
{
|
||||||
|
Some(b) => b,
|
||||||
|
None => return vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let futures = block
|
||||||
|
.split("<div class=\"col\">")
|
||||||
|
.skip(1)
|
||||||
|
.map(|el| self.get_video_item(el.to_string(), requester.clone()));
|
||||||
|
|
||||||
|
join_all(futures)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_item(
|
||||||
|
&self,
|
||||||
|
seg: String,
|
||||||
|
mut requester: Requester,
|
||||||
|
) -> Result<VideoItem> {
|
||||||
|
let video_url = seg
|
||||||
|
.split(" href=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("video url".into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut title = seg
|
||||||
|
.split("card-title")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('>').nth(1))
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("video title".into()))?
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title).titlecase();
|
||||||
|
|
||||||
|
let id = video_url
|
||||||
|
.split('/')
|
||||||
|
.nth(4)
|
||||||
|
.and_then(|s| s.split('.').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("video id".into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let thumb_block = seg
|
||||||
|
.split("card-thumbnail")
|
||||||
|
.nth(1)
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("thumb block".into()))?;
|
||||||
|
|
||||||
|
let mut thumb = thumb_block
|
||||||
|
.split("src=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if thumb.starts_with("data:image") {
|
||||||
|
thumb = thumb_block
|
||||||
|
.split("data-webp=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let preview = thumb_block
|
||||||
|
.split("data-preview=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let (tags, formats, views, duration) =
|
||||||
|
self.extract_media(&video_url, &mut requester).await?;
|
||||||
|
|
||||||
|
Ok(VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url,
|
||||||
|
"pimpbunny".into(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.formats(formats)
|
||||||
|
.tags(tags)
|
||||||
|
.preview(preview)
|
||||||
|
.views(views))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_media(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
requester: &mut Requester,
|
||||||
|
) -> Result<(Vec<String>, Vec<VideoFormat>, u32, u32)> {
|
||||||
|
let text = requester
|
||||||
|
.get(url, Some(Version::HTTP_2))
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::from(format!("{}", e)))?;
|
||||||
|
|
||||||
|
let json_str = text
|
||||||
|
.split("application/ld+json\">")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("</script>").next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("ld+json".into()))?;
|
||||||
|
|
||||||
|
let json: serde_json::Value = serde_json::from_str(json_str)?;
|
||||||
|
|
||||||
|
let video_url = json["contentUrl"].as_str().unwrap_or("").to_string();
|
||||||
|
let quality = video_url
|
||||||
|
.split('_')
|
||||||
|
.last()
|
||||||
|
.and_then(|s| s.split('.').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let views = json["interactionStatistic"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|a| a.first())
|
||||||
|
.and_then(|v| v["userInteractionCount"].as_str())
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let duration = json["duration"]
|
||||||
|
.as_str()
|
||||||
|
.map(|d| parse_time_to_seconds(&d.replace(['P','T','H','M','S'], "")).unwrap_or(0))
|
||||||
|
.unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
vec![],
|
||||||
|
vec![VideoFormat::new(video_url, quality, "video/mp4".into())],
|
||||||
|
views,
|
||||||
|
duration,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for PimpbunnyProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
_pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
_per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let page = page.parse::<u8>().unwrap_or(1);
|
||||||
|
|
||||||
|
let res = match query {
|
||||||
|
Some(q) => self.to_owned().query(cache, page, &q, options).await,
|
||||||
|
None => self.get(cache, page, &sort, options).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.unwrap_or_else(|e| {
|
||||||
|
eprintln!("pimpbunny error: {e}");
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_channel(&self, v: ClientVersion) -> Option<Channel> {
|
||||||
|
Some(self.build_channel(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,17 @@
|
|||||||
use crate::DbPool;
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
use crate::providers::Provider;
|
use crate::providers::Provider;
|
||||||
use crate::schema::videos;
|
use crate::status::*;
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
use crate::util::discord::send_discord_error_report;
|
||||||
use crate::util::parse_abbreviated_number;
|
|
||||||
use crate::util::time::parse_time_to_seconds;
|
use crate::util::time::parse_time_to_seconds;
|
||||||
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
use cute::c;
|
use async_trait::async_trait;
|
||||||
use error_chain::error_chain;
|
use error_chain::error_chain;
|
||||||
use htmlentity::entity::{ICodedDataTrait, decode};
|
use htmlentity::entity::{decode, ICodedDataTrait};
|
||||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::env;
|
|
||||||
use std::vec;
|
use std::vec;
|
||||||
use wreq::{Client, Proxy};
|
use std::fmt::Write;
|
||||||
use wreq_util::Emulation;
|
|
||||||
|
|
||||||
#[macro_use(c)]
|
|
||||||
|
|
||||||
error_chain! {
|
error_chain! {
|
||||||
foreign_links {
|
foreign_links {
|
||||||
@@ -24,422 +20,283 @@ error_chain! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
struct PmvhavenRequest {
|
|
||||||
all: bool, //true,
|
|
||||||
pmv: bool, //false,
|
|
||||||
hmv: bool, //false,
|
|
||||||
hypno: bool, //false,
|
|
||||||
tiktok: bool, //false,
|
|
||||||
koreanbj: bool, //false,
|
|
||||||
other: bool, // false,
|
|
||||||
explicitContent: Option<bool>, //null,
|
|
||||||
sameSexContent: Option<bool>, //null,
|
|
||||||
seizureWarning: Option<bool>, //null,
|
|
||||||
tags: Vec<String>, //[],
|
|
||||||
music: Vec<String>, //[],
|
|
||||||
stars: Vec<String>, //[],
|
|
||||||
creators: Vec<String>, //[],
|
|
||||||
range: Vec<u32>, //[0,40],
|
|
||||||
activeTime: String, //"All time",
|
|
||||||
activeQuality: String, //"Quality",
|
|
||||||
aspectRatio: String, //"Aspect Ratio",
|
|
||||||
activeView: String, //"Newest",
|
|
||||||
index: u32, //2,
|
|
||||||
hideUntagged: bool, //true,
|
|
||||||
showSubscriptionsOnly: bool, //false,
|
|
||||||
query: String, //"no",
|
|
||||||
profile: Option<String>, //null
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PmvhavenRequest {
|
|
||||||
pub fn new(page: u32) -> Self {
|
|
||||||
PmvhavenRequest {
|
|
||||||
all: true,
|
|
||||||
pmv: false,
|
|
||||||
hmv: false,
|
|
||||||
hypno: false,
|
|
||||||
tiktok: false,
|
|
||||||
koreanbj: false,
|
|
||||||
other: false,
|
|
||||||
explicitContent: None,
|
|
||||||
sameSexContent: None,
|
|
||||||
seizureWarning: None,
|
|
||||||
tags: vec![],
|
|
||||||
music: vec![],
|
|
||||||
stars: vec![],
|
|
||||||
creators: vec![],
|
|
||||||
range: vec![0, 40],
|
|
||||||
activeTime: "All time".to_string(),
|
|
||||||
activeQuality: "Quality".to_string(),
|
|
||||||
aspectRatio: "Aspect Ratio".to_string(),
|
|
||||||
activeView: "Newest".to_string(),
|
|
||||||
index: page,
|
|
||||||
hideUntagged: true,
|
|
||||||
showSubscriptionsOnly: false,
|
|
||||||
query: "no".to_string(),
|
|
||||||
profile: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn hypno(&mut self) -> &mut Self {
|
|
||||||
self.all = false;
|
|
||||||
self.pmv = false;
|
|
||||||
self.hmv = false;
|
|
||||||
self.tiktok = false;
|
|
||||||
self.koreanbj = false;
|
|
||||||
self.other = false;
|
|
||||||
self.hypno = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn pmv(&mut self) -> &mut Self {
|
|
||||||
self.all = false;
|
|
||||||
self.pmv = true;
|
|
||||||
self.hmv = false;
|
|
||||||
self.tiktok = false;
|
|
||||||
self.koreanbj = false;
|
|
||||||
self.other = false;
|
|
||||||
self.hypno = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn hmv(&mut self) -> &mut Self {
|
|
||||||
self.all = false;
|
|
||||||
self.pmv = false;
|
|
||||||
self.hmv = true;
|
|
||||||
self.tiktok = false;
|
|
||||||
self.koreanbj = false;
|
|
||||||
self.other = false;
|
|
||||||
self.hypno = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn tiktok(&mut self) -> &mut Self {
|
|
||||||
self.all = false;
|
|
||||||
self.pmv = false;
|
|
||||||
self.hmv = false;
|
|
||||||
self.tiktok = true;
|
|
||||||
self.koreanbj = false;
|
|
||||||
self.other = false;
|
|
||||||
self.hypno = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn koreanbj(&mut self) -> &mut Self {
|
|
||||||
self.all = false;
|
|
||||||
self.pmv = false;
|
|
||||||
self.hmv = false;
|
|
||||||
self.tiktok = false;
|
|
||||||
self.koreanbj = true;
|
|
||||||
self.other = false;
|
|
||||||
self.hypno = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn other(&mut self) -> &mut Self {
|
|
||||||
self.all = false;
|
|
||||||
self.pmv = false;
|
|
||||||
self.hmv = false;
|
|
||||||
self.tiktok = false;
|
|
||||||
self.koreanbj = false;
|
|
||||||
self.other = true;
|
|
||||||
self.hypno = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
struct PmvhavenSearch {
|
|
||||||
mode: String, //"DefaultMoreSearch",
|
|
||||||
data: String, //"pmv",
|
|
||||||
index: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PmvhavenSearch {
|
|
||||||
fn new(search: String, page: u32) -> PmvhavenSearch {
|
|
||||||
PmvhavenSearch {
|
|
||||||
mode: "DefaultMoreSearch".to_string(),
|
|
||||||
data: search,
|
|
||||||
index: page,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct PmvhavenVideo {
|
|
||||||
title: String, //JAV Addiction Therapy",
|
|
||||||
uploader: Option<String>, //itonlygetsworse",
|
|
||||||
duration: f32, //259.093333,
|
|
||||||
width: Option<String>, //3840",
|
|
||||||
height: Option<String>, //2160",
|
|
||||||
ratio: Option<u32>, //50,
|
|
||||||
thumbnails: Vec<Option<String>>, //[
|
|
||||||
// "placeholder",
|
|
||||||
// "https://storage.pmvhaven.com/686f24e96f7124f3dfbe90ab/thumbnail/JAV Addiction Therapy_686f24e96f7124f3dfbe90ab.png",
|
|
||||||
// "https://storage.pmvhaven.com/686f24e96f7124f3dfbe90ab/thumbnail/webp320_686f24e96f7124f3dfbe90ab.webp"
|
|
||||||
// ],
|
|
||||||
views: u32, //1971,
|
|
||||||
url: Option<String>, //https://storage.pmvhaven.com/686f24e96f7124f3dfbe90ab/JAV Addiction Therapy_686f24e96f7124f3dfbe90ab.mp4",
|
|
||||||
previewUrlCompressed: Option<String>, //https://storage.pmvhaven.com/686f24e96f7124f3dfbe90ab/videoPreview/comus_686f24e96f7124f3dfbe90ab.mp4",
|
|
||||||
seizureWarning: Option<bool>, //false,
|
|
||||||
isoDate: Option<String>, //2025-07-10T02:52:26.000Z",
|
|
||||||
gayContent: Option<bool>, //false,
|
|
||||||
transContent: Option<bool>, //false,
|
|
||||||
creator: Option<String>, //itonlygetsworse",
|
|
||||||
_id: String, //686f2aeade2062f93d72931f",
|
|
||||||
totalRaters: Option<u32>, //42,
|
|
||||||
rating: Option<u32>, //164
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PmvhavenVideo {
|
|
||||||
fn to_videoitem(self) -> VideoItem {
|
|
||||||
let encoded_title = percent_encode_emojis(&self.title);
|
|
||||||
let thumbnail = self.thumbnails[self.thumbnails.len()-1].clone().unwrap_or("".to_string());
|
|
||||||
let video_id = thumbnail.split("_").collect::<Vec<&str>>().last().unwrap_or(&"").to_string().split('.').next().unwrap_or("").to_string();
|
|
||||||
let mut item = VideoItem::new(
|
|
||||||
self._id.clone(),
|
|
||||||
self.title.clone(),
|
|
||||||
format!("https://pmvhaven.com/video/{}_{}", self.title.replace(" ","-"), self._id),
|
|
||||||
"pmvhaven".to_string(),
|
|
||||||
thumbnail,
|
|
||||||
self.duration as u32,
|
|
||||||
)
|
|
||||||
|
|
||||||
.views(self.views);
|
|
||||||
item = match self.creator{
|
|
||||||
Some(c) => item.uploader(c),
|
|
||||||
_ => item,
|
|
||||||
};
|
|
||||||
item = match self.previewUrlCompressed{
|
|
||||||
Some(u) => item.preview(u),
|
|
||||||
_ => item,
|
|
||||||
};
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define a percent-encoding set that encodes all non-ASCII characters
|
|
||||||
const EMOJI_ENCODE_SET: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'%').add(b'<').add(b'>').add(b'?').add(b'[').add(b'\\').add(b']').add(b'^').add(b'`').add(b'{').add(b'|').add(b'}');
|
|
||||||
|
|
||||||
// Helper function to percent-encode emojis and other non-ASCII chars
|
|
||||||
fn percent_encode_emojis(s: &str) -> String {
|
|
||||||
utf8_percent_encode(s, EMOJI_ENCODE_SET).to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct PmvhavenResponse {
|
|
||||||
httpStatusCode: Option<u32>,
|
|
||||||
data: Vec<PmvhavenVideo>,
|
|
||||||
count: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PmvhavenResponse {
|
|
||||||
fn to_videoitems(self) -> Vec<VideoItem> {
|
|
||||||
return c![video.to_videoitem(), for video in self.data];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PmvhavenProvider {
|
pub struct PmvhavenProvider {
|
||||||
url: String,
|
url: String,
|
||||||
|
stars: Arc<RwLock<Vec<String>>>,
|
||||||
|
categories: Arc<RwLock<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PmvhavenProvider {
|
impl PmvhavenProvider {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
PmvhavenProvider {
|
Self {
|
||||||
url: "https://pmvhaven.com".to_string(),
|
url: "https://pmvhaven.com".to_string(),
|
||||||
|
stars: Arc::new(RwLock::new(vec![])),
|
||||||
|
categories: Arc::new(RwLock::new(vec![])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn get(&self, cache: VideoCache, page: u8, category: String, sort:String) -> Result<Vec<VideoItem>> {
|
|
||||||
let index = format!("pmvhaven:{}:{}", page, category);
|
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
|
||||||
let url = format!("{}/api/getmorevideos", self.url);
|
let _ = clientversion;
|
||||||
let mut request = PmvhavenRequest::new(page as u32);
|
|
||||||
request.activeView = sort;
|
let categories = self
|
||||||
println!("Category: {}", category);
|
.categories
|
||||||
request = match category.as_str() {
|
.read()
|
||||||
"hypno" => { request.hypno(); request },
|
.map(|g| g.clone())
|
||||||
"pmv" => { request.pmv(); request },
|
.unwrap_or_default();
|
||||||
"hmv" => { request.hmv(); request },
|
|
||||||
"tiktok" => { request.tiktok(); request },
|
Channel {
|
||||||
"koreanbj" => { request.koreanbj(); request },
|
id: "pmvhaven".to_string(),
|
||||||
"other" => { request.other(); request },
|
name: "PMVHaven".to_string(),
|
||||||
_ => request,
|
description: "Best PMV Videos".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=pmvhaven.com".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories,
|
||||||
|
options: vec![
|
||||||
|
ChannelOption {
|
||||||
|
id: "sort".into(),
|
||||||
|
title: "Sort".into(),
|
||||||
|
description: "Sort the Videos".into(),
|
||||||
|
systemImage: "list.number".into(),
|
||||||
|
colorName: "blue".into(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "relevance".into(),
|
||||||
|
title: "Relevance".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "newest".into(),
|
||||||
|
title: "Newest".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "oldest".into(),
|
||||||
|
title: "Oldest".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "most viewed".into(),
|
||||||
|
title: "Most Viewed".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "most liked".into(),
|
||||||
|
title: "Most Liked".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "most disliked".into(),
|
||||||
|
title: "Most Disliked".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
ChannelOption {
|
||||||
|
id: "duration".into(),
|
||||||
|
title: "Duration".into(),
|
||||||
|
description: "Length of the Videos".into(),
|
||||||
|
systemImage: "timer".into(),
|
||||||
|
colorName: "green".into(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "any".into(),
|
||||||
|
title: "Any".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "<4 min".into(),
|
||||||
|
title: "<4 min".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "4-20 min".into(),
|
||||||
|
title: "4-20 min".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "20-60 min".into(),
|
||||||
|
title: "20-60 min".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: ">1 hour".into(),
|
||||||
|
title: ">1 hour".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_unique(target: &Arc<RwLock<Vec<String>>>, item: String) {
|
||||||
|
if let Ok(mut vec) = target.write() {
|
||||||
|
if !vec.iter().any(|x| x == &item) {
|
||||||
|
vec.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search = query.trim().to_string();
|
||||||
|
|
||||||
|
let sort = match options.sort.as_deref() {
|
||||||
|
Some("newest") => "&sort=-uploadDate",
|
||||||
|
Some("oldest") => "&sort=uploadDate",
|
||||||
|
Some("most viewed") => "&sort=-views",
|
||||||
|
Some("most liked") => "&sort=-likes",
|
||||||
|
Some("most disliked") => "&sort=-dislikes",
|
||||||
|
_ => "",
|
||||||
};
|
};
|
||||||
|
|
||||||
let old_items = match cache.get(&index) {
|
let duration = match options.duration.as_deref() {
|
||||||
Some((time, items)) => {
|
Some("<4 min") => "&durationMax=240",
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
Some("4-20 min") => "&durationMin=240&durationMax=1200",
|
||||||
println!("Cache hit for URL: {}", url);
|
Some("20-60 min") => "&durationMin=1200&durationMax=3600",
|
||||||
return Ok(items.clone());
|
Some(">1 hour") => "&durationMin=3600",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
let endpoint = if search.is_empty() {
|
||||||
|
"api/videos"
|
||||||
} else {
|
} else {
|
||||||
items.clone()
|
"api/videos/search"
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
let mut url = format!(
|
||||||
let client = Client::builder()
|
"{}/{endpoint}?limit=100&page={page}{duration}{sort}",
|
||||||
.cert_verification(false)
|
self.url
|
||||||
.emulation(Emulation::Firefox136)
|
);
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let response = client
|
if let Ok(stars) = self.stars.read() {
|
||||||
.post(url.clone())
|
if let Some(star) = stars.iter().find(|s| s.eq_ignore_ascii_case(&search)) {
|
||||||
// .proxy(proxy)
|
url.push_str(&format!("&stars={star}"));
|
||||||
.json(&request)
|
|
||||||
.header("Content-Type", "text/plain;charset=UTF-8")
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
if response.status().is_success() {
|
|
||||||
let videos = match response.json::<PmvhavenResponse>().await {
|
|
||||||
Ok(resp) => resp,
|
|
||||||
Err(e) => {
|
|
||||||
println!("Failed to parse PmvhavenResponse: {}", e);
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(cats) = self.categories.read() {
|
||||||
|
if let Some(cat) = cats.iter().find(|c| c.eq_ignore_ascii_case(&search)) {
|
||||||
|
url.push_str(&format!("&tagMode=OR&tags={cat}&expandTags=false"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !search.is_empty() {
|
||||||
|
url.push_str(&format!("&q={search}"));
|
||||||
|
}
|
||||||
|
if let Some((time, items)) = cache.get(&url) {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 300 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut requester = match options.requester {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return Ok(vec![]),
|
||||||
};
|
};
|
||||||
let video_items: Vec<VideoItem> = videos.to_videoitems();
|
|
||||||
if !video_items.is_empty() {
|
let text = requester.get(&url, None).await.unwrap_or_default();
|
||||||
|
let json = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
|
||||||
|
let items = self.get_video_items_from_json(json).await;
|
||||||
|
|
||||||
|
if !items.is_empty() {
|
||||||
cache.remove(&url);
|
cache.remove(&url);
|
||||||
cache.insert(url.clone(), video_items.clone());
|
cache.insert(url, items.clone());
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
}
|
||||||
return Ok(video_items);
|
Ok(items)
|
||||||
}
|
}
|
||||||
// else {
|
|
||||||
// let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
|
||||||
// let flare = Flaresolverr::new(flare_url);
|
|
||||||
// let result = flare
|
|
||||||
// .solve(FlareSolverrRequest {
|
|
||||||
// cmd: "request.get".to_string(),
|
|
||||||
// url: url.clone(),
|
|
||||||
// maxTimeout: 60000,
|
|
||||||
// })
|
|
||||||
// .await;
|
|
||||||
// let video_items = match result {
|
|
||||||
// Ok(res) => {
|
|
||||||
// // println!("FlareSolverr response: {}", res);
|
|
||||||
// self.get_video_items_from_html(res.solution.response)
|
|
||||||
// }
|
|
||||||
// Err(e) => {
|
|
||||||
// println!("Error solving FlareSolverr: {}", e);
|
|
||||||
// return Err("Failed to solve FlareSolverr".into());
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// if !video_items.is_empty() {
|
|
||||||
// cache.remove(&url);
|
|
||||||
// cache.insert(url.clone(), video_items.clone());
|
|
||||||
// } else {
|
|
||||||
// return Ok(old_items);
|
|
||||||
// }
|
|
||||||
// Ok(video_items)
|
|
||||||
// }
|
|
||||||
Err("Failed to get Videos".into())
|
|
||||||
}
|
|
||||||
async fn query(&self, cache: VideoCache, page: u8, query: &str) -> Result<Vec<VideoItem>> {
|
|
||||||
let index = format!("pmvhaven:{}:{}", query, page);
|
|
||||||
let url = format!("{}/api/v2/search", self.url);
|
|
||||||
let request = PmvhavenSearch::new(query.to_string(),page as u32);
|
|
||||||
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
|
||||||
let old_items = match cache.get(&index) {
|
|
||||||
Some((time, items)) => {
|
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
|
||||||
return Ok(items.clone());
|
|
||||||
} else {
|
|
||||||
let _ = cache.check().await;
|
|
||||||
return Ok(items.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
async fn get_video_items_from_json(&self, json: serde_json::Value) -> Vec<VideoItem> {
|
||||||
let client = Client::builder()
|
let mut items = vec![];
|
||||||
.cert_verification(false)
|
|
||||||
.emulation(Emulation::Firefox136)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let response = client
|
if !json.get("success").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||||
.post(url.clone())
|
return items;
|
||||||
// .proxy(proxy)
|
|
||||||
.json(&request)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
if response.status().is_success() {
|
|
||||||
let videos = match response.json::<PmvhavenResponse>().await {
|
|
||||||
Ok(resp) => resp,
|
|
||||||
Err(e) => {
|
|
||||||
println!("Failed to parse PmvhavenResponse: {}", e);
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
let video_items: Vec<VideoItem> = videos.to_videoitems();
|
let videos = json.get("data").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||||
if !video_items.is_empty() {
|
|
||||||
cache.remove(&url);
|
for video in videos {
|
||||||
cache.insert(url.clone(), video_items.clone());
|
let title = decode(video.get("title").and_then(|v| v.as_str()).unwrap_or("").as_bytes())
|
||||||
} else {
|
.to_string()
|
||||||
return Ok(old_items);
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let id = video
|
||||||
|
.get("_id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or(&title)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let video_url = video.get("videoUrl").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
|
let thumb = video.get("thumbnailUrl").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
|
let preview = video.get("previewUrl").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
|
|
||||||
|
let views = video.get("views").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
|
let duration = parse_time_to_seconds(video.get("duration").and_then(|v| v.as_str()).unwrap_or("0")).unwrap_or(0);
|
||||||
|
|
||||||
|
let tags = video.get("tags").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||||
|
let stars = video.get("starsTags").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||||
|
for t in tags.iter() {
|
||||||
|
if let Some(s) = t.as_str() {
|
||||||
|
let decoded = decode(s.as_bytes()).to_string().unwrap_or_default();
|
||||||
|
Self::push_unique(&self.categories, decoded.clone());
|
||||||
}
|
}
|
||||||
return Ok(video_items);
|
|
||||||
}
|
}
|
||||||
// else {
|
for t in stars.iter() {
|
||||||
// let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
if let Some(s) = t.as_str() {
|
||||||
// let flare = Flaresolverr::new(flare_url);
|
let decoded = decode(s.as_bytes()).to_string().unwrap_or_default();
|
||||||
// let result = flare
|
Self::push_unique(&self.stars, decoded.clone());
|
||||||
// .solve(FlareSolverrRequest {
|
}
|
||||||
// cmd: "request.get".to_string(),
|
}
|
||||||
// url: url.clone(),
|
|
||||||
// maxTimeout: 60000,
|
items.push(
|
||||||
// })
|
VideoItem::new(id, title, video_url.replace(' ', "%20"), "pmvhaven".into(), thumb, duration as u32)
|
||||||
// .await;
|
.views(views as u32)
|
||||||
// let video_items = match result {
|
.preview(preview)
|
||||||
// Ok(res) => self.get_video_items_from_html(res.solution.response),
|
);
|
||||||
// Err(e) => {
|
}
|
||||||
// println!("Error solving FlareSolverr: {}", e);
|
|
||||||
// return Err("Failed to solve FlareSolverr".into());
|
items
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// if !video_items.is_empty() {
|
|
||||||
// cache.remove(&url);
|
|
||||||
// cache.insert(url.clone(), video_items.clone());
|
|
||||||
// } else {
|
|
||||||
// return Ok(old_items);
|
|
||||||
// }
|
|
||||||
// Ok(video_items)
|
|
||||||
// }
|
|
||||||
Err("Failed to query Videos".into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Provider for PmvhavenProvider {
|
impl Provider for PmvhavenProvider {
|
||||||
async fn get_videos(
|
async fn get_videos(
|
||||||
&self,
|
&self,
|
||||||
cache: VideoCache,
|
cache: VideoCache,
|
||||||
pool: DbPool,
|
_pool: DbPool,
|
||||||
sort: String,
|
_sort: String,
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
page: String,
|
page: String,
|
||||||
per_page: String,
|
_per_page: String,
|
||||||
options: ServerOptions,
|
options: ServerOptions,
|
||||||
) -> Vec<VideoItem> {
|
) -> Vec<VideoItem> {
|
||||||
let _ = per_page;
|
let page = page.parse::<u8>().unwrap_or(1);
|
||||||
let _ = pool; // Ignored in this implementation
|
let query = query.unwrap_or_default();
|
||||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
|
||||||
Some(q) => self.query(cache, page.parse::<u8>().unwrap_or(1), &q).await,
|
match self.query(cache, page, &query, options).await {
|
||||||
None => {
|
|
||||||
self.get(cache, page.parse::<u8>().unwrap_or(1), options.category.unwrap(), sort)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match videos {
|
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error fetching videos: {}", e);
|
eprintln!("pmvhaven error: {e}");
|
||||||
|
let mut chain_str = String::new();
|
||||||
|
for (i, cause) in e.iter().enumerate() {
|
||||||
|
let _ = writeln!(chain_str, "{}. {}", i + 1, cause);
|
||||||
|
}
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(chain_str),
|
||||||
|
Some("PMVHaven Provider"),
|
||||||
|
Some("Failed to load videos from PMVHaven"),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
).await;
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<Channel> {
|
||||||
|
Some(self.build_channel(clientversion))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
205
src/providers/porn00.rs
Normal file
205
src/providers/porn00.rs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::vec;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Porn00Provider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl Porn00Provider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Porn00Provider {
|
||||||
|
url: "https://www.porn00.org".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"popular" => "/popular-vids",
|
||||||
|
"top-rated" => "/top-vids",
|
||||||
|
_ => "/latest-vids",
|
||||||
|
};
|
||||||
|
|
||||||
|
let list_str = match sort {
|
||||||
|
"popular" => "list_videos_common_videos_list",
|
||||||
|
"top-rated" => "list_videos_common_videos_list",
|
||||||
|
_ => "list_videos_most_recent_videos",
|
||||||
|
};
|
||||||
|
|
||||||
|
let video_url = format!("{}{}/?mode=async^&function=get_block^&block_id={}^&from={}", self.url, sort_string, list_str, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_lowercase().trim().replace(" ", "-");
|
||||||
|
let video_url = format!("{}/q/{}/?mode=async&function=get_block&block_id=list_videos_videos_list_search_result&q={}&category_ids=&sort_by=post_date&from_videos={}&from_albums={}&", self.url, search_string, search_string, page, page);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("<div class=\"pagination\"").collect::<Vec<&str>>()[0]
|
||||||
|
.split("<div class=\"item \">")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0].to_string();
|
||||||
|
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let raw_duration = video_segment.split("<div class=\"duration\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let thumb = video_segment.split("<img class=\"thumb ").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-original=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let views_part = video_segment.split("<div class=\"views\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let views = parse_abbreviated_number(&views_part).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"porn00".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.views(views)
|
||||||
|
;
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for Porn00Provider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q,options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
230
src/providers/pornhat.rs
Normal file
230
src/providers/pornhat.rs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::vec;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PornhatProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl PornhatProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
PornhatProvider {
|
||||||
|
url: "https://www.pornhat.com".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options:ServerOptions
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"trending" => "/trending",
|
||||||
|
"popular" => "/popular",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
let video_url = format!("{}{}/{}/", self.url, sort_string, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options:ServerOptions
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_lowercase().trim().replace(" ", "-");
|
||||||
|
let mut video_url = format!("{}/search/{}/{}/", self.url, search_string, page);
|
||||||
|
|
||||||
|
if search_string.starts_with("@"){
|
||||||
|
let url_part = search_string.split("@").collect::<Vec<&str>>()[1].replace(":", "/");
|
||||||
|
video_url = format!("{}/{}/", self.url, url_part);
|
||||||
|
}
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("<div class=\"pagination\"").collect::<Vec<&str>>()[0]
|
||||||
|
.split("item thumb-bl thumb-bl-video video_")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = format!("{}{}", self.url, video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]);
|
||||||
|
let preview_url = video_segment.split("data-preview-custom=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let raw_duration = video_segment.split("fa fa-clock-o").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<span>").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let thumb = video_segment.split("<img class=\"thumb lazy-load\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-original=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut tags = vec![];
|
||||||
|
if video_segment.contains("href=\"/sites/"){
|
||||||
|
let raw_tags = video_segment.split("href=\"/sites/").collect::<Vec<&str>>()[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.split("/\"").collect::<Vec<&str>>()[0].to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
for tag in raw_tags {
|
||||||
|
if !tag.is_empty() {
|
||||||
|
tags.push(format!("@sites:{}",tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if video_segment.contains("href=\"/models/"){
|
||||||
|
let raw_tags = video_segment.split("href=\"/models/").collect::<Vec<&str>>()[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.split("/\"").collect::<Vec<&str>>()[0].to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
for tag in raw_tags {
|
||||||
|
if !tag.is_empty() {
|
||||||
|
tags.push(format!("@models:{}",tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let views_part = video_segment.split("fa fa-eye").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<span>").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let views = parse_abbreviated_number(&views_part).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"Pornhat".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.preview(preview_url)
|
||||||
|
.views(views)
|
||||||
|
.tags(tags)
|
||||||
|
;
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for PornhatProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,326 +1,260 @@
|
|||||||
use crate::schema::videos::url;
|
|
||||||
use crate::util::parse_abbreviated_number;
|
use crate::util::parse_abbreviated_number;
|
||||||
use crate::DbPool;
|
use crate::DbPool;
|
||||||
use crate::providers::Provider;
|
use crate::providers::Provider;
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
|
||||||
use crate::util::time::parse_time_to_seconds;
|
use crate::util::time::parse_time_to_seconds;
|
||||||
use crate::videos::{ServerOptions, VideoItem};
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
|
||||||
use error_chain::error_chain;
|
use error_chain::error_chain;
|
||||||
use futures::stream::SplitSink;
|
use htmlentity::entity::{decode, ICodedDataTrait};
|
||||||
use htmlentity::entity::{ICodedDataTrait, decode};
|
use async_trait::async_trait;
|
||||||
use std::env;
|
|
||||||
use std::vec;
|
use std::vec;
|
||||||
use wreq::{Client, Proxy};
|
|
||||||
use wreq_util::Emulation;
|
|
||||||
|
|
||||||
error_chain! {
|
error_chain! {
|
||||||
foreign_links {
|
foreign_links {
|
||||||
Io(std::io::Error);
|
Io(std::io::Error);
|
||||||
HttpRequest(wreq::Error);
|
HttpRequest(wreq::Error);
|
||||||
}
|
}
|
||||||
|
errors {
|
||||||
|
Parse(msg: String) {
|
||||||
|
description("parse error")
|
||||||
|
display("parse error: {}", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PornhubProvider {
|
pub struct PornhubProvider {
|
||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PornhubProvider {
|
impl PornhubProvider {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
PornhubProvider {
|
Self {
|
||||||
url: "https://www.pornhub.com".to_string(),
|
url: "https://www.pornhub.com".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(
|
async fn get(
|
||||||
&self,
|
&self,
|
||||||
cache: VideoCache,
|
cache: VideoCache,
|
||||||
page: u8,
|
page: u8,
|
||||||
sort: &str,
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
) -> Result<Vec<VideoItem>> {
|
) -> Result<Vec<VideoItem>> {
|
||||||
let video_url = format!("{}/video?o={}&page={}", self.url, sort, page);
|
let video_url = format!("{}/video?o={}&page={}", self.url, sort, page);
|
||||||
|
|
||||||
let old_items = match cache.get(&video_url) {
|
let old_items = match cache.get(&video_url) {
|
||||||
Some((time, items)) => {
|
Some((time, items)) if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
|
||||||
println!("Cache hit for URL: {}", video_url);
|
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
} else {
|
|
||||||
items.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
vec![]
|
|
||||||
}
|
}
|
||||||
|
Some((_, items)) => items.clone(),
|
||||||
|
None => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
let mut requester = match options.requester.clone() {
|
||||||
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
Some(r) => r,
|
||||||
|
None => return Ok(old_items),
|
||||||
let mut response = client.get(video_url.clone())
|
|
||||||
// .proxy(proxy.clone())
|
|
||||||
.send().await?;
|
|
||||||
if response.status().is_redirection(){
|
|
||||||
|
|
||||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
|
||||||
// .proxy(proxy)
|
|
||||||
.send().await?;
|
|
||||||
}
|
|
||||||
if response.status().is_success() {
|
|
||||||
let text = response.text().await?;
|
|
||||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone(),"<ul id=\"video");
|
|
||||||
if !video_items.is_empty() {
|
|
||||||
cache.remove(&video_url);
|
|
||||||
cache.insert(video_url.clone(), video_items.clone());
|
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
|
||||||
} else {
|
|
||||||
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
|
||||||
let flare = Flaresolverr::new(flare_url);
|
|
||||||
let result = flare
|
|
||||||
.solve(FlareSolverrRequest {
|
|
||||||
cmd: "request.get".to_string(),
|
|
||||||
url: video_url.clone(),
|
|
||||||
maxTimeout: 60000,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let video_items = match result {
|
|
||||||
Ok(res) => {
|
|
||||||
// println!("FlareSolverr response: {}", res);
|
|
||||||
self.get_video_items_from_html(res.solution.response,"<ul id=\"video")
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error solving FlareSolverr: {}", e);
|
|
||||||
return Err("Failed to solve FlareSolverr".into());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if !video_items.is_empty() {
|
|
||||||
|
let text = match requester.get(&video_url, None).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => return Ok(old_items),
|
||||||
|
};
|
||||||
|
|
||||||
|
let video_items = self.get_video_items_from_html(text, "<ul id=\"video");
|
||||||
|
|
||||||
|
if video_items.is_empty() {
|
||||||
|
Ok(old_items)
|
||||||
|
} else {
|
||||||
cache.remove(&video_url);
|
cache.remove(&video_url);
|
||||||
cache.insert(video_url.clone(), video_items.clone());
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
Ok(video_items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn query(
|
async fn query(
|
||||||
&self,
|
&self,
|
||||||
cache: VideoCache,
|
cache: VideoCache,
|
||||||
page: u8,
|
page: u8,
|
||||||
query: &str,
|
query: &str,
|
||||||
sort: &str,
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
) -> Result<Vec<VideoItem>> {
|
) -> Result<Vec<VideoItem>> {
|
||||||
let mut split_string = "<ul id=\"video";
|
let mut split_string = "<ul id=\"video";
|
||||||
let search_string = query.to_lowercase().trim().replace(" ", "+");
|
let search_string = query.to_lowercase().trim().replace(' ', "+");
|
||||||
let mut video_url = format!("{}/video/search?search={}&page={}", self.url, search_string, page);
|
|
||||||
if query.starts_with("@"){
|
let mut video_url =
|
||||||
let url_parts = query[1..].split(":").collect::<Vec<&str>>();
|
format!("{}/video/search?search={}&page={}", self.url, search_string, page);
|
||||||
video_url = [self.url.to_string(), url_parts[0].to_string(), url_parts[1].replace(" ", "-").to_string(), "videos?page=".to_string()].join("/");
|
|
||||||
video_url += &page.to_string();
|
if query.starts_with('@') {
|
||||||
if query.contains("@model") || query.contains("@pornstar"){
|
let mut parts = query[1..].split(':');
|
||||||
|
let a = parts.next().unwrap_or("");
|
||||||
|
let b = parts.next().unwrap_or("");
|
||||||
|
video_url = format!("{}/{}/{}/videos?page={}", self.url, a, b.replace(' ', "-"), page);
|
||||||
|
|
||||||
|
if query.contains("@model") || query.contains("@pornstar") {
|
||||||
split_string = "mostRecentVideosSection";
|
split_string = "mostRecentVideosSection";
|
||||||
}
|
}
|
||||||
if query.contains("@channels"){
|
if query.contains("@channels") {
|
||||||
split_string = "<ul class=\"videos row-5-thumbs";
|
split_string = "<ul class=\"videos row-5-thumbs";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.contains("@channels"){
|
video_url.push_str(match (query.contains("@channels"), sort) {
|
||||||
video_url += match sort {
|
(true, "mv") => "&o=vi",
|
||||||
"mr" => "",
|
(true, "tr") => "&o=ra",
|
||||||
"mv" => "&o=vi",
|
(false, "mv") => "&o=mv",
|
||||||
"tr" => "&o=ra",
|
(false, "tr") => "&o=tr",
|
||||||
|
(false, "lg") => "&o=lg",
|
||||||
_ => "",
|
_ => "",
|
||||||
}
|
});
|
||||||
} else{
|
|
||||||
video_url += format!("&o={}", sort).as_str();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
|
||||||
let old_items = match cache.get(&video_url) {
|
let old_items = match cache.get(&video_url) {
|
||||||
Some((time, items)) => {
|
Some((time, items)) if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
|
||||||
return Ok(items.clone());
|
|
||||||
} else {
|
|
||||||
let _ = cache.check().await;
|
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
}
|
}
|
||||||
}
|
Some((_, items)) => items.clone(),
|
||||||
None => {
|
None => vec![],
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
let mut requester = match options.requester.clone() {
|
||||||
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
Some(r) => r,
|
||||||
|
None => return Ok(old_items),
|
||||||
let mut response = client.get(video_url.clone())
|
|
||||||
.proxy(proxy.clone())
|
|
||||||
.send().await?;
|
|
||||||
|
|
||||||
if response.status().is_redirection(){
|
|
||||||
|
|
||||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
|
||||||
.proxy(proxy)
|
|
||||||
.send().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
let text = response.text().await?;
|
|
||||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone(),split_string);
|
|
||||||
if !video_items.is_empty() {
|
|
||||||
cache.remove(&video_url);
|
|
||||||
cache.insert(video_url.clone(), video_items.clone());
|
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
|
||||||
} else {
|
|
||||||
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
|
||||||
let flare = Flaresolverr::new(flare_url);
|
|
||||||
let result = flare
|
|
||||||
.solve(FlareSolverrRequest {
|
|
||||||
cmd: "request.get".to_string(),
|
|
||||||
url: video_url.clone(),
|
|
||||||
maxTimeout: 60000,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let video_items = match result {
|
|
||||||
Ok(res) => self.get_video_items_from_html(res.solution.response,split_string),
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error solving FlareSolverr: {}", e);
|
|
||||||
return Err("Failed to solve FlareSolverr".into());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if !video_items.is_empty() {
|
|
||||||
|
let text = match requester.get(&video_url, None).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => return Ok(old_items),
|
||||||
|
};
|
||||||
|
|
||||||
|
let video_items = self.get_video_items_from_html(text, split_string);
|
||||||
|
|
||||||
|
if video_items.is_empty() {
|
||||||
|
Ok(old_items)
|
||||||
|
} else {
|
||||||
cache.remove(&video_url);
|
cache.remove(&video_url);
|
||||||
cache.insert(video_url.clone(), video_items.clone());
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
Ok(video_items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_video_items_from_html(&self, html: String, split_string: &str) -> Vec<VideoItem> {
|
fn get_video_items_from_html(&self, html: String, split_string: &str) -> Vec<VideoItem> {
|
||||||
if html.is_empty() {
|
let content = match html.split(split_string).nth(1) {
|
||||||
println!("HTML is empty");
|
Some(c) => c,
|
||||||
return vec![];
|
None => return vec![],
|
||||||
}
|
|
||||||
let mut items: Vec<VideoItem> = Vec::new();
|
|
||||||
let video_listing_content = html.split(split_string).collect::<Vec<&str>>()[1].split("Porn in German").collect::<Vec<&str>>()[0];
|
|
||||||
let raw_videos = video_listing_content
|
|
||||||
.split("class=\"pcVideoListItem ")
|
|
||||||
.collect::<Vec<&str>>()[1..]
|
|
||||||
.to_vec();
|
|
||||||
for video_segment in &raw_videos {
|
|
||||||
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
|
||||||
// for (index, line) in vid.iter().enumerate() {
|
|
||||||
// println!("Line {}: {}", index, line);
|
|
||||||
// }
|
|
||||||
if video_segment.contains("wrapVideoBlock"){
|
|
||||||
continue; // Skip if the segment is a wrapVideoBlock
|
|
||||||
}
|
|
||||||
let mut video_url: String = String::new();
|
|
||||||
if !video_segment.contains("<a href=\"") {
|
|
||||||
let url_part = video_segment.split("data-video-vkey=\"").collect::<Vec<&str>>()[1]
|
|
||||||
.split("\"")
|
|
||||||
.collect::<Vec<&str>>()[0];
|
|
||||||
video_url = format!("{}{}", self.url, url_part);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
let url_part = video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
|
||||||
.split("\"")
|
|
||||||
.collect::<Vec<&str>>()[0];
|
|
||||||
if url_part.is_empty() || url_part == "javascript:void(0)" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
video_url = format!("{}{}", self.url, url_part);
|
|
||||||
}
|
|
||||||
if video_url == "https://www.pornhub.comjavascript:void(0)".to_string() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
|
||||||
.split("\"")
|
|
||||||
.collect::<Vec<&str>>()[0]
|
|
||||||
.to_string();
|
|
||||||
// html decode
|
|
||||||
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
|
||||||
let id = video_segment.split("data-video-id=\"").collect::<Vec<&str>>()[1]
|
|
||||||
.split("\"")
|
|
||||||
.collect::<Vec<&str>>()[0]
|
|
||||||
.to_string();
|
|
||||||
let raw_duration = video_segment.split("duration").collect::<Vec<&str>>()[1].split(">").collect::<Vec<&str>>()[1]
|
|
||||||
.split("<")
|
|
||||||
.collect::<Vec<&str>>()[0]
|
|
||||||
.to_string();
|
|
||||||
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
|
||||||
let view_part = match video_segment.split("iews\">").collect::<Vec<&str>>().len(){
|
|
||||||
2 => video_segment.split("iews\">").collect::<Vec<&str>>()[1],
|
|
||||||
3 => video_segment.split("iews\">").collect::<Vec<&str>>()[2],
|
|
||||||
_ => "<var>0<", // Skip if the format is unexpected
|
|
||||||
};
|
};
|
||||||
let views = parse_abbreviated_number(view_part
|
|
||||||
.split("<var>").collect::<Vec<&str>>()[1]
|
|
||||||
.split("<")
|
|
||||||
.collect::<Vec<&str>>()[0]).unwrap_or(0);
|
|
||||||
|
|
||||||
let thumb = video_segment.split("src=\"").collect::<Vec<&str>>()[1]
|
let content = content.split("Porn in German").next().unwrap_or("");
|
||||||
.split("\"")
|
|
||||||
.collect::<Vec<&str>>()[0]
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
for seg in content
|
||||||
|
.split("class=\"pcVideoListItem ")
|
||||||
|
.skip(1)
|
||||||
|
.filter(|s| !s.contains("wrapVideoBlock"))
|
||||||
|
{
|
||||||
|
let url_part = seg
|
||||||
|
.split("<a href=\"")
|
||||||
|
.nth(1)
|
||||||
|
.or_else(|| seg.split("data-video-vkey=\"").nth(1))
|
||||||
|
.and_then(|s| s.split('"').next());
|
||||||
|
|
||||||
|
let video_url = match url_part {
|
||||||
|
Some(u) if !u.is_empty() && u != "javascript:void(0)" => format!("{}{}", self.url, u),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut title = seg
|
||||||
|
.split("\" title=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
|
||||||
|
let id = match seg
|
||||||
|
.split("data-video-id=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
{
|
||||||
|
Some(id) => id.to_string(),
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
let mut uploaderBlock = String::new();
|
let raw_duration = seg
|
||||||
let mut uploader_href = vec![];
|
.split("duration")
|
||||||
let mut tag = String::new();
|
.nth(1)
|
||||||
if video_segment.contains("videoUploaderBlock") {
|
.and_then(|s| s.split('>').nth(1))
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.unwrap_or("0:00");
|
||||||
|
|
||||||
uploaderBlock = video_segment.split("videoUploaderBlock").collect::<Vec<&str>>()[1]
|
let duration = parse_time_to_seconds(raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let views = seg
|
||||||
|
.split("iews\">")
|
||||||
|
.filter_map(|p| p.split("<var>").nth(1))
|
||||||
|
.next()
|
||||||
|
.and_then(|v| v.split('<').next())
|
||||||
|
.and_then(|v| parse_abbreviated_number(v))
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let thumb = seg
|
||||||
|
.split("src=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
uploader_href = uploaderBlock.split("href=\"").collect::<Vec<&str>>()[1]
|
|
||||||
.split("\"")
|
|
||||||
.collect::<Vec<&str>>()[0]
|
|
||||||
.split("/").collect::<Vec<&str>>();
|
|
||||||
tag = format!("@{}:{}", uploader_href[1], uploader_href[2].replace("-", " "));
|
|
||||||
|
|
||||||
|
let (tag, uploader) = if seg.contains("videoUploaderBlock") {
|
||||||
|
let href = seg
|
||||||
|
.split("videoUploaderBlock")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("href=\"").nth(1))
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
let parts: Vec<&str> = href.split('/').collect();
|
||||||
|
if parts.len() >= 3 {
|
||||||
|
(
|
||||||
|
Some(format!("@{}:{}", parts[1], parts[2].replace('-', " "))),
|
||||||
|
Some(parts[2].to_string()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut item = VideoItem::new(
|
||||||
let mut video_item = VideoItem::new(
|
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
video_url.to_string(),
|
video_url,
|
||||||
"pornhub".to_string(),
|
"pornhub".into(),
|
||||||
thumb,
|
thumb,
|
||||||
duration,
|
duration,
|
||||||
)
|
);
|
||||||
;
|
|
||||||
if views > 0 {
|
if views > 0 {
|
||||||
video_item = video_item.views(views);
|
item = item.views(views);
|
||||||
}
|
}
|
||||||
if !tag.is_empty() {
|
if let Some(t) = tag {
|
||||||
video_item = video_item.tags(vec![tag])
|
item = item.tags(vec![t]);
|
||||||
.uploader(uploader_href[2].to_string());
|
|
||||||
}
|
}
|
||||||
// if video_segment.contains("data-mediabook=\"") {
|
if let Some(u) = uploader {
|
||||||
// let preview = video_segment.split("data-mediabook=\"").collect::<Vec<&str>>()[1]
|
item = item.uploader(u);
|
||||||
// .split("\"")
|
|
||||||
// .collect::<Vec<&str>>()[0]
|
|
||||||
// .to_string();
|
|
||||||
// video_item = video_item.preview(preview);
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
items.push(video_item);
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Provider for PornhubProvider {
|
impl Provider for PornhubProvider {
|
||||||
async fn get_videos(
|
async fn get_videos(
|
||||||
&self,
|
&self,
|
||||||
@@ -332,29 +266,29 @@ impl Provider for PornhubProvider {
|
|||||||
per_page: String,
|
per_page: String,
|
||||||
options: ServerOptions,
|
options: ServerOptions,
|
||||||
) -> Vec<VideoItem> {
|
) -> Vec<VideoItem> {
|
||||||
let _ = options;
|
let _ = pool;
|
||||||
let _ = per_page;
|
let _ = per_page;
|
||||||
let _ = pool; // Ignored in this implementation
|
|
||||||
let mut sort = sort.to_lowercase();
|
let page = page.parse::<u8>().unwrap_or(1);
|
||||||
if sort.contains("date"){
|
let mut sort = match sort.as_str() {
|
||||||
sort = "mr".to_string();
|
"mv" => "mv",
|
||||||
}
|
"tr" => "tr",
|
||||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
"cm" => "cm",
|
||||||
Some(q) => {
|
"lg" => "lg",
|
||||||
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, &sort)
|
_ => "mr",
|
||||||
.await
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
match videos {
|
if sort.contains("date") {
|
||||||
Ok(v) => v,
|
sort = "mr".into();
|
||||||
Err(e) => {
|
}
|
||||||
println!("Error fetching videos: {}", e);
|
|
||||||
|
let res = match query {
|
||||||
|
Some(q) => self.query(cache, page, &q, &sort, options).await,
|
||||||
|
None => self.get(cache, page, &sort, options).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.unwrap_or_else(|e| {
|
||||||
|
eprintln!("PornhubProvider error: {e}");
|
||||||
vec![]
|
vec![]
|
||||||
}
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
271
src/providers/pornxp.rs
Normal file
271
src/providers/pornxp.rs
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::status::*;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
// use std::sync::{Arc, RwLock};
|
||||||
|
// use std::thread;
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PornxpProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl PornxpProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let provider = PornxpProvider {
|
||||||
|
url: "https://pornxp.me".to_string(),
|
||||||
|
};
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
|
||||||
|
let _ = clientversion;
|
||||||
|
Channel {
|
||||||
|
id: "pornxp".to_string(),
|
||||||
|
name: "PornXP".to_string(),
|
||||||
|
description: "For Those Who Know The Difference".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=pornxp.me".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories: vec![],
|
||||||
|
options: vec![
|
||||||
|
ChannelOption {
|
||||||
|
id: "sort".to_string(),
|
||||||
|
title: "Sort".to_string(),
|
||||||
|
description: "Sort the Videos".to_string(),
|
||||||
|
systemImage: "list.number".to_string(),
|
||||||
|
colorName: "blue".to_string(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "new".into(),
|
||||||
|
title: "New".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "best".into(),
|
||||||
|
title: "Best".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string: String = match sort {
|
||||||
|
"best" => "best".to_string(),
|
||||||
|
_ => "new".to_string(),
|
||||||
|
};
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/{}?page={}",
|
||||||
|
self.url, sort_string, page
|
||||||
|
);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_string().to_lowercase().trim().to_string();
|
||||||
|
|
||||||
|
let sort_string: String = match sort {
|
||||||
|
"best" => "".to_string(),
|
||||||
|
_ => "&sort=new".to_string(),
|
||||||
|
};
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/tags/{}?page={}{}",
|
||||||
|
self.url, search_string, page, sort_string
|
||||||
|
);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("id=\"pages\"").collect::<Vec<&str>>()[0]
|
||||||
|
.split("<div id=\"content\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<div class=\"item_cont\">")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = format!("{}{}", self.url, video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string());
|
||||||
|
let mut title = video_segment
|
||||||
|
.split("<div class=\"item_title\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
|
||||||
|
let thumb = match video_segment.contains("<img class=\"item_img lazy\""){
|
||||||
|
true => format!("https:{}", video_segment.split("<img ").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-src=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string()),
|
||||||
|
false => format!("https:{}", video_segment.split("<img ").collect::<Vec<&str>>()[1]
|
||||||
|
.split("src=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string()),};
|
||||||
|
let raw_duration = video_segment
|
||||||
|
.split("<div class=\"item_dur\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let preview = format!("https:{}",video_segment
|
||||||
|
.split("data-preview=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string());
|
||||||
|
let tags = video_segment.split("<div class=\"item_tags\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("</div>")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.split("<a href=\"")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.into_iter().map(|s| s.split(">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0].to_string()).collect::<Vec<String>>();
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"pornxpme".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.tags(tags)
|
||||||
|
.preview(preview);
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for PornxpProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<crate::status::Channel> {
|
||||||
|
Some(self.build_channel(clientversion))
|
||||||
|
}
|
||||||
|
}
|
||||||
188
src/providers/pornzog.rs
Normal file
188
src/providers/pornzog.rs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::vec;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PornzogProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl PornzogProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
PornzogProvider {
|
||||||
|
url: "https://pornzog.com".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
sort: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let mut search_params = vec![format!("page={}", page), "site=hdzog".to_string()];
|
||||||
|
if !query.is_empty() {
|
||||||
|
search_params.push(format!("s={}", query.replace(" ", "+")));
|
||||||
|
}
|
||||||
|
let sort_string = match sort.as_str() {
|
||||||
|
"relevance" => "o=relevance",
|
||||||
|
"viewed" => "o=viewed",
|
||||||
|
"rated" => "o=rated",
|
||||||
|
"longest" => "o=longest",
|
||||||
|
_ => "o=recent",
|
||||||
|
};
|
||||||
|
search_params.push(format!("{}", &sort_string));
|
||||||
|
|
||||||
|
let video_url = format!("{}/search/?{}", self.url, search_params.join("&"));
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("class=\"paginator\"").collect::<Vec<&str>>()[0]
|
||||||
|
.split("class=\"thumb-video ")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let mut video_url: String = video_segment.split("href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
if video_url.starts_with("/") {
|
||||||
|
video_url = format!("{}{}", self.url, video_url);
|
||||||
|
}
|
||||||
|
let mut title = video_segment.split("alt=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
|
||||||
|
let thumb = format!(
|
||||||
|
"{}",
|
||||||
|
video_segment.split("<img ").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-original=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
let raw_duration = video_segment
|
||||||
|
.split("class=\"duration\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32;
|
||||||
|
// let uploader = video_segment.split("class=\"source\">").collect::<Vec<&str>>()[1]
|
||||||
|
// .split(">").collect::<Vec<&str>>()[1]
|
||||||
|
// .split("<").collect::<Vec<&str>>()[0]
|
||||||
|
// .to_string();
|
||||||
|
|
||||||
|
let tags = video_segment.split("class=\"tags\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("</p>")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.split("<a href=\"")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|el| {
|
||||||
|
el.split(">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"pornzog".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
// .uploader(uploader)
|
||||||
|
.tags(tags);
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for PornzogProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = self
|
||||||
|
.query(
|
||||||
|
cache,
|
||||||
|
page.parse::<u8>().unwrap_or(1),
|
||||||
|
query.unwrap_or("".to_string()).as_str(),
|
||||||
|
sort,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,14 @@
|
|||||||
use crate::schema::videos::url;
|
|
||||||
use crate::util::parse_abbreviated_number;
|
|
||||||
use crate::DbPool;
|
use crate::DbPool;
|
||||||
use crate::providers::Provider;
|
use crate::providers::Provider;
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
use crate::util::parse_abbreviated_number;
|
||||||
use crate::util::time::parse_time_to_seconds;
|
use crate::util::time::parse_time_to_seconds;
|
||||||
use crate::videos::{ServerOptions, VideoItem};
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use async_trait::async_trait;
|
||||||
use error_chain::error_chain;
|
use error_chain::error_chain;
|
||||||
use futures::stream::SplitSink;
|
|
||||||
use htmlentity::entity::{ICodedDataTrait, decode};
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::env;
|
|
||||||
use std::os::linux::raw;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::vec;
|
use std::vec;
|
||||||
use wreq::{Client, Proxy};
|
|
||||||
use wreq_util::Emulation;
|
|
||||||
|
|
||||||
error_chain! {
|
error_chain! {
|
||||||
foreign_links {
|
foreign_links {
|
||||||
@@ -39,12 +32,13 @@ impl RedtubeProvider {
|
|||||||
cache: VideoCache,
|
cache: VideoCache,
|
||||||
page: u8,
|
page: u8,
|
||||||
sort: &str,
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
) -> Result<Vec<VideoItem>> {
|
) -> Result<Vec<VideoItem>> {
|
||||||
let video_url = format!("{}/mostviewed", self.url);
|
let _ = sort;
|
||||||
|
let video_url = format!("{}/mostviewed?page={}", self.url, page);
|
||||||
let old_items = match cache.get(&video_url) {
|
let old_items = match cache.get(&video_url) {
|
||||||
Some((time, items)) => {
|
Some((time, items)) => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
println!("Cache hit for URL: {}", video_url);
|
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
} else {
|
} else {
|
||||||
items.clone()
|
items.clone()
|
||||||
@@ -54,21 +48,8 @@ impl RedtubeProvider {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
|
||||||
|
|
||||||
let mut response = client.get(video_url.clone())
|
|
||||||
// .proxy(proxy.clone())
|
|
||||||
.send().await?;
|
|
||||||
if response.status().is_redirection(){
|
|
||||||
|
|
||||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
|
||||||
// .proxy(proxy)
|
|
||||||
.send().await?;
|
|
||||||
}
|
|
||||||
if response.status().is_success() {
|
|
||||||
let text = response.text().await?;
|
|
||||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
if !video_items.is_empty() {
|
if !video_items.is_empty() {
|
||||||
cache.remove(&video_url);
|
cache.remove(&video_url);
|
||||||
@@ -77,34 +58,6 @@ impl RedtubeProvider {
|
|||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
Ok(video_items)
|
Ok(video_items)
|
||||||
} else {
|
|
||||||
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
|
||||||
let flare = Flaresolverr::new(flare_url);
|
|
||||||
let result = flare
|
|
||||||
.solve(FlareSolverrRequest {
|
|
||||||
cmd: "request.get".to_string(),
|
|
||||||
url: video_url.clone(),
|
|
||||||
maxTimeout: 60000,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let video_items = match result {
|
|
||||||
Ok(res) => {
|
|
||||||
// println!("FlareSolverr response: {}", res);
|
|
||||||
self.get_video_items_from_html(res.solution.response)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error solving FlareSolverr: {}", e);
|
|
||||||
return Err("Failed to solve FlareSolverr".into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !video_items.is_empty() {
|
|
||||||
cache.remove(&video_url);
|
|
||||||
cache.insert(video_url.clone(), video_items.clone());
|
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async fn query(
|
async fn query(
|
||||||
&self,
|
&self,
|
||||||
@@ -112,7 +65,9 @@ impl RedtubeProvider {
|
|||||||
page: u8,
|
page: u8,
|
||||||
query: &str,
|
query: &str,
|
||||||
sort: &str,
|
sort: &str,
|
||||||
|
options: ServerOptions
|
||||||
) -> Result<Vec<VideoItem>> {
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let _ = sort; //TODO
|
||||||
let search_string = query.to_lowercase().trim().replace(" ", "+");
|
let search_string = query.to_lowercase().trim().replace(" ", "+");
|
||||||
let video_url = format!("{}/?search={}&page={}", self.url, search_string, page);
|
let video_url = format!("{}/?search={}&page={}", self.url, search_string, page);
|
||||||
|
|
||||||
@@ -130,23 +85,8 @@ impl RedtubeProvider {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
|
||||||
|
|
||||||
let mut response = client.get(video_url.clone())
|
|
||||||
.proxy(proxy.clone())
|
|
||||||
.send().await?;
|
|
||||||
|
|
||||||
if response.status().is_redirection(){
|
|
||||||
|
|
||||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
|
||||||
.proxy(proxy)
|
|
||||||
.send().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
let text = response.text().await?;
|
|
||||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html_query(text.clone());
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html_query(text.clone());
|
||||||
if !video_items.is_empty() {
|
if !video_items.is_empty() {
|
||||||
cache.remove(&video_url);
|
cache.remove(&video_url);
|
||||||
@@ -155,31 +95,6 @@ impl RedtubeProvider {
|
|||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
Ok(video_items)
|
Ok(video_items)
|
||||||
} else {
|
|
||||||
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
|
||||||
let flare = Flaresolverr::new(flare_url);
|
|
||||||
let result = flare
|
|
||||||
.solve(FlareSolverrRequest {
|
|
||||||
cmd: "request.get".to_string(),
|
|
||||||
url: video_url.clone(),
|
|
||||||
maxTimeout: 60000,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let video_items = match result {
|
|
||||||
Ok(res) => self.get_video_items_from_html_query(res.solution.response),
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error solving FlareSolverr: {}", e);
|
|
||||||
return Err("Failed to solve FlareSolverr".into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !video_items.is_empty() {
|
|
||||||
cache.remove(&video_url);
|
|
||||||
cache.insert(video_url.clone(), video_items.clone());
|
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
@@ -188,20 +103,28 @@ impl RedtubeProvider {
|
|||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let mut items: Vec<VideoItem> = Vec::new();
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
let video_listing_content = html.split("<script type=\"application/ld+json\">").collect::<Vec<&str>>()[1].split("</script>").collect::<Vec<&str>>()[0];
|
let video_listing_content = html
|
||||||
|
.split("<script type=\"application/ld+json\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("</script>")
|
||||||
|
.collect::<Vec<&str>>()[0];
|
||||||
let mut videos: Value = serde_json::from_str(video_listing_content).unwrap();
|
let mut videos: Value = serde_json::from_str(video_listing_content).unwrap();
|
||||||
for vid in videos.as_array_mut().unwrap() {
|
for vid in videos.as_array_mut().unwrap() {
|
||||||
let mut video_url: String = vid["embedUrl"].as_str().unwrap_or("").to_string();
|
let video_url: String = vid["embedUrl"].as_str().unwrap_or("").to_string();
|
||||||
let mut title: String = vid["name"].as_str().unwrap_or("").to_string();
|
let mut title: String = vid["name"].as_str().unwrap_or("").to_string();
|
||||||
// html decode
|
// html decode
|
||||||
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
let id = video_url.split("=").collect::<Vec<&str>>()[1].to_string();
|
let id = video_url.split("=").collect::<Vec<&str>>()[1].to_string();
|
||||||
let raw_duration = vid["duration"].as_str().unwrap_or("0");
|
let raw_duration = vid["duration"].as_str().unwrap_or("0");
|
||||||
let duration = raw_duration.replace("PT", "").replace("S","").parse::<u32>().unwrap();
|
let duration = raw_duration
|
||||||
|
.replace("PT", "")
|
||||||
|
.replace("S", "")
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap();
|
||||||
let views: u64 = vid["interactionCount"].as_u64().unwrap_or(0);
|
let views: u64 = vid["interactionCount"].as_u64().unwrap_or(0);
|
||||||
let thumb = vid["thumbnailUrl"].as_str().unwrap_or("").to_string();
|
let thumb = vid["thumbnailUrl"].as_str().unwrap_or("").to_string();
|
||||||
|
|
||||||
let mut video_item = VideoItem::new(
|
let video_item = VideoItem::new(
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
video_url.to_string(),
|
video_url.to_string(),
|
||||||
@@ -209,8 +132,7 @@ impl RedtubeProvider {
|
|||||||
thumb,
|
thumb,
|
||||||
duration,
|
duration,
|
||||||
)
|
)
|
||||||
.views(views as u32)
|
.views(views as u32);
|
||||||
;
|
|
||||||
items.push(video_item);
|
items.push(video_item);
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
@@ -223,39 +145,64 @@ impl RedtubeProvider {
|
|||||||
}
|
}
|
||||||
let mut items: Vec<VideoItem> = Vec::new();
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
let video_listing_content = html.split("videos_grid").collect::<Vec<&str>>()[1];
|
let video_listing_content = html.split("videos_grid").collect::<Vec<&str>>()[1];
|
||||||
let videos = video_listing_content.split("<li id=\"tags_videos_").collect::<Vec<&str>>()[1..].to_vec();
|
let videos = video_listing_content
|
||||||
|
.split("<li id=\"tags_videos_")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
for vid in videos {
|
for vid in videos {
|
||||||
// for (i, c) in vid.split("\n").enumerate() {
|
// for (i, c) in vid.split("\n").enumerate() {
|
||||||
// println!("{}: {}", i, c);
|
// println!("{}: {}", i, c);
|
||||||
// }
|
// }
|
||||||
let id = vid.split("data-video-id=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
let id = vid.split("data-video-id=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
let video_url = format!("{}/{}", self.url, id);
|
let video_url = format!("{}/{}", self.url, id);
|
||||||
let title = vid.split(" <a title=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].trim().to_string();
|
let title = vid.split(" <a title=\"").collect::<Vec<&str>>()[1]
|
||||||
let thumb = vid.split("<img").collect::<Vec<&str>>()[1].split(" data-src=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
.split("\"")
|
||||||
let raw_duration = vid.split("<span class=\"video-properties tm_video_duration\">").collect::<Vec<&str>>()[1].split("</span>").collect::<Vec<&str>>()[0].trim().to_string();
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
let thumb = vid.split("<img").collect::<Vec<&str>>()[1]
|
||||||
|
.split(" data-src=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let raw_duration = vid
|
||||||
|
.split("<span class=\"video-properties tm_video_duration\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("</span>")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
let views_str = vid.split("<span class='info-views'>").collect::<Vec<&str>>()[1].split("</span>").collect::<Vec<&str>>()[0].trim().to_string();
|
let views_str = vid
|
||||||
|
.split("<span class='info-views'>")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("</span>")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
let views = parse_abbreviated_number(&views_str).unwrap_or(0) as u32;
|
let views = parse_abbreviated_number(&views_str).unwrap_or(0) as u32;
|
||||||
let preview = vid.split("<img").collect::<Vec<&str>>()[1].split(" data-mediabook=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
let preview = vid.split("<img").collect::<Vec<&str>>()[1]
|
||||||
|
.split(" data-mediabook=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let video_item = VideoItem::new(
|
let video_item =
|
||||||
id,
|
VideoItem::new(id, title, video_url, "redtube".to_string(), thumb, duration)
|
||||||
title,
|
|
||||||
video_url,
|
|
||||||
"redtube".to_string(),
|
|
||||||
thumb,
|
|
||||||
duration,
|
|
||||||
)
|
|
||||||
.views(views)
|
.views(views)
|
||||||
.preview(preview)
|
.preview(preview);
|
||||||
;
|
|
||||||
items.push(video_item);
|
items.push(video_item);
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Provider for RedtubeProvider {
|
impl Provider for RedtubeProvider {
|
||||||
async fn get_videos(
|
async fn get_videos(
|
||||||
&self,
|
&self,
|
||||||
@@ -271,16 +218,16 @@ impl Provider for RedtubeProvider {
|
|||||||
let _ = per_page;
|
let _ = per_page;
|
||||||
let _ = pool;
|
let _ = pool;
|
||||||
let mut sort = sort.to_lowercase();
|
let mut sort = sort.to_lowercase();
|
||||||
if sort.contains("date"){
|
if sort.contains("date") {
|
||||||
sort = "mr".to_string();
|
sort = "mr".to_string();
|
||||||
}
|
}
|
||||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
Some(q) => {
|
Some(q) => {
|
||||||
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, &sort)
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, &sort, options)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
288
src/providers/rule34gen.rs
Normal file
288
src/providers/rule34gen.rs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
use crate::api::*;
|
||||||
|
use crate::status::*;
|
||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::vec;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Rule34genProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl Rule34genProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Rule34genProvider {
|
||||||
|
url: "https://rule34gen.com".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
|
||||||
|
let _ = clientversion;
|
||||||
|
Channel {
|
||||||
|
id: "rule34gen".to_string(),
|
||||||
|
name: "Rule34Gen".to_string(),
|
||||||
|
description: "If it exists, here might be an AI generated video of it".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=rule34gen.com".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories: vec![],
|
||||||
|
options: vec![ChannelOption {
|
||||||
|
id: "sort".to_string(),
|
||||||
|
title: "Sort".to_string(),
|
||||||
|
description: "Sort the Videos".to_string(), //"Sort the videos by Date or Name.".to_string(),
|
||||||
|
systemImage: "list.number".to_string(),
|
||||||
|
colorName: "blue".to_string(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "post_date".to_string(),
|
||||||
|
title: "Newest".to_string(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "video_viewed".to_string(),
|
||||||
|
title: "Most Viewed".to_string(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "rating".to_string(),
|
||||||
|
title: "Top Rated".to_string(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "duration".to_string(),
|
||||||
|
title: "Longest".to_string(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "pseudo_random".to_string(),
|
||||||
|
title: "Random".to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
}],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: Some(1800),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Time went backwards");
|
||||||
|
|
||||||
|
let timestamp_millis = now.as_millis(); // u128
|
||||||
|
let expected_sorts = vec!["post_date", "video_viewed", "rating", "duration", "pseudo_random"];
|
||||||
|
let sort = if expected_sorts.contains(&sort) {
|
||||||
|
sort
|
||||||
|
} else {
|
||||||
|
"post_date"
|
||||||
|
};
|
||||||
|
|
||||||
|
let index = format!("rule34gen:{}:{}", page, sort);
|
||||||
|
|
||||||
|
let url = format!("{}/?mode=async&function=get_block&block_id=custom_list_videos_most_recent_videos&tag_ids=&sort_by={}&from={}&_={}", self.url, sort, page, timestamp_millis);
|
||||||
|
|
||||||
|
let mut old_items: Vec<VideoItem> = vec![];
|
||||||
|
if !(sort == "pseudo_random") {
|
||||||
|
old_items = match cache.get(&index) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
// println!("Cache hit for URL: {}", url);
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&url);
|
||||||
|
cache.insert(url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Time went backwards");
|
||||||
|
let timestamp_millis = now.as_millis(); // u128
|
||||||
|
let expected_sorts = vec!["post_date", "video_viewed", "rating", "duration", "pseudo_random"];
|
||||||
|
let sort = if expected_sorts.contains(&sort) {
|
||||||
|
sort
|
||||||
|
} else {
|
||||||
|
"post_date"
|
||||||
|
};
|
||||||
|
|
||||||
|
let index = format!("rule34gen:{}:{}:{}", page, sort, query);
|
||||||
|
|
||||||
|
let url = format!("{}/search/{}/?mode=async&function=get_block&block_id=custom_list_videos_videos_list_search&tag_ids=&sort_by={}&from_videos={}&from_albums={}&_={}", self.url, query.replace(" ","-"), sort, page, page, timestamp_millis);
|
||||||
|
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&index) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&url);
|
||||||
|
cache.insert(url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let video_listing_content = html.split("<div class=\"thumbs clearfix\" id=\"custom_list_videos").collect::<Vec<&str>>()[1].split("<div class=\"pagination\"").collect::<Vec<&str>>()[0].to_string();
|
||||||
|
let raw_videos = video_listing_content
|
||||||
|
.split("<div class=\"item thumb video_")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>()[1]
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
|
||||||
|
if video_segment.contains("https://rule34gen.com/images/advertisements"){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut title = video_segment.split("<div class=\"thumb_title\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_segment.split("https://rule34gen.com/video/").collect::<Vec<&str>>()[1].split("/").collect::<Vec<&str>>()[0].to_string();
|
||||||
|
let raw_duration = video_segment.split("<div class=\"time\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
let views = parse_abbreviated_number(&video_segment
|
||||||
|
.split("<div class=\"views\">").collect::<Vec<&str>>()[1].split("</svg>").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]).unwrap_or(0);
|
||||||
|
//https://rule34gen.com/get_file/47/5e71602b7642f9b997f90c979a368c99b8aad90d89/3942000/3942353/3942353_preview.mp4/
|
||||||
|
//https://rule34gen.com/get_file/47/5e71602b7642f9b997f90c979a368c99b8aad90d89/3942000/3942353/3942353_preview.mp4/
|
||||||
|
let thumb = video_segment.split("<img class=\"thumb lazy-load\" src=\"").collect::<Vec<&str>>()[1].split("data-original=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let url = video_segment.split("<a class=\"th js-open-popup\" href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// let preview = video_segment.split("<div class=\"img wrap_image\" data-preview=\"").collect::<Vec<&str>>()[1]
|
||||||
|
// .split("\"")
|
||||||
|
// .collect::<Vec<&str>>()[0]
|
||||||
|
// .to_string();
|
||||||
|
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
url.to_string(),
|
||||||
|
"rule34gen".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.views(views)
|
||||||
|
// .preview(preview)
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for Rule34genProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = options;
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool; // Ignored in this implementation
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<crate::status::Channel> {
|
||||||
|
Some(self.build_channel(clientversion))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,315 +1,266 @@
|
|||||||
use crate::util::parse_abbreviated_number;
|
|
||||||
use crate::DbPool;
|
use crate::DbPool;
|
||||||
use crate::providers::Provider;
|
use crate::providers::Provider;
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
use crate::util::discord::send_discord_error_report;
|
||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
use crate::util::time::parse_time_to_seconds;
|
use crate::util::time::parse_time_to_seconds;
|
||||||
use crate::videos::{ServerOptions, VideoItem};
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use async_trait::async_trait;
|
||||||
use error_chain::error_chain;
|
use error_chain::error_chain;
|
||||||
use futures::stream::SplitSink;
|
|
||||||
use htmlentity::entity::{ICodedDataTrait, decode};
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
use std::env;
|
|
||||||
use std::vec;
|
|
||||||
use wreq::{Client, Proxy};
|
|
||||||
use wreq_util::Emulation;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
error_chain! {
|
error_chain! {
|
||||||
foreign_links {
|
foreign_links {
|
||||||
Io(std::io::Error);
|
Io(std::io::Error);
|
||||||
HttpRequest(wreq::Error);
|
HttpRequest(wreq::Error);
|
||||||
}
|
}
|
||||||
|
errors {
|
||||||
|
ParsingError(t: String) {
|
||||||
|
description("html parsing error")
|
||||||
|
display("HTML parsing error: '{}'", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Rule34videoProvider {
|
pub struct Rule34videoProvider {
|
||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rule34videoProvider {
|
impl Rule34videoProvider {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Rule34videoProvider {
|
Rule34videoProvider {
|
||||||
url: "https://rule34video.com".to_string(),
|
url: "https://rule34video.com".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper to safely extract a string between two delimiters
|
||||||
|
fn extract_between<'a>(content: &'a str, start_pat: &str, end_pat: &str) -> Option<&'a str> {
|
||||||
|
let start_idx = content.find(start_pat)? + start_pat.len();
|
||||||
|
let sub = &content[start_idx..];
|
||||||
|
let end_idx = sub.find(end_pat)?;
|
||||||
|
Some(&sub[..end_idx])
|
||||||
|
}
|
||||||
|
|
||||||
async fn get(
|
async fn get(
|
||||||
&self,
|
&self,
|
||||||
cache: VideoCache,
|
cache: VideoCache,
|
||||||
page: u8,
|
page: u8,
|
||||||
sort: &str,
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
) -> Result<Vec<VideoItem>> {
|
) -> Result<Vec<VideoItem>> {
|
||||||
let now = SystemTime::now()
|
let timestamp_millis = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.expect("Time went backwards");
|
.map(|d| d.as_millis())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
let timestamp_millis = now.as_millis(); // u128
|
let expected_sorts = vec![
|
||||||
let expected_sorts = vec!["post_date", "video_viewed", "rating", "duration", "pseudo_random"];
|
"post_date",
|
||||||
let sort = if expected_sorts.contains(&sort) {
|
"video_viewed",
|
||||||
|
"rating",
|
||||||
|
"duration",
|
||||||
|
"pseudo_random",
|
||||||
|
];
|
||||||
|
let sort_val = if expected_sorts.contains(&sort) {
|
||||||
sort
|
sort
|
||||||
} else {
|
} else {
|
||||||
"post_date"
|
"post_date"
|
||||||
};
|
};
|
||||||
|
|
||||||
let index = format!("rule34video:{}:{}", page, sort);
|
let index = format!("rule34video:{}:{}", page, sort_val);
|
||||||
|
|
||||||
let url = format!("{}/?mode=async&function=get_block&block_id=custom_list_videos_most_recent_videos&tag_ids=&sort_by={}&from={}&_={}", self.url, sort, page, timestamp_millis);
|
if sort_val != "pseudo_random" {
|
||||||
|
if let Some((time, items)) = cache.get(&index) {
|
||||||
let mut old_items: Vec<VideoItem> = vec![];
|
if time.elapsed().unwrap_or_default().as_secs() < 300 {
|
||||||
if !(sort == "pseudo_random") {
|
|
||||||
old_items = match cache.get(&index) {
|
|
||||||
Some((time, items)) => {
|
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
|
||||||
println!("Cache hit for URL: {}", url);
|
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
} else {
|
|
||||||
items.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
let mut requester = options.requester.clone().ok_or("Requester missing")?;
|
||||||
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
let url = format!(
|
||||||
|
"{}/?mode=async&function=get_block&block_id=custom_list_videos_most_recent_videos&tag_ids=&sort_by={}&from={}&_={}",
|
||||||
|
self.url, sort_val, page, timestamp_millis
|
||||||
|
);
|
||||||
|
|
||||||
|
let text = requester.get(&url, None).await.unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error fetching rule34video URL {}: {}", url, e);
|
||||||
|
let _ = send_discord_error_report(e.to_string(), None, Some(&url), None, file!(), line!(), module_path!());
|
||||||
|
"".to_string()
|
||||||
|
});
|
||||||
|
let video_items = self.get_video_items_from_html(text);
|
||||||
|
|
||||||
let mut response = client.get(url.clone())
|
|
||||||
// .proxy(proxy.clone())
|
|
||||||
.send().await?;
|
|
||||||
while response.status().is_redirection(){
|
|
||||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
|
||||||
// .proxy(proxy.clone())
|
|
||||||
.send().await?;
|
|
||||||
}
|
|
||||||
if response.status().is_success() {
|
|
||||||
let text = response.text().await?;
|
|
||||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
|
||||||
if !video_items.is_empty() {
|
if !video_items.is_empty() {
|
||||||
cache.remove(&url);
|
cache.insert(index, video_items.clone());
|
||||||
cache.insert(url.clone(), video_items.clone());
|
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
Ok(video_items)
|
||||||
} else {
|
} else {
|
||||||
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
// Return empty or old items if available
|
||||||
let flare = Flaresolverr::new(flare_url);
|
Ok(cache
|
||||||
let result = flare
|
.get(&index)
|
||||||
.solve(FlareSolverrRequest {
|
.map(|(_, items)| items)
|
||||||
cmd: "request.get".to_string(),
|
.unwrap_or_default())
|
||||||
url: url.clone(),
|
|
||||||
maxTimeout: 60000,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let video_items = match result {
|
|
||||||
Ok(res) => {
|
|
||||||
// println!("FlareSolverr response: {}", res);
|
|
||||||
self.get_video_items_from_html(res.solution.response)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error solving FlareSolverr: {}", e);
|
|
||||||
return Err("Failed to solve FlareSolverr".into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !video_items.is_empty() {
|
|
||||||
cache.remove(&url);
|
|
||||||
cache.insert(url.clone(), video_items.clone());
|
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn query(
|
async fn query(
|
||||||
&self,
|
&self,
|
||||||
cache: VideoCache,
|
cache: VideoCache,
|
||||||
page: u8,
|
page: u8,
|
||||||
query: &str,
|
query: &str,
|
||||||
sort: &str,
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
) -> Result<Vec<VideoItem>> {
|
) -> Result<Vec<VideoItem>> {
|
||||||
let now = SystemTime::now()
|
let timestamp_millis = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.expect("Time went backwards");
|
.map(|d| d.as_millis())
|
||||||
let timestamp_millis = now.as_millis(); // u128
|
.unwrap_or(0);
|
||||||
let expected_sorts = vec!["post_date", "video_viewed", "rating", "duration", "pseudo_random"];
|
|
||||||
let sort = if expected_sorts.contains(&sort) {
|
let expected_sorts = vec![
|
||||||
|
"post_date",
|
||||||
|
"video_viewed",
|
||||||
|
"rating",
|
||||||
|
"duration",
|
||||||
|
"pseudo_random",
|
||||||
|
];
|
||||||
|
let sort_val = if expected_sorts.contains(&sort) {
|
||||||
sort
|
sort
|
||||||
} else {
|
} else {
|
||||||
"post_date"
|
"post_date"
|
||||||
};
|
};
|
||||||
|
|
||||||
let index = format!("rule34video:{}:{}:{}", page, sort, query);
|
let index = format!("rule34video:{}:{}:{}", page, sort_val, query);
|
||||||
|
|
||||||
let url = format!("{}/search/{}/?mode=async&function=get_block&block_id=custom_list_videos_videos_list_search&tag_ids=&sort_by={}&from_videos={}&from_albums={}&_={}", self.url, query, sort, page, page, timestamp_millis);
|
if let Some((time, items)) = cache.get(&index) {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 300 {
|
||||||
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
|
||||||
let old_items = match cache.get(&index) {
|
|
||||||
Some((time, items)) => {
|
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
|
||||||
return Ok(items.clone());
|
|
||||||
} else {
|
|
||||||
let _ = cache.check().await;
|
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let proxy = Proxy::all("http://192.168.0.103:8081").unwrap();
|
let mut requester = options.requester.clone().ok_or("Requester missing")?;
|
||||||
let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?;
|
let url = format!(
|
||||||
|
"{}/search/{}/?mode=async&function=get_block&block_id=custom_list_videos_videos_list_search&tag_ids=&sort_by={}&from_videos={}&from_albums={}&_={}",
|
||||||
|
self.url,
|
||||||
|
query.replace(" ", "-"),
|
||||||
|
sort_val,
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
timestamp_millis
|
||||||
|
);
|
||||||
|
|
||||||
let mut response = client.get(url.clone())
|
let text = requester.get(&url, None).await.unwrap_or_else(|e| {
|
||||||
// .proxy(proxy.clone())
|
eprintln!("Error fetching rule34video URL {}: {}", url, e);
|
||||||
.send().await?;
|
let _ = send_discord_error_report(e.to_string(), None, Some(&url), None, file!(), line!(), module_path!());
|
||||||
|
"".to_string()
|
||||||
|
});
|
||||||
|
let video_items = self.get_video_items_from_html(text);
|
||||||
|
|
||||||
if response.status().is_redirection(){
|
|
||||||
|
|
||||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
|
||||||
// .proxy(proxy)
|
|
||||||
.send().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
let text = response.text().await?;
|
|
||||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
|
||||||
if !video_items.is_empty() {
|
if !video_items.is_empty() {
|
||||||
cache.remove(&url);
|
cache.insert(index, video_items.clone());
|
||||||
cache.insert(url.clone(), video_items.clone());
|
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
Ok(video_items)
|
||||||
} else {
|
} else {
|
||||||
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
Ok(cache
|
||||||
let flare = Flaresolverr::new(flare_url);
|
.get(&index)
|
||||||
let result = flare
|
.map(|(_, items)| items)
|
||||||
.solve(FlareSolverrRequest {
|
.unwrap_or_default())
|
||||||
cmd: "request.get".to_string(),
|
|
||||||
url: url.clone(),
|
|
||||||
maxTimeout: 60000,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let video_items = match result {
|
|
||||||
Ok(res) => self.get_video_items_from_html(res.solution.response),
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error solving FlareSolverr: {}", e);
|
|
||||||
return Err("Failed to solve FlareSolverr".into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !video_items.is_empty() {
|
|
||||||
cache.remove(&url);
|
|
||||||
cache.insert(url.clone(), video_items.clone());
|
|
||||||
} else {
|
|
||||||
return Ok(old_items);
|
|
||||||
}
|
|
||||||
Ok(video_items)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
if html.is_empty() {
|
if html.is_empty() {
|
||||||
println!("HTML is empty");
|
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let mut items: Vec<VideoItem> = Vec::new();
|
|
||||||
let video_listing_content = html.split("<div class=\"thumbs clearfix\" id=\"custom_list_videos").collect::<Vec<&str>>()[1].split("<div class=\"pagination\"").collect::<Vec<&str>>()[0].to_string();
|
|
||||||
let raw_videos = video_listing_content
|
|
||||||
.split("<div class=\"item thumb video_")
|
|
||||||
.collect::<Vec<&str>>()[1..]
|
|
||||||
.to_vec();
|
|
||||||
for video_segment in &raw_videos {
|
|
||||||
// let vid = video_segment.split("\n").collect::<Vec<&str>>()[1]
|
|
||||||
// for (index, line) in vid.iter().enumerate() {
|
|
||||||
// println!("Line {}: {}", index, line);
|
|
||||||
// }
|
|
||||||
|
|
||||||
if video_segment.contains("https://rule34video.com/images/advertisements"){
|
// Safely isolate the video listing section
|
||||||
|
let video_listing = match Self::extract_between(
|
||||||
|
&html,
|
||||||
|
"id=\"custom_list_videos",
|
||||||
|
"<div class=\"pagination\"",
|
||||||
|
) {
|
||||||
|
Some(content) => content,
|
||||||
|
None => return vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
// Skip the first split result as it's the preamble
|
||||||
|
let raw_videos = video_listing
|
||||||
|
.split("<div class=\"item thumb video_")
|
||||||
|
.skip(1);
|
||||||
|
|
||||||
|
for video_segment in raw_videos {
|
||||||
|
if video_segment.contains("title=\"Advertisement\"") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut title = video_segment.split("<div class=\"thumb_title\">").collect::<Vec<&str>>()[1]
|
// Title extraction
|
||||||
.split("<")
|
let title_raw =
|
||||||
.collect::<Vec<&str>>()[0]
|
Self::extract_between(video_segment, "<div class=\"thumb_title\">", "<")
|
||||||
.to_string();
|
.unwrap_or("Unknown");
|
||||||
// html decode
|
let title = decode(title_raw.as_bytes())
|
||||||
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
.to_string()
|
||||||
let id = video_segment.split("https://rule34video.com/video/").collect::<Vec<&str>>()[1].split("/").collect::<Vec<&str>>()[0].to_string();
|
.unwrap_or_else(|_| title_raw.to_string());
|
||||||
let raw_duration = video_segment.split("<div class=\"time\">").collect::<Vec<&str>>()[1]
|
|
||||||
.split("<")
|
// ID extraction
|
||||||
.collect::<Vec<&str>>()[0]
|
let id = Self::extract_between(video_segment, "https://rule34video.com/video/", "/")
|
||||||
.to_string();
|
.unwrap_or("0")
|
||||||
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
|
||||||
let views = parse_abbreviated_number(&video_segment
|
|
||||||
.split("<div class=\"views\">").collect::<Vec<&str>>()[1].split("</svg>").collect::<Vec<&str>>()[1]
|
|
||||||
.split("<")
|
|
||||||
.collect::<Vec<&str>>()[0]).unwrap_or(0);
|
|
||||||
//https://rule34video.com/get_file/47/5e71602b7642f9b997f90c979a368c99b8aad90d89/3942000/3942353/3942353_preview.mp4/
|
|
||||||
//https://rule34video.com/get_file/47/5e71602b7642f9b997f90c979a368c99b8aad90d89/3942000/3942353/3942353_preview.mp4/
|
|
||||||
let thumb = video_segment.split("<img class=\"thumb lazy-load\" src=\"").collect::<Vec<&str>>()[1].split("data-original=\"").collect::<Vec<&str>>()[1]
|
|
||||||
.split("\"")
|
|
||||||
.collect::<Vec<&str>>()[0]
|
|
||||||
.to_string();
|
|
||||||
let url = video_segment.split("<a class=\"th js-open-popup\" href=\"").collect::<Vec<&str>>()[1]
|
|
||||||
.split("\"")
|
|
||||||
.collect::<Vec<&str>>()[0]
|
|
||||||
.to_string();
|
|
||||||
let preview = video_segment.split("<div class=\"img wrap_image\" data-preview=\"").collect::<Vec<&str>>()[1]
|
|
||||||
.split("\"")
|
|
||||||
.collect::<Vec<&str>>()[0]
|
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
// Duration extraction
|
||||||
|
let raw_duration =
|
||||||
|
Self::extract_between(video_segment, "<div class=\"time\">", "<").unwrap_or("0:00");
|
||||||
|
let duration = parse_time_to_seconds(raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
let mut video_item = VideoItem::new(
|
// Views extraction
|
||||||
id,
|
let views_segment = Self::extract_between(video_segment, "<div class=\"views\">", "<");
|
||||||
title,
|
let views_count_str = views_segment
|
||||||
url.to_string(),
|
.and_then(|s| s.split("</svg>").nth(1))
|
||||||
"Rule34video".to_string(),
|
.unwrap_or("0");
|
||||||
thumb,
|
let views = parse_abbreviated_number(views_count_str.trim()).unwrap_or(0);
|
||||||
duration,
|
|
||||||
)
|
|
||||||
.views(views)
|
|
||||||
// .preview(preview)
|
|
||||||
;
|
|
||||||
|
|
||||||
|
// Thumbnail extraction
|
||||||
|
let thumb = Self::extract_between(video_segment, "data-original=\"", "\"")
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
items.push(video_item);
|
// URL extraction
|
||||||
|
let url =
|
||||||
|
Self::extract_between(video_segment, "<a class=\"th js-open-popup\" href=\"", "\"")
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
VideoItem::new(id, title, url, "Rule34video".to_string(), thumb, duration)
|
||||||
|
.views(views),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return items;
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Provider for Rule34videoProvider {
|
impl Provider for Rule34videoProvider {
|
||||||
async fn get_videos(
|
async fn get_videos(
|
||||||
&self,
|
&self,
|
||||||
cache: VideoCache,
|
cache: VideoCache,
|
||||||
pool: DbPool,
|
_pool: DbPool,
|
||||||
sort: String,
|
sort: String,
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
page: String,
|
page: String,
|
||||||
per_page: String,
|
_per_page: String,
|
||||||
options: ServerOptions,
|
options: ServerOptions,
|
||||||
) -> Vec<VideoItem> {
|
) -> Vec<VideoItem> {
|
||||||
let _ = options;
|
let page_num = page.parse::<u8>().unwrap_or(1);
|
||||||
let _ = per_page;
|
|
||||||
let _ = pool; // Ignored in this implementation
|
let result = match query {
|
||||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
Some(q) => self.query(cache, page_num, &q, &sort, options).await,
|
||||||
Some(q) => {
|
None => self.get(cache, page_num, &sort, options).await,
|
||||||
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, &sort)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
match videos {
|
|
||||||
|
match result {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error fetching videos: {}", e);
|
eprintln!("Error fetching videos: {}", e);
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
394
src/providers/sxyprn.rs
Normal file
394
src/providers/sxyprn.rs
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::discord::format_error_chain;
|
||||||
|
use crate::util::discord::send_discord_error_report;
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::ServerOptions;
|
||||||
|
use crate::videos::VideoItem;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
JsonError(serde_json::Error);
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
Parse(msg: String) {
|
||||||
|
description("html parse error")
|
||||||
|
display("html parse error: {}", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SxyprnProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl SxyprnProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
SxyprnProvider {
|
||||||
|
url: "https://sxyprn.com".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
page: u8,
|
||||||
|
sort: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort.as_str() {
|
||||||
|
"views" => "views",
|
||||||
|
"rating" => "rating",
|
||||||
|
"orgasmic" => "orgasmic",
|
||||||
|
_ => "latest",
|
||||||
|
};
|
||||||
|
// Extract needed fields from options at the start
|
||||||
|
let filter = options.filter.clone().unwrap();
|
||||||
|
let filter_string = match filter.as_str() {
|
||||||
|
"other" => "other",
|
||||||
|
"all" => "all",
|
||||||
|
_ => "top",
|
||||||
|
};
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let url_str = format!(
|
||||||
|
"{}/blog/all/{}.html?fl={}&sm={}",
|
||||||
|
self.url,
|
||||||
|
((page as u32) - 1) * 20,
|
||||||
|
filter_string,
|
||||||
|
sort_string
|
||||||
|
);
|
||||||
|
|
||||||
|
let old_items = match cache.get(&url_str) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = requester.get(&url_str, None).await.unwrap();
|
||||||
|
// Pass a reference to options if needed, or reconstruct as needed
|
||||||
|
let video_items = match self
|
||||||
|
.get_video_items_from_html(text.clone(), pool, requester)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(items) => items,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error parsing video items: {}", e);
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(format_error_chain(&e)),
|
||||||
|
Some("Sxyprn Provider"),
|
||||||
|
Some(&format!("URL: {}", url_str)),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
).await;
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// let video_items: Vec<VideoItem> = self
|
||||||
|
// .get_video_items_from_html(text.clone(), pool, requester)
|
||||||
|
// .await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&url_str);
|
||||||
|
cache.insert(url_str.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
sort: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort.as_str() {
|
||||||
|
"views" => "views",
|
||||||
|
"rating" => "trending",
|
||||||
|
"orgasmic" => "orgasmic",
|
||||||
|
_ => "latest",
|
||||||
|
};
|
||||||
|
// Extract needed fields from options at the start
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let search_string = query.replace(" ", "-");
|
||||||
|
let url_str = format!(
|
||||||
|
"{}/{}.html?page={}&sm={}",
|
||||||
|
self.url,
|
||||||
|
search_string,
|
||||||
|
((page as u32) - 1) * 20,
|
||||||
|
sort_string
|
||||||
|
);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&url_str) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let text = requester.get(&url_str, None).await.unwrap();
|
||||||
|
|
||||||
|
let video_items = match self
|
||||||
|
.get_video_items_from_html(text.clone(), pool, requester)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(items) => items,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error parsing video items: {}", e);// 1. Convert the error to a string immediately
|
||||||
|
send_discord_error_report(
|
||||||
|
e.to_string(),
|
||||||
|
Some(format_error_chain(&e)),
|
||||||
|
Some("Sxyprn Provider"),
|
||||||
|
Some(&format!("URL: {}", url_str)),
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
module_path!(),
|
||||||
|
).await;
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// let video_items: Vec<VideoItem> = self
|
||||||
|
// .get_video_items_from_html(text.clone(), pool, requester)
|
||||||
|
// .await;
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&url_str);
|
||||||
|
cache.insert(url_str.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_items_from_html(
|
||||||
|
&self,
|
||||||
|
html: String,
|
||||||
|
_pool: DbPool,
|
||||||
|
_requester: Requester,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
if html.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// take content before "<script async"
|
||||||
|
let before_script = html
|
||||||
|
.split("<script async")
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("missing '<script async' split point".into()))?;
|
||||||
|
|
||||||
|
// split into video segments (skip the first chunk)
|
||||||
|
let raw_videos: Vec<&str> = before_script.split("post_el_small'").skip(1).collect();
|
||||||
|
|
||||||
|
if raw_videos.is_empty() {
|
||||||
|
return Err(ErrorKind::Parse("no 'post_el_small\\'' segments found".into()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
for video_segment in raw_videos {
|
||||||
|
// url id
|
||||||
|
let url = video_segment
|
||||||
|
.split("/post/")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('\'').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("failed to extract /post/ url".into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let video_url = format!("https://hottub.spacemoehre.de/proxy/sxyprn/post/{}", url);
|
||||||
|
|
||||||
|
// title parts
|
||||||
|
let title_parts = video_segment
|
||||||
|
.split("post_text")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("style=''>").nth(1))
|
||||||
|
.and_then(|s| s.split("</div>").next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("failed to extract title_parts".into()))?;
|
||||||
|
|
||||||
|
let document = Html::parse_document(title_parts);
|
||||||
|
let selector = Selector::parse("*")
|
||||||
|
.map_err(|e| ErrorKind::Parse(format!("selector parse failed: {e}")))?;
|
||||||
|
|
||||||
|
let mut texts = Vec::new();
|
||||||
|
for element in document.select(&selector) {
|
||||||
|
let text = element.text().collect::<Vec<_>>().join(" ");
|
||||||
|
if !text.trim().is_empty() {
|
||||||
|
texts.push(text.trim().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut title = texts.join(" ");
|
||||||
|
title = decode(title.as_bytes())
|
||||||
|
.to_string()
|
||||||
|
.unwrap_or(title)
|
||||||
|
.replace(" ", " ");
|
||||||
|
|
||||||
|
title = title
|
||||||
|
.replace('\n', "")
|
||||||
|
.replace(" + ", " ")
|
||||||
|
.replace(" ", " ")
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if title.to_ascii_lowercase().starts_with("new ") {
|
||||||
|
title = title[4..].to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// id (DON'T index [6])
|
||||||
|
let id = video_url
|
||||||
|
.split('/')
|
||||||
|
.last()
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("failed to extract id from video_url".into()))?
|
||||||
|
.split('?')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// thumb
|
||||||
|
let thumb_path = video_segment
|
||||||
|
.split("<img class='mini_post_vid_thumb lazyload'")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("data-src='").nth(1))
|
||||||
|
.and_then(|s| s.split('\'').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("failed to extract thumb".into()))?;
|
||||||
|
|
||||||
|
let thumb = format!("https:{thumb_path}");
|
||||||
|
|
||||||
|
// preview
|
||||||
|
let preview = if video_segment.contains("class='hvp_player'") {
|
||||||
|
Some(format!(
|
||||||
|
"https:{}",
|
||||||
|
video_segment
|
||||||
|
.split("class='hvp_player'")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split(" src='").nth(1))
|
||||||
|
.and_then(|s| s.split('\'').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("failed to extract preview src".into()))?
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// views
|
||||||
|
let views = video_segment
|
||||||
|
.split("<strong>·</strong> ")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split_whitespace().next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("failed to extract views".into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// duration
|
||||||
|
let raw_duration = video_segment
|
||||||
|
.split("duration_small")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split("title='").nth(1))
|
||||||
|
.and_then(|s| s.split('\'').nth(1))
|
||||||
|
.and_then(|s| s.split('>').nth(1))
|
||||||
|
.and_then(|s| s.split('<').next())
|
||||||
|
.ok_or_else(|| ErrorKind::Parse("failed to extract duration".into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
// stream urls (your filter condition looks suspicious; leaving as-is)
|
||||||
|
let stream_urls = video_segment
|
||||||
|
.split("extlink_icon extlink")
|
||||||
|
.filter_map(|part| {
|
||||||
|
part.split("href='")
|
||||||
|
.last()
|
||||||
|
.and_then(|s| s.split('\'').next())
|
||||||
|
.map(|u| u.to_string())
|
||||||
|
})
|
||||||
|
.filter(|url| url.starts_with("https://lulustream."))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
let video_item_url = stream_urls.first().cloned().unwrap_or_else(|| {
|
||||||
|
format!("https://hottub.spacemoehre.de/proxy/sxyprn/post/{}", id)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_item_url,
|
||||||
|
"sxyprn".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.views(views.parse::<u32>().unwrap_or(0));
|
||||||
|
|
||||||
|
if let Some(p) = preview {
|
||||||
|
video_item = video_item.preview(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for SxyprnProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = per_page;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(
|
||||||
|
cache,
|
||||||
|
pool,
|
||||||
|
page.parse::<u8>().unwrap_or(1),
|
||||||
|
&q,
|
||||||
|
sort,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, pool, page.parse::<u8>().unwrap_or(1), sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
560
src/providers/tnaflix.rs
Normal file
560
src/providers/tnaflix.rs
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::status::*;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
// use std::sync::{Arc, RwLock};
|
||||||
|
// use std::thread;
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TnaflixProvider {
|
||||||
|
url: String,
|
||||||
|
// sites: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
// categories: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
// stars: Arc<RwLock<Vec<FilterOption>>>,
|
||||||
|
}
|
||||||
|
impl TnaflixProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let provider = TnaflixProvider {
|
||||||
|
url: "https://www.tnaflix.com".to_string(),
|
||||||
|
// sites: Arc::new(RwLock::new(vec![FilterOption {
|
||||||
|
// id: "all".to_string(),
|
||||||
|
// title: "All".to_string(),
|
||||||
|
// }])),
|
||||||
|
// categories: Arc::new(RwLock::new(vec![FilterOption {
|
||||||
|
// id: "all".to_string(),
|
||||||
|
// title: "All".to_string(),
|
||||||
|
// }])),
|
||||||
|
// stars: Arc::new(RwLock::new(vec![FilterOption {
|
||||||
|
// id: "all".to_string(),
|
||||||
|
// title: "All".to_string(),
|
||||||
|
// }])),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Kick off the background load but return immediately
|
||||||
|
// provider.spawn_initial_load();
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn spawn_initial_load(&self) {
|
||||||
|
// let url = self.url.clone();
|
||||||
|
// let sites = Arc::clone(&self.sites);
|
||||||
|
// let categories = Arc::clone(&self.categories);
|
||||||
|
// let stars = Arc::clone(&self.stars);
|
||||||
|
|
||||||
|
// thread::spawn(move || {
|
||||||
|
// // Create a tiny runtime just for these async tasks
|
||||||
|
// let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
// .enable_all()
|
||||||
|
// .build()
|
||||||
|
// .expect("build tokio runtime");
|
||||||
|
|
||||||
|
// rt.block_on(async move {
|
||||||
|
// // If you have a streaming sites loader, call it here too
|
||||||
|
// if let Err(e) = Self::load_sites(&url, sites).await {
|
||||||
|
// eprintln!("load_sites_into failed: {e}");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if let Err(e) = Self::load_categories(&url, categories).await {
|
||||||
|
// eprintln!("load_categories failed: {e}");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if let Err(e) = Self::load_stars(&url, stars).await {
|
||||||
|
// eprintln!("load_stars failed: {e}");
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async fn load_stars(base_url: &str, stars: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
// let mut requester = util::requester::Requester::new();
|
||||||
|
// for page in [1..10].into_iter().flatten() {
|
||||||
|
// let text = requester
|
||||||
|
// .get(format!("{}/pornstars?page={}", &base_url, page).as_str())
|
||||||
|
// .await
|
||||||
|
// .unwrap();
|
||||||
|
// if text.contains("404 Not Found") || text.is_empty() {
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// let stars_div = text
|
||||||
|
// .split("<span>Hall of Fame Pornstars</span>")
|
||||||
|
// .collect::<Vec<&str>>()[1]
|
||||||
|
// .split("pagination")
|
||||||
|
// .collect::<Vec<&str>>()[0];
|
||||||
|
// for stars_element in stars_div.split("<a ").collect::<Vec<&str>>()[1..].to_vec() {
|
||||||
|
// let star_url = stars_element.split("href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
// .split("\"")
|
||||||
|
// .collect::<Vec<&str>>()[0];
|
||||||
|
// let star_id = star_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
// let star_name = stars_element.split("title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
// .split("\"")
|
||||||
|
// .collect::<Vec<&str>>()[0]
|
||||||
|
// .to_string();
|
||||||
|
// Self::push_unique(
|
||||||
|
// &stars,
|
||||||
|
// FilterOption {
|
||||||
|
// id: star_id,
|
||||||
|
// title: star_name,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return Ok(());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async fn load_sites(base_url: &str, sites: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
// let mut requester = util::requester::Requester::new();
|
||||||
|
// let mut page = 0;
|
||||||
|
// loop {
|
||||||
|
// page += 1;
|
||||||
|
// let text = requester
|
||||||
|
// .get(format!("{}/sites/{}/", &base_url, page).as_str())
|
||||||
|
// .await
|
||||||
|
// .unwrap();
|
||||||
|
// if text.contains("404 Not Found") || text.is_empty() {
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// let sites_div = text
|
||||||
|
// .split("id=\"list_content_sources_sponsors_list_items\"")
|
||||||
|
// .collect::<Vec<&str>>()[1]
|
||||||
|
// .split("class=\"pagination\"")
|
||||||
|
// .collect::<Vec<&str>>()[0];
|
||||||
|
// for sites_element in
|
||||||
|
// sites_div.split("class=\"headline\"").collect::<Vec<&str>>()[1..].to_vec()
|
||||||
|
// {
|
||||||
|
// let site_url = sites_element.split("href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
// .split("\"")
|
||||||
|
// .collect::<Vec<&str>>()[0];
|
||||||
|
// let site_id = site_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
// let site_name = sites_element.split("<h2>").collect::<Vec<&str>>()[1]
|
||||||
|
// .split("<")
|
||||||
|
// .collect::<Vec<&str>>()[0]
|
||||||
|
// .to_string();
|
||||||
|
// Self::push_unique(
|
||||||
|
// &sites,
|
||||||
|
// FilterOption {
|
||||||
|
// id: site_id,
|
||||||
|
// title: site_name,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return Ok(());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async fn load_networks(base_url: &str, networks: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
|
||||||
|
// let mut requester = util::requester::Requester::new();
|
||||||
|
// let text = requester.get(&base_url).await.unwrap();
|
||||||
|
// let networks_div = text.split("class=\"sites__list\"").collect::<Vec<&str>>()[1]
|
||||||
|
// .split("</div>")
|
||||||
|
// .collect::<Vec<&str>>()[0];
|
||||||
|
// for network_element in
|
||||||
|
// networks_div.split("sites__item").collect::<Vec<&str>>()[1..].to_vec()
|
||||||
|
// {
|
||||||
|
// if network_element.contains("sites__all") {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// let network_url = network_element.split("href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
// .split("\"")
|
||||||
|
// .collect::<Vec<&str>>()[0];
|
||||||
|
// let network_id = network_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
// let network_name = network_element.split(">").collect::<Vec<&str>>()[1]
|
||||||
|
// .split("<")
|
||||||
|
// .collect::<Vec<&str>>()[0]
|
||||||
|
// .to_string();
|
||||||
|
// Self::push_unique(
|
||||||
|
// &networks,
|
||||||
|
// FilterOption {
|
||||||
|
// id: network_id,
|
||||||
|
// title: network_name,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// return Ok(());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Push one item with minimal lock time and dedup by id
|
||||||
|
// fn push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
|
||||||
|
// if let Ok(mut vec) = target.write() {
|
||||||
|
// if !vec.iter().any(|x| x.id == item.id) {
|
||||||
|
// vec.push(item);
|
||||||
|
// // Optional: keep it sorted for nicer UX
|
||||||
|
// // vec.sort_by(|a,b| a.title.cmp(&b.title));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
|
||||||
|
let _ = clientversion;
|
||||||
|
// let sites: Vec<FilterOption> = self
|
||||||
|
// .sites
|
||||||
|
// .read()
|
||||||
|
// .map(|g| g.clone()) // or: .map(|g| g.to_vec())
|
||||||
|
// .unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new())
|
||||||
|
|
||||||
|
// let networks: Vec<FilterOption> = self
|
||||||
|
// .networks
|
||||||
|
// .read()
|
||||||
|
// .map(|g| g.clone()) // or: .map(|g| g.to_vec())
|
||||||
|
// .unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new())
|
||||||
|
|
||||||
|
// let stars: Vec<FilterOption> = self
|
||||||
|
// .stars
|
||||||
|
// .read()
|
||||||
|
// .map(|g| g.clone()) // or: .map(|g| g.to_vec())
|
||||||
|
// .unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new())
|
||||||
|
|
||||||
|
Channel {
|
||||||
|
id: "tnaflix".to_string(),
|
||||||
|
name: "TnAflix".to_string(),
|
||||||
|
description: "Just Tits and Ass".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=www.tnaflix.com".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories: vec![],
|
||||||
|
options: vec![
|
||||||
|
ChannelOption {
|
||||||
|
id: "sort".to_string(),
|
||||||
|
title: "Sort".to_string(),
|
||||||
|
description: "Sort the Videos".to_string(),
|
||||||
|
systemImage: "list.number".to_string(),
|
||||||
|
colorName: "blue".to_string(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "new".into(),
|
||||||
|
title: "New".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "featured".into(),
|
||||||
|
title: "Featured".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "toprated".into(),
|
||||||
|
title: "Top Rated".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
ChannelOption {
|
||||||
|
id: "duration".to_string(),
|
||||||
|
title: "Duration".to_string(),
|
||||||
|
description: "Length of the Videos".to_string(),
|
||||||
|
systemImage: "timer".to_string(),
|
||||||
|
colorName: "green".to_string(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "all".into(),
|
||||||
|
title: "All".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "short".into(),
|
||||||
|
title: "Short (1-3 min)".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "medium".into(),
|
||||||
|
title: "Medium (3-10 min)".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "long".into(),
|
||||||
|
title: "Long (10-30 min)".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "full".into(),
|
||||||
|
title: "Full length (30+ min)".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
// ChannelOption {
|
||||||
|
// id: "sites".to_string(),
|
||||||
|
// title: "Sites".to_string(),
|
||||||
|
// description: "Filter for different Sites".to_string(),
|
||||||
|
// systemImage: "rectangle.stack".to_string(),
|
||||||
|
// colorName: "green".to_string(),
|
||||||
|
// options: sites,
|
||||||
|
// multiSelect: false,
|
||||||
|
// },
|
||||||
|
// ChannelOption {
|
||||||
|
// id: "networks".to_string(),
|
||||||
|
// title: "Networks".to_string(),
|
||||||
|
// description: "Filter for different Networks".to_string(),
|
||||||
|
// systemImage: "list.dash".to_string(),
|
||||||
|
// colorName: "purple".to_string(),
|
||||||
|
// options: networks,
|
||||||
|
// multiSelect: false,
|
||||||
|
// },
|
||||||
|
// ChannelOption {
|
||||||
|
// id: "stars".to_string(),
|
||||||
|
// title: "Stars".to_string(),
|
||||||
|
// description: "Filter for different Pornstars".to_string(),
|
||||||
|
// systemImage: "star.fill".to_string(),
|
||||||
|
// colorName: "yellow".to_string(),
|
||||||
|
// options: stars,
|
||||||
|
// multiSelect: false,
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string: String = match sort {
|
||||||
|
"featured" => "featured".to_string(),
|
||||||
|
"toprated" => "toprated".to_string(),
|
||||||
|
_ => "new".to_string(),
|
||||||
|
};
|
||||||
|
let duration_string: String = match options.duration.unwrap_or("all".to_string()).as_str() {
|
||||||
|
"short" => "short".to_string(),
|
||||||
|
"medium" => "medium".to_string(),
|
||||||
|
"long" => "long".to_string(),
|
||||||
|
"full" => "full".to_string(),
|
||||||
|
_ => "all".to_string(),
|
||||||
|
};
|
||||||
|
// if options.network.is_some()
|
||||||
|
// && !options.network.as_ref().unwrap().is_empty()
|
||||||
|
// && options.network.as_ref().unwrap() != "all"
|
||||||
|
// {
|
||||||
|
// sort_string = format!(
|
||||||
|
// "networks/{}{}",
|
||||||
|
// options.network.as_ref().unwrap(),
|
||||||
|
// alt_sort_string
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// if options.sites.is_some()
|
||||||
|
// && !options.sites.as_ref().unwrap().is_empty()
|
||||||
|
// && options.sites.as_ref().unwrap() != "all"
|
||||||
|
// {
|
||||||
|
// sort_string = format!(
|
||||||
|
// "sites/{}{}",
|
||||||
|
// options.sites.as_ref().unwrap(),
|
||||||
|
// alt_sort_string
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// if options.stars.is_some()
|
||||||
|
// && !options.stars.as_ref().unwrap().is_empty()
|
||||||
|
// && options.stars.as_ref().unwrap() != "all"
|
||||||
|
// {
|
||||||
|
// sort_string = format!(
|
||||||
|
// "models/{}{}",
|
||||||
|
// options.stars.as_ref().unwrap(),
|
||||||
|
// alt_sort_string
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/{}/{}?d={}",
|
||||||
|
self.url, sort_string, page, duration_string
|
||||||
|
);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_string().to_lowercase().trim().replace(" ", "+");
|
||||||
|
let duration_string: String = match options.duration.unwrap_or("all".to_string()).as_str() {
|
||||||
|
"short" => "short".to_string(),
|
||||||
|
"medium" => "medium".to_string(),
|
||||||
|
"long" => "long".to_string(),
|
||||||
|
"full" => "full".to_string(),
|
||||||
|
_ => "all".to_string(),
|
||||||
|
};
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/search?what={}&d={}&page={}",
|
||||||
|
self.url, search_string, duration_string, page
|
||||||
|
);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("pagination ").collect::<Vec<&str>>()[0]
|
||||||
|
.split("row video-list")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("col-xs-6 col-md-4 col-xl-3 mb-3")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = video_segment.split(" href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let mut title = video_segment
|
||||||
|
.split("class=\"video-title text-break\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[5].to_string();
|
||||||
|
|
||||||
|
let thumb = match video_segment.contains("data-src=\""){
|
||||||
|
true => video_segment.split("data-src=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string(),
|
||||||
|
false => video_segment.split("<img src=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string(),
|
||||||
|
};
|
||||||
|
let raw_duration = video_segment
|
||||||
|
.split("thumb-icon video-duration\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32;
|
||||||
|
let views = match video_segment.contains("icon-eye\"></i>") {
|
||||||
|
true => parse_abbreviated_number(
|
||||||
|
video_segment
|
||||||
|
.split("icon-eye\"></i>")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim(),
|
||||||
|
)
|
||||||
|
.unwrap_or(0) as u32,
|
||||||
|
false => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let preview = video_segment
|
||||||
|
.split("data-trailer=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"tnaflix".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.views(views)
|
||||||
|
.preview(preview);
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for TnaflixProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<crate::status::Channel> {
|
||||||
|
Some(self.build_channel(clientversion))
|
||||||
|
}
|
||||||
|
}
|
||||||
284
src/providers/xxdbx.rs
Normal file
284
src/providers/xxdbx.rs
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::api::ClientVersion;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::status::*;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::vec;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_date(s: &str) -> bool {
|
||||||
|
// Regex: strict yyyy-mm-dd (no validation of real calendar dates, just format)
|
||||||
|
let re = Regex::new(r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$").unwrap();
|
||||||
|
re.is_match(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct XxdbxProvider {
|
||||||
|
url: String,
|
||||||
|
stars: Arc<RwLock<Vec<String>>>,
|
||||||
|
channels: Arc<RwLock<Vec<String>>>,
|
||||||
|
}
|
||||||
|
impl XxdbxProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let provider = XxdbxProvider {
|
||||||
|
url: "https://xxdbx.com".to_string(),
|
||||||
|
stars: Arc::new(RwLock::new(vec![])),
|
||||||
|
channels: Arc::new(RwLock::new(vec![])),
|
||||||
|
};
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
|
||||||
|
let _ = clientversion;
|
||||||
|
Channel {
|
||||||
|
id: "xxdbx".to_string(),
|
||||||
|
name: "xxdbx".to_string(),
|
||||||
|
description: "XXX Video Database".to_string(),
|
||||||
|
premium: false,
|
||||||
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=xxdbx.com".to_string(),
|
||||||
|
status: "active".to_string(),
|
||||||
|
categories: vec![],
|
||||||
|
options: vec![
|
||||||
|
ChannelOption {
|
||||||
|
id: "sort".to_string(),
|
||||||
|
title: "Sort".to_string(),
|
||||||
|
description: "Sort the Videos".to_string(),
|
||||||
|
systemImage: "list.number".to_string(),
|
||||||
|
colorName: "blue".to_string(),
|
||||||
|
options: vec![
|
||||||
|
FilterOption {
|
||||||
|
id: "new".into(),
|
||||||
|
title: "New".into(),
|
||||||
|
},
|
||||||
|
FilterOption {
|
||||||
|
id: "popular".into(),
|
||||||
|
title: "Most Popular".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nsfw: true,
|
||||||
|
cacheDuration: Some(1800),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string: String = match sort {
|
||||||
|
"popular" => "most-popular".to_string(),
|
||||||
|
_ => "".to_string(),
|
||||||
|
};
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/{}?page={}",
|
||||||
|
self.url, sort_string, page
|
||||||
|
);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.trim().to_string();
|
||||||
|
let mut search_type = "search";
|
||||||
|
|
||||||
|
if self.channels.read().unwrap().iter().map(|s|s.to_ascii_lowercase()).collect::<Vec<String>>().contains(&search_string.to_ascii_lowercase()) {
|
||||||
|
search_type = "channels";
|
||||||
|
} else if self.stars.read().unwrap().iter().map(|s|s.to_ascii_lowercase()).collect::<Vec<String>>().contains(&search_string.to_ascii_lowercase()) {
|
||||||
|
search_type = "stars";
|
||||||
|
} else if is_valid_date(&search_string){
|
||||||
|
search_type = "dates";
|
||||||
|
}
|
||||||
|
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/{}/{}?page={}",
|
||||||
|
self.url, search_type, search_string, page
|
||||||
|
);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() || html.contains("404 Not Found") {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("</article>").collect::<Vec<&str>>()[0]
|
||||||
|
.split("<div class=\"vids\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<div class=\"v\">")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}\n\n", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = format!("{}{}", self.url, video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string());
|
||||||
|
let mut title = video_segment
|
||||||
|
.split("<div class=\"v_title\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
|
||||||
|
let thumb = format!("https:{}", video_segment.split("<img ").collect::<Vec<&str>>()[1]
|
||||||
|
.split("src=\"").collect::<Vec<&str>>().last().unwrap()
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string());
|
||||||
|
let raw_duration = video_segment
|
||||||
|
.split("<div class=\"v_dur\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let preview = format!("https:{}",video_segment
|
||||||
|
.split("data-preview=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string());
|
||||||
|
let tags = video_segment.split("<div class=\"v_tags\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("</div>").collect::<Vec<&str>>()[0]
|
||||||
|
.split("<a href=\"")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.into_iter().map(|s| s.split("\"").collect::<Vec<&str>>()[0].replace("%20"," ").to_string()).collect::<Vec<String>>();
|
||||||
|
for tag in tags.clone() {
|
||||||
|
let shorted_tag = tag.split("/").collect::<Vec<&str>>()[2].to_string();
|
||||||
|
if tag.contains("channels") && self.channels.read().unwrap().contains(&shorted_tag) == false {
|
||||||
|
self.channels.write().unwrap().push(shorted_tag.clone());
|
||||||
|
}
|
||||||
|
if tag.contains("stars") && self.stars.read().unwrap().contains(&shorted_tag) == false {
|
||||||
|
self.stars.write().unwrap().push(shorted_tag.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"xxdbx".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.tags(tags.into_iter().map(|s| s.split("/").collect::<Vec<&str>>().last().unwrap().to_string()).collect::<Vec<String>>())
|
||||||
|
.preview(preview);
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for XxdbxProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<crate::status::Channel> {
|
||||||
|
Some(self.build_channel(clientversion))
|
||||||
|
}
|
||||||
|
}
|
||||||
215
src/providers/xxthots.rs
Normal file
215
src/providers/xxthots.rs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct XxthotsProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl XxthotsProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
XxthotsProvider {
|
||||||
|
url: "https://xxthots.com".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"popular" => "/most-popular",
|
||||||
|
"top-rated" => "/top-rated",
|
||||||
|
_ => "/latest-updates/",
|
||||||
|
};
|
||||||
|
|
||||||
|
let list_str = match sort {
|
||||||
|
"popular" => "list_videos_common_videos_list",
|
||||||
|
"top-rated" => "list_videos_common_videos_list",
|
||||||
|
_ => "list_videos_most_recent_videos",
|
||||||
|
};
|
||||||
|
let video_url = format!(
|
||||||
|
"{}{}?mode=async^&function=get_block^&block_id={}^&from={}",
|
||||||
|
self.url, sort_string, list_str, page
|
||||||
|
);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let search_string = query.to_lowercase().trim().replace(" ", "-");
|
||||||
|
let video_url = format!(
|
||||||
|
"{}/search/{}/?mode=async&function=get_block&block_id=list_videos_videos_list_search_result&category_ids=&sort_by=&from_videos={}&from_albums={}&",
|
||||||
|
self.url, search_string, page, page
|
||||||
|
);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html
|
||||||
|
.split("<div class=\"pagination\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.split("<div class=\"thumb thumb_rel item \">")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
let video_url: String = video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
let raw_duration = video_segment
|
||||||
|
.split("<div class=\"time\">")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let thumb = video_segment
|
||||||
|
.split("<img class=\"lazy-load")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-original=\"")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let views_part = video_segment
|
||||||
|
.split("svg-icon icon-eye")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("</i>")
|
||||||
|
.collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let views = parse_abbreviated_number(&views_part).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"xxthots".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.views(views);
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for XxthotsProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = options;
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
204
src/providers/youjizz.rs
Normal file
204
src/providers/youjizz.rs
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
use crate::util::parse_abbreviated_number;
|
||||||
|
use crate::DbPool;
|
||||||
|
use crate::providers::Provider;
|
||||||
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::time::parse_time_to_seconds;
|
||||||
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
|
use error_chain::error_chain;
|
||||||
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
|
use std::vec;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
HttpRequest(wreq::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct YoujizzProvider {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
impl YoujizzProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
YoujizzProvider {
|
||||||
|
url: "https://www.youjizz.com".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
sort: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let sort_string = match sort {
|
||||||
|
"popular" => "/most-popular",
|
||||||
|
"top-rated" => "/top-rated",
|
||||||
|
"top-rated-week" => "/top-rated-week",
|
||||||
|
"top-rated-month" => "/top-rated-month",
|
||||||
|
"trending" => "/trending",
|
||||||
|
"random" => "/random",
|
||||||
|
_ => "/newest-clips",
|
||||||
|
};
|
||||||
|
let video_url = format!("{}{}/{}.html", self.url, sort_string, page);
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
items.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
page: u8,
|
||||||
|
query: &str,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Result<Vec<VideoItem>> {
|
||||||
|
let video_url = format!("{}/search/{}-{}.html", self.url, query.to_lowercase().trim(), page);
|
||||||
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
|
let old_items = match cache.get(&video_url) {
|
||||||
|
Some((time, items)) => {
|
||||||
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
|
return Ok(items.clone());
|
||||||
|
} else {
|
||||||
|
let _ = cache.check().await;
|
||||||
|
return Ok(items.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = options.requester.clone().unwrap();
|
||||||
|
|
||||||
|
let text = requester.get(&video_url, None).await.unwrap();
|
||||||
|
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||||
|
if !video_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), video_items.clone());
|
||||||
|
} else {
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
Ok(video_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||||
|
if html.is_empty() {
|
||||||
|
println!("HTML is empty");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut items: Vec<VideoItem> = Vec::new();
|
||||||
|
let raw_videos = html.split("class=\"mobile-only\"").collect::<Vec<&str>>()[0]
|
||||||
|
.split("class=\"default video-item\"")
|
||||||
|
.collect::<Vec<&str>>()[1..]
|
||||||
|
.to_vec();
|
||||||
|
for video_segment in &raw_videos {
|
||||||
|
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||||
|
// for (index, line) in vid.iter().enumerate() {
|
||||||
|
// println!("Line {}: {}", index, line);
|
||||||
|
// }
|
||||||
|
// if video_segment.contains(" src=\"https://cdne-static.cdn1122.com/app/1/images/spacer.gif") {
|
||||||
|
// println!("Skipping video segment due to placeholder thumbnail");
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
let video_url: String = format!("{}{}",self.url, video_segment.split("href=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0].to_string());
|
||||||
|
let mut title = video_segment.split("class=\"video-title\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split(">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
// html decode
|
||||||
|
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||||
|
let id = video_url.split("/").collect::<Vec<&str>>()[4].to_string();
|
||||||
|
|
||||||
|
let thumb = format!("https:{}",video_segment.split("<img ").collect::<Vec<&str>>()[1]
|
||||||
|
.split("data-original=\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string());
|
||||||
|
let raw_duration = video_segment.split("fa fa-clock-o\"></i> ").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string();
|
||||||
|
let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32;
|
||||||
|
let views = parse_abbreviated_number(video_segment.split("format-views\">").collect::<Vec<&str>>()[1]
|
||||||
|
.split("<")
|
||||||
|
.collect::<Vec<&str>>()[0]
|
||||||
|
.to_string().as_str()).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let video_item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
video_url.to_string(),
|
||||||
|
"youjizz".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.views(views)
|
||||||
|
;
|
||||||
|
items.push(video_item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for YoujizzProvider {
|
||||||
|
async fn get_videos(
|
||||||
|
&self,
|
||||||
|
cache: VideoCache,
|
||||||
|
pool: DbPool,
|
||||||
|
sort: String,
|
||||||
|
query: Option<String>,
|
||||||
|
page: String,
|
||||||
|
per_page: String,
|
||||||
|
options: ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = per_page;
|
||||||
|
let _ = pool;
|
||||||
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
Some(q) => {
|
||||||
|
self.query(cache, page.parse::<u8>().unwrap_or(1), &q,options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match videos {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching videos: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/proxies/hanimecdn.rs
Normal file
54
src/proxies/hanimecdn.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use ntex::{
|
||||||
|
http::Response,
|
||||||
|
web::{self, HttpRequest, error},
|
||||||
|
};
|
||||||
|
use ntex::http::header::{CONTENT_LENGTH, CONTENT_TYPE};
|
||||||
|
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
|
||||||
|
pub async fn get_image(
|
||||||
|
req: HttpRequest,
|
||||||
|
requester: web::types::State<Requester>,
|
||||||
|
) -> Result<impl web::Responder, web::Error> {
|
||||||
|
let endpoint = req.match_info().query("endpoint").to_string();
|
||||||
|
let image_url = format!("https://hanime-cdn.com/{}", endpoint);
|
||||||
|
|
||||||
|
let upstream = match requester
|
||||||
|
.get_ref()
|
||||||
|
.clone()
|
||||||
|
.get_raw_with_headers(
|
||||||
|
image_url.as_str(),
|
||||||
|
vec![("Referer".to_string(), "https://hanime.tv/".to_string())],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(_) => return Ok(web::HttpResponse::NotFound().finish()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = upstream.status();
|
||||||
|
let headers = upstream.headers().clone();
|
||||||
|
|
||||||
|
// Read body from upstream
|
||||||
|
let bytes = upstream.bytes().await.map_err(error::ErrorBadGateway)?;
|
||||||
|
|
||||||
|
// Build response and forward headers
|
||||||
|
let mut resp = Response::build(status);
|
||||||
|
|
||||||
|
if let Some(ct) = headers.get(CONTENT_TYPE) {
|
||||||
|
if let Ok(ct_str) = ct.to_str() {
|
||||||
|
resp.set_header(CONTENT_TYPE, ct_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(cl) = headers.get(CONTENT_LENGTH) {
|
||||||
|
if let Ok(cl_str) = cl.to_str() {
|
||||||
|
resp.set_header(CONTENT_LENGTH, cl_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either zero-copy to ntex Bytes...
|
||||||
|
// Ok(resp.body(NtexBytes::from(bytes)))
|
||||||
|
|
||||||
|
// ...or simple & compatible:
|
||||||
|
Ok(resp.body(bytes.to_vec()))
|
||||||
|
}
|
||||||
64
src/proxies/javtiful.rs
Normal file
64
src/proxies/javtiful.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use ntex::web;
|
||||||
|
use wreq::Version;
|
||||||
|
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct JavtifulProxy {
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JavtifulProxy {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
JavtifulProxy {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_video_url(
|
||||||
|
&self,
|
||||||
|
url: String,
|
||||||
|
requester: web::types::State<Requester>,
|
||||||
|
) -> String {
|
||||||
|
let mut requester = requester.get_ref().clone();
|
||||||
|
let url = "https://javtiful.com/".to_string() + &url;
|
||||||
|
let text = requester.get(&url, None).await.unwrap_or("".to_string());
|
||||||
|
if text.is_empty() {
|
||||||
|
return "".to_string();
|
||||||
|
}
|
||||||
|
let video_id = url
|
||||||
|
.split('/')
|
||||||
|
.nth(4)
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let token = text.split("data-csrf-token=\"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split('"').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let form = wreq::multipart::Form::new()
|
||||||
|
.text("video_id", video_id.clone())
|
||||||
|
.text("pid_c", "".to_string())
|
||||||
|
.text("token", token.clone());
|
||||||
|
let resp = match requester
|
||||||
|
.post_multipart(
|
||||||
|
"https://javtiful.com/ajax/get_cdn",
|
||||||
|
form,
|
||||||
|
vec![("Referer".to_string(), url.to_string())],
|
||||||
|
Some(Version::HTTP_11),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return "".to_string(),
|
||||||
|
};
|
||||||
|
let text = resp.text().await.unwrap_or_default();
|
||||||
|
let json: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
|
||||||
|
let video_url = json.get("playlists")
|
||||||
|
.map(|v| v.to_string().replace("\"", ""))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
return video_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/proxies/mod.rs
Normal file
35
src/proxies/mod.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use ntex::web;
|
||||||
|
|
||||||
|
use crate::{proxies::sxyprn::SxyprnProxy, util::requester::Requester};
|
||||||
|
|
||||||
|
pub mod sxyprn;
|
||||||
|
pub mod hanimecdn;
|
||||||
|
pub mod javtiful;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum AnyProxy {
|
||||||
|
Sxyprn(SxyprnProxy),
|
||||||
|
Javtiful(javtiful::JavtifulProxy),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Proxy {
|
||||||
|
async fn get_video_url(
|
||||||
|
&self,
|
||||||
|
url: String,
|
||||||
|
requester: web::types::State<Requester>,
|
||||||
|
) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Proxy for AnyProxy {
|
||||||
|
async fn get_video_url(
|
||||||
|
&self,
|
||||||
|
url: String,
|
||||||
|
requester: web::types::State<Requester>,
|
||||||
|
) -> String {
|
||||||
|
match self {
|
||||||
|
AnyProxy::Sxyprn(p) => p.get_video_url(url, requester).await,
|
||||||
|
AnyProxy::Javtiful(p) => p.get_video_url(url, requester).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/proxies/sxyprn.rs
Normal file
78
src/proxies/sxyprn.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use ntex::web;
|
||||||
|
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
|
||||||
|
/// Extracts digits from a string and sums them.
|
||||||
|
fn ssut51(arg: &str) -> u32 {
|
||||||
|
arg.chars()
|
||||||
|
.filter(|c| c.is_ascii_digit())
|
||||||
|
.map(|c| c.to_digit(10).unwrap())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes a token: "<sum1>-<host>-<sum2>" using Base64 URL-safe variant.
|
||||||
|
fn boo(sum1: u32, sum2: u32) -> String {
|
||||||
|
let raw = format!("{}-{}-{}", sum1, "sxyprn.com", sum2);
|
||||||
|
let encoded = general_purpose::STANDARD.encode(raw);
|
||||||
|
|
||||||
|
// Replace + → -, / → _, = → .
|
||||||
|
encoded
|
||||||
|
.replace('+', "-")
|
||||||
|
.replace('/', "_")
|
||||||
|
.replace('=', ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SxyprnProxy {
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SxyprnProxy {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
SxyprnProxy {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_video_url(
|
||||||
|
&self,
|
||||||
|
url: String,
|
||||||
|
requester: web::types::State<Requester>,
|
||||||
|
) -> String {
|
||||||
|
let mut requester = requester.get_ref().clone();
|
||||||
|
let url = "https://sxyprn.com/".to_string() + &url;
|
||||||
|
let text = requester.get(&url, None).await.unwrap_or("".to_string());
|
||||||
|
if text.is_empty() {
|
||||||
|
return "".to_string();
|
||||||
|
}
|
||||||
|
let data_string = text.split("data-vnfo='").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\":\"").collect::<Vec<&str>>()[1]
|
||||||
|
.split("\"}").collect::<Vec<&str>>()[0].replace("\\","");
|
||||||
|
//println!("src: {}",data_string);
|
||||||
|
let mut tmp = data_string
|
||||||
|
.split("/")
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
//println!("tmp: {:?}",tmp);
|
||||||
|
tmp[1] = format!("{}8/{}", tmp[1], boo(ssut51(tmp[6].as_str()), ssut51(tmp[7].as_str())));
|
||||||
|
|
||||||
|
//println!("tmp[1]: {:?}",tmp[1]);
|
||||||
|
//preda
|
||||||
|
tmp[5] = format!(
|
||||||
|
"{}",
|
||||||
|
tmp[5].parse::<u32>().unwrap() - ssut51(tmp[6].as_str()) - ssut51(tmp[7].as_str())
|
||||||
|
);
|
||||||
|
//println!("tmp: {:?}",tmp);
|
||||||
|
let sxyprn_video_url = format!("https://sxyprn.com{}",tmp.join("/"));
|
||||||
|
|
||||||
|
let response = requester.get_raw(&sxyprn_video_url).await;
|
||||||
|
match response {
|
||||||
|
Ok(resp) => {
|
||||||
|
return format!("https:{}", resp.headers().get("Location").unwrap().to_str().unwrap_or("").to_string());
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error fetching video URL: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/proxy.rs
Normal file
47
src/proxy.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use ntex::web::{self, HttpRequest};
|
||||||
|
|
||||||
|
use crate::proxies::javtiful::JavtifulProxy;
|
||||||
|
use crate::proxies::sxyprn::SxyprnProxy;
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
use crate::proxies::*;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(
|
||||||
|
web::resource("/sxyprn/{endpoint}*")
|
||||||
|
.route(web::post().to(proxy2redirect))
|
||||||
|
.route(web::get().to(proxy2redirect)),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/javtiful/{endpoint}*")
|
||||||
|
.route(web::post().to(proxy2redirect))
|
||||||
|
.route(web::get().to(proxy2redirect)),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/hanime-cdn/{endpoint}*")
|
||||||
|
.route(web::post().to(crate::proxies::hanimecdn::get_image))
|
||||||
|
.route(web::get().to(crate::proxies::hanimecdn::get_image)),
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn proxy2redirect(req: HttpRequest,
|
||||||
|
requester: web::types::State<Requester>,) -> Result<impl web::Responder, web::Error> {
|
||||||
|
let proxy = get_proxy(req.uri().to_string().split("/").collect::<Vec<&str>>()[2]).unwrap();
|
||||||
|
let endpoint = req.match_info().query("endpoint").to_string();
|
||||||
|
let video_url = match proxy.get_video_url(endpoint, requester).await{
|
||||||
|
url if url != "" => url,
|
||||||
|
_ => "Error".to_string(),
|
||||||
|
};
|
||||||
|
Ok(web::HttpResponse::Found()
|
||||||
|
.header("Location", video_url)
|
||||||
|
.finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_proxy(proxy: &str) -> Option<AnyProxy> {
|
||||||
|
match proxy {
|
||||||
|
"sxyprn" => Some(AnyProxy::Sxyprn(SxyprnProxy::new())),
|
||||||
|
"javtiful" => Some(AnyProxy::Javtiful(JavtifulProxy::new())),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ pub struct Channel {
|
|||||||
pub categories: Vec<String>, //[],
|
pub categories: Vec<String>, //[],
|
||||||
pub options: Vec<ChannelOption>,
|
pub options: Vec<ChannelOption>,
|
||||||
pub nsfw: bool, //true
|
pub nsfw: bool, //true
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cacheDuration: Option<u32>, //Some(86400)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@@ -31,7 +33,7 @@ pub struct ChannelOption {
|
|||||||
pub multiSelect: bool, //true
|
pub multiSelect: bool, //true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize, Debug, Clone)]
|
||||||
pub struct FilterOption{
|
pub struct FilterOption{
|
||||||
pub id: String, //"sort",
|
pub id: String, //"sort",
|
||||||
pub title: String, //"Sort",
|
pub title: String, //"Sort",
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
use std::time::{SystemTime};
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
|
||||||
use crate::videos::VideoItem;
|
use crate::videos::VideoItem;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct VideoCache{
|
pub struct VideoCache {
|
||||||
cache: Arc<Mutex<std::collections::HashMap<String, (SystemTime, Vec<VideoItem>)>>>, // url -> time+Items
|
cache: Arc<Mutex<std::collections::HashMap<String, (SystemTime, Vec<VideoItem>)>>>, // url -> time+Items
|
||||||
|
max_size: usize,
|
||||||
}
|
}
|
||||||
impl VideoCache {
|
impl VideoCache {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
VideoCache {
|
VideoCache {
|
||||||
cache: Arc::new(Mutex::new(std::collections::HashMap::new())),
|
cache: Arc::new(Mutex::new(std::collections::HashMap::new())),
|
||||||
|
max_size: 100,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn max_size(&mut self, size: usize) -> &mut Self {
|
||||||
|
self.max_size = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self, key: &str) -> Option<(SystemTime, Vec<VideoItem>)> {
|
pub fn get(&self, key: &str) -> Option<(SystemTime, Vec<VideoItem>)> {
|
||||||
let cache = self.cache.lock().ok()?;
|
let cache = self.cache.lock().ok()?;
|
||||||
cache.get(key).cloned()
|
cache.get(key).cloned()
|
||||||
@@ -24,14 +30,22 @@ impl VideoCache {
|
|||||||
|
|
||||||
pub fn insert(&self, key: String, value: Vec<VideoItem>) {
|
pub fn insert(&self, key: String, value: Vec<VideoItem>) {
|
||||||
if let Ok(mut cache) = self.cache.lock() {
|
if let Ok(mut cache) = self.cache.lock() {
|
||||||
|
if cache.len() >= self.max_size {
|
||||||
|
// Simple eviction policy: remove a random entry
|
||||||
|
if let Some(first_key) = cache.keys().next().cloned() {
|
||||||
|
cache.remove(&first_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
cache.insert(key.clone(), (SystemTime::now(), value.clone()));
|
cache.insert(key.clone(), (SystemTime::now(), value.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&self, key: &str) {
|
pub fn remove(&self, key: &str) {
|
||||||
if let Ok(mut cache) = self.cache.lock() {
|
if let Ok(mut cache) = self.cache.lock() {
|
||||||
cache.remove(key);
|
cache.remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entries(&self) -> Option<Vec<(String, (SystemTime, Vec<VideoItem>))>> {
|
pub fn entries(&self) -> Option<Vec<(String, (SystemTime, Vec<VideoItem>))>> {
|
||||||
if let Ok(cache) = self.cache.lock() {
|
if let Ok(cache) = self.cache.lock() {
|
||||||
// Return a cloned vector of the cache entries
|
// Return a cloned vector of the cache entries
|
||||||
@@ -40,21 +54,24 @@ impl VideoCache {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check(&self) -> Result<(), Box<dyn std::error::Error>>{
|
pub async fn check(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let iter = match self.entries(){
|
let iter = match self.entries() {
|
||||||
Some(iter) => iter,
|
Some(iter) => iter,
|
||||||
None => return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Could not get entries")))
|
None => {
|
||||||
|
return Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
"Could not get entries",
|
||||||
|
)));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (key, (time, _items)) in iter {
|
for (key, (time, _items)) in iter {
|
||||||
if let Ok(elapsed) = time.elapsed() {
|
if let Ok(elapsed) = time.elapsed() {
|
||||||
if elapsed > Duration::from_secs(60*60){
|
if elapsed > Duration::from_secs(60 * 60) {
|
||||||
println!("Key: {}, elapsed: {:?}", key, elapsed);
|
|
||||||
self.remove(&key);
|
self.remove(&key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
107
src/util/discord.rs
Normal file
107
src/util/discord.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use serde_json::json;
|
||||||
|
use crate::util::requester;
|
||||||
|
|
||||||
|
// Global cache: Map<ErrorSignature, LastSentTimestamp>
|
||||||
|
static ERROR_CACHE: Lazy<DashMap<String, u64>> = Lazy::new(DashMap::new);
|
||||||
|
// const COOLDOWN_SECONDS: u64 = 3600; // 1 Hour cooldown
|
||||||
|
|
||||||
|
pub fn format_error_chain(err: &dyn Error) -> String {
|
||||||
|
let mut chain_str = String::new();
|
||||||
|
let mut current_err: Option<&dyn Error> = Some(err);
|
||||||
|
let mut index = 1;
|
||||||
|
while let Some(e) = current_err {
|
||||||
|
let _ = writeln!(chain_str, "{}. {}", index, e);
|
||||||
|
current_err = e.source();
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
chain_str
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_discord_error_report(
|
||||||
|
error_msg: String,
|
||||||
|
error_chain: Option<String>,
|
||||||
|
context: Option<&str>,
|
||||||
|
extra_info: Option<&str>,
|
||||||
|
file: &str,
|
||||||
|
line: u32,
|
||||||
|
module: &str,
|
||||||
|
) {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// --- Deduplication Logic ---
|
||||||
|
// Create a unique key based on error content and location
|
||||||
|
let error_signature = format!("{}-{}-{}", error_msg, file, line);
|
||||||
|
|
||||||
|
if let Some(_) = ERROR_CACHE.get(&error_signature) {
|
||||||
|
// if now - *last_sent < COOLDOWN_SECONDS {
|
||||||
|
// Error is still in cooldown, skip sending
|
||||||
|
return;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the cache with the current timestamp
|
||||||
|
ERROR_CACHE.insert(error_signature, now);
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
let webhook_url = match std::env::var("DISCORD_WEBHOOK") {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_FIELD: usize = 1024;
|
||||||
|
let truncate = |s: &str| {
|
||||||
|
if s.len() > MAX_FIELD {
|
||||||
|
format!("{}…", &s[..MAX_FIELD - 1])
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let payload = json!({
|
||||||
|
"embeds": [{
|
||||||
|
"title": "🚨 Rust Error Report",
|
||||||
|
"color": 0xE74C3C,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Error",
|
||||||
|
"value": format!("```{}```", truncate(&error_msg)),
|
||||||
|
"inline": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Error Chain",
|
||||||
|
"value": truncate(&error_chain.unwrap_or_else(|| "No chain provided".to_string())),
|
||||||
|
"inline": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Location",
|
||||||
|
"value": format!("`{}`:{}\n`{}`", file, line, module),
|
||||||
|
"inline": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Context",
|
||||||
|
"value": truncate(context.unwrap_or("n/a")),
|
||||||
|
"inline": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Extra Info",
|
||||||
|
"value": truncate(extra_info.unwrap_or("n/a")),
|
||||||
|
"inline": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer": {
|
||||||
|
"text": format!("Unix time: {} | Cooldown active", now)
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut requester = requester::Requester::new();
|
||||||
|
let _ = requester.post_json(&webhook_url, &payload, vec![]).await;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, env};
|
||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use wreq::Client;
|
use wreq::{Client, Proxy};
|
||||||
use wreq_util::Emulation;
|
use wreq_util::Emulation;
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
@@ -13,67 +13,60 @@ pub struct FlareSolverrRequest {
|
|||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
pub struct FlaresolverrCookie {
|
pub struct FlaresolverrCookie {
|
||||||
name: String, //"cf_clearance",
|
pub name: String,
|
||||||
value: String, //"lnKoXclrIp_mDrWJFfktPGm8GDyxjSpzy9dx0qDTiRg-1748689259-1.2.1.1-AIFERAPCdCSvvdu1mposNdUpKV9wHZXBpSI2L9k9TaKkPcqmomON_XEb6ZtRBtrmQu_DC8AzKllRg2vNzVKOUsvv9ndjQ.vv8Z7cNkgzpIbGFy96kXyAYH2mUk3Q7enZovDlEbK5kpV3Sbmd2M3_bUCBE1WjAMMdXlyNElH1LOpUm149O9hrluXjAffo4SwHI4HO0UckBPWBlBqhznKPgXxU0g8VHLDeYnQKViY8rP2ud4tyzKnJUxuYXzr4aWBNMp6TESp49vesRiel_Y5m.rlTY4zSb517S9iPbEQiYHRI.uH5mMHVI3jvJl0Mx94tPrpFnkhDdmzL3DRSllJe9k786Lf21I9WBoH2cCR3yHw",
|
pub value: String,
|
||||||
domain: String, //".discord.com",
|
pub domain: String,
|
||||||
path: String, //"/",
|
pub path: String,
|
||||||
expires: f64, //1780225259.237105,
|
pub expires: f64,
|
||||||
size: u64, //438,
|
pub size: u64,
|
||||||
httpOnly: bool, //true,
|
pub httpOnly: bool,
|
||||||
secure: bool, //true,
|
pub secure: bool,
|
||||||
session: bool, //false,
|
pub session: bool,
|
||||||
sameSite: Option<String>, //"None",
|
pub sameSite: Option<String>,
|
||||||
priority: String, //"Medium",
|
pub priority: String,
|
||||||
sameParty: bool, //false,
|
pub sameParty: bool,
|
||||||
sourceScheme: String, //"Secure",
|
pub sourceScheme: String,
|
||||||
sourcePort: u32, //443,
|
pub sourcePort: u32,
|
||||||
partitionKey: Option<String>, //"https://perverzija.com"
|
pub partitionKey: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
pub struct FlareSolverrSolution {
|
pub struct FlareSolverrSolution {
|
||||||
url: String,
|
pub url: String,
|
||||||
status: u32,
|
pub status: u32,
|
||||||
pub response: String,
|
pub response: String,
|
||||||
headers: HashMap<String, String>,
|
pub headers: HashMap<String, String>,
|
||||||
cookies: Vec<FlaresolverrCookie>,
|
pub cookies: Vec<FlaresolverrCookie>,
|
||||||
userAgent: String,
|
pub userAgent: String,
|
||||||
}
|
}
|
||||||
// impl FlareSolverrSolution {
|
|
||||||
// fn to_client(&self,){
|
|
||||||
// let mut headers = header::HeaderMap::new();
|
|
||||||
// for (h, v) in &self.headers {
|
|
||||||
// println!("{}: {}", h, v);
|
|
||||||
// headers.insert(
|
|
||||||
// header::HeaderName::from_bytes(h.as_bytes()).unwrap(),
|
|
||||||
// header::HeaderValue::from_str(v).unwrap(),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// // let client = reqwest::Client::builder()
|
|
||||||
// // .danger_accept_invalid_certs(true)
|
|
||||||
// // .
|
|
||||||
// // .build().unwrap();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
pub struct FlareSolverrResponse {
|
pub struct FlareSolverrResponse {
|
||||||
status: String,
|
pub status: String,
|
||||||
message: String,
|
pub message: String,
|
||||||
pub solution: FlareSolverrSolution,
|
pub solution: FlareSolverrSolution,
|
||||||
startTimestamp: u64,
|
pub startTimestamp: u64,
|
||||||
endTimestamp: u64,
|
pub endTimestamp: u64,
|
||||||
version: String,
|
pub version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Flaresolverr {
|
pub struct Flaresolverr {
|
||||||
url: String
|
url: String,
|
||||||
|
proxy: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Flaresolverr {
|
impl Flaresolverr {
|
||||||
pub fn new(url: String) -> Self {
|
pub fn new(url: String) -> Self {
|
||||||
Flaresolverr {
|
Self {
|
||||||
url: url
|
url,
|
||||||
|
proxy: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_proxy(&mut self, proxy: bool) {
|
||||||
|
self.proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn solve(
|
pub async fn solve(
|
||||||
&self,
|
&self,
|
||||||
request: FlareSolverrRequest,
|
request: FlareSolverrRequest,
|
||||||
@@ -82,17 +75,31 @@ impl Flaresolverr {
|
|||||||
.emulation(Emulation::Firefox136)
|
.emulation(Emulation::Firefox136)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let response = client
|
let mut req = client
|
||||||
.post(&self.url)
|
.post(&self.url)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
"cmd": request.cmd,
|
"cmd": request.cmd,
|
||||||
"url": request.url,
|
"url": request.url,
|
||||||
"maxTimeout": request.maxTimeout,
|
"maxTimeout": request.maxTimeout,
|
||||||
}))
|
}));
|
||||||
.send().await?;
|
|
||||||
|
|
||||||
let body: FlareSolverrResponse = response.json::<FlareSolverrResponse>().await?;
|
if self.proxy {
|
||||||
|
if let Ok(proxy_url) = env::var("BURP_URL") {
|
||||||
|
match Proxy::all(&proxy_url) {
|
||||||
|
Ok(proxy) => {
|
||||||
|
req = req.proxy(proxy);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Invalid proxy URL '{}': {}", proxy_url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = req.send().await?;
|
||||||
|
|
||||||
|
let body = response.json::<FlareSolverrResponse>().await?;
|
||||||
Ok(body)
|
Ok(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
pub mod time;
|
pub mod time;
|
||||||
pub mod flaresolverr;
|
pub mod flaresolverr;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
pub mod requester;
|
||||||
|
pub mod discord;
|
||||||
|
|
||||||
pub fn parse_abbreviated_number(s: &str) -> Option<u32> {
|
pub fn parse_abbreviated_number(s: &str) -> Option<u32> {
|
||||||
let s = s.trim();
|
let s = s.trim();
|
||||||
|
|||||||
269
src/util/requester.rs
Normal file
269
src/util/requester.rs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use wreq::multipart::Form;
|
||||||
|
use std::env;
|
||||||
|
use wreq::Client;
|
||||||
|
use wreq::Proxy;
|
||||||
|
use wreq::Response;
|
||||||
|
use wreq::Version;
|
||||||
|
use wreq::header::HeaderValue;
|
||||||
|
use wreq::redirect::Policy;
|
||||||
|
use wreq_util::Emulation;
|
||||||
|
|
||||||
|
use crate::util::flaresolverr::FlareSolverrRequest;
|
||||||
|
use crate::util::flaresolverr::Flaresolverr;
|
||||||
|
|
||||||
|
// A Send + Sync error type for all async paths
|
||||||
|
type AnyErr = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
|
pub struct Requester {
|
||||||
|
#[serde(skip)]
|
||||||
|
client: Client,
|
||||||
|
proxy: bool,
|
||||||
|
flaresolverr_session: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Requester {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let client = Client::builder()
|
||||||
|
.cert_verification(false)
|
||||||
|
.emulation(Emulation::Firefox136)
|
||||||
|
.cookie_store(true)
|
||||||
|
.redirect(Policy::default())
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
|
Requester {
|
||||||
|
client,
|
||||||
|
proxy: false,
|
||||||
|
flaresolverr_session: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_proxy(&mut self, proxy: bool) {
|
||||||
|
if proxy {
|
||||||
|
println!("Proxy enabled");
|
||||||
|
}
|
||||||
|
self.proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_raw(&mut self, url: &str) -> Result<Response, wreq::Error> {
|
||||||
|
let client = Client::builder()
|
||||||
|
.cert_verification(false)
|
||||||
|
.emulation(Emulation::Firefox136)
|
||||||
|
.cookie_store(true)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
|
let mut request = client.get(url).version(Version::HTTP_11);
|
||||||
|
|
||||||
|
if self.proxy {
|
||||||
|
if let Ok(proxy_url) = env::var("BURP_URL") {
|
||||||
|
let proxy = Proxy::all(&proxy_url).unwrap();
|
||||||
|
request = request.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.send().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_raw_with_headers(
|
||||||
|
&mut self,
|
||||||
|
url: &str,
|
||||||
|
headers: Vec<(String, String)>,
|
||||||
|
) -> Result<Response, wreq::Error> {
|
||||||
|
let client = Client::builder()
|
||||||
|
.cert_verification(false)
|
||||||
|
.emulation(Emulation::Firefox136)
|
||||||
|
.cookie_store(true)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
|
let mut request = client.get(url).version(Version::HTTP_11);
|
||||||
|
|
||||||
|
if self.proxy {
|
||||||
|
if let Ok(proxy_url) = env::var("BURP_URL") {
|
||||||
|
let proxy = Proxy::all(&proxy_url).unwrap();
|
||||||
|
request = request.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set custom headers
|
||||||
|
for (key, value) in headers.iter() {
|
||||||
|
request = request.header(key, value);
|
||||||
|
}
|
||||||
|
request.send().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post_json<S>(
|
||||||
|
&mut self,
|
||||||
|
url: &str,
|
||||||
|
data: &S,
|
||||||
|
headers: Vec<(String, String)>,
|
||||||
|
) -> Result<Response, wreq::Error>
|
||||||
|
where
|
||||||
|
S: Serialize + ?Sized,
|
||||||
|
{
|
||||||
|
let mut request = self.client.post(url).version(Version::HTTP_11).json(data);
|
||||||
|
|
||||||
|
// Set custom headers
|
||||||
|
for (key, value) in headers.iter() {
|
||||||
|
request = request.header(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.proxy {
|
||||||
|
if let Ok(proxy_url) = env::var("BURP_URL") {
|
||||||
|
let proxy = Proxy::all(&proxy_url).unwrap();
|
||||||
|
request = request.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.send().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post(
|
||||||
|
&mut self,
|
||||||
|
url: &str,
|
||||||
|
data: &str,
|
||||||
|
headers: Vec<(&str, &str)>,
|
||||||
|
) -> Result<Response, wreq::Error> {
|
||||||
|
let mut request = self.client.post(url).version(Version::HTTP_11).body(data.to_string());
|
||||||
|
|
||||||
|
// Set custom headers
|
||||||
|
for (key, value) in headers.iter() {
|
||||||
|
request = request.header(key.to_string(), value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.proxy {
|
||||||
|
if let Ok(proxy_url) = env::var("BURP_URL") {
|
||||||
|
let proxy = Proxy::all(&proxy_url).unwrap();
|
||||||
|
request = request.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.send().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post_multipart(
|
||||||
|
&mut self,
|
||||||
|
url: &str,
|
||||||
|
form: Form,
|
||||||
|
headers: Vec<(String, String)>,
|
||||||
|
_http_version: Option<Version>,
|
||||||
|
) -> Result<Response, wreq::Error>
|
||||||
|
{
|
||||||
|
let http_version = match _http_version {
|
||||||
|
Some(v) => v,
|
||||||
|
None => Version::HTTP_11,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut request = self.client.post(url).multipart(form).version(http_version);
|
||||||
|
|
||||||
|
// Set custom headers
|
||||||
|
for (key, value) in headers.iter() {
|
||||||
|
request = request.header(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.proxy {
|
||||||
|
if let Ok(proxy_url) = env::var("BURP_URL") {
|
||||||
|
let proxy = Proxy::all(&proxy_url).unwrap();
|
||||||
|
request = request.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.send().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get(&mut self, url: &str, _http_version: Option<Version>) -> Result<String, AnyErr> {
|
||||||
|
let http_version = match _http_version {
|
||||||
|
Some(v) => v,
|
||||||
|
None => Version::HTTP_11,
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let mut request = self.client.get(url).version(http_version);
|
||||||
|
if self.proxy {
|
||||||
|
if let Ok(proxy_url) = env::var("BURP_URL") {
|
||||||
|
let proxy = Proxy::all(&proxy_url).unwrap();
|
||||||
|
request = request.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let response = request.send().await?;
|
||||||
|
if response.status().is_success() || response.status().as_u16() == 404 {
|
||||||
|
return Ok(response.text().await?);
|
||||||
|
}
|
||||||
|
if response.status().as_u16() == 429 {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"Direct request to {} failed with status: {}",
|
||||||
|
url,
|
||||||
|
response.status()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// If direct request failed, try FlareSolverr. Map its error to a Send+Sync error immediately,
|
||||||
|
// so no non-Send error value lives across later `.await`s.
|
||||||
|
let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set");
|
||||||
|
let mut flare = Flaresolverr::new(flare_url);
|
||||||
|
if self.proxy && env::var("BURP_URL").is_ok() {
|
||||||
|
flare.set_proxy(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = flare
|
||||||
|
.solve(FlareSolverrRequest {
|
||||||
|
cmd: "request.get".to_string(),
|
||||||
|
url: url.to_string(),
|
||||||
|
maxTimeout: 60000,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| -> AnyErr { format!("Failed to solve FlareSolverr: {e}").into() })?;
|
||||||
|
|
||||||
|
// Rebuild client and apply UA/cookies from FlareSolverr
|
||||||
|
let cookie_origin = url.split('/').take(3).collect::<Vec<&str>>().join("/");
|
||||||
|
|
||||||
|
self.client = Client::builder()
|
||||||
|
.cert_verification(false)
|
||||||
|
.emulation(Emulation::Firefox136)
|
||||||
|
.cookie_store(true)
|
||||||
|
.redirect(Policy::default())
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
|
let useragent = res.solution.userAgent;
|
||||||
|
self.client
|
||||||
|
.update()
|
||||||
|
.headers(|headers| {
|
||||||
|
headers.insert("User-Agent", HeaderValue::from_str(&useragent).unwrap());
|
||||||
|
})
|
||||||
|
.apply()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let Ok(origin) = url::Url::parse(&cookie_origin) {
|
||||||
|
for cookie in res.solution.cookies {
|
||||||
|
let header =
|
||||||
|
HeaderValue::from_str(&format!("{}={}", cookie.name, cookie.value)).unwrap();
|
||||||
|
self.client.set_cookie(&origin, header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry the original URL with the updated client & (optional) proxy
|
||||||
|
let mut request = self.client.get(url).version(Version::HTTP_11);
|
||||||
|
if self.proxy {
|
||||||
|
if let Ok(proxy_url) = env::var("BURP_URL") {
|
||||||
|
let proxy = Proxy::all(&proxy_url).unwrap();
|
||||||
|
request = request.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = request.send().await?;
|
||||||
|
if response.status().is_success() {
|
||||||
|
return Ok(response.text().await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to FlareSolverr-provided body
|
||||||
|
Ok(res.solution.response)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
pub fn parse_time_to_seconds(s: &str) -> Option<i64> {
|
pub fn parse_time_to_seconds(s: &str) -> Option<i64> {
|
||||||
let parts: Vec<_> = s.split(':').collect();
|
let parts: Vec<_> = s.trim().split(':').collect();
|
||||||
match parts.len() {
|
match parts.len() {
|
||||||
2 => {
|
2 => {
|
||||||
// MM:SS
|
// MM:SS
|
||||||
|
|||||||
187
src/videos.rs
187
src/videos.rs
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
pub struct VideosRequest {
|
pub struct VideosRequest {
|
||||||
@@ -28,13 +28,27 @@ pub struct VideosRequest {
|
|||||||
pub featured: Option<String>, // "featured",
|
pub featured: Option<String>, // "featured",
|
||||||
pub category: Option<String>, // "pmv"
|
pub category: Option<String>, // "pmv"
|
||||||
pub sites: Option<String>, //
|
pub sites: Option<String>, //
|
||||||
|
pub filter: Option<String>, //
|
||||||
|
pub language: Option<String>, //
|
||||||
|
pub networks: Option<String>, //
|
||||||
|
pub stars: Option<String>, //
|
||||||
|
pub categories: Option<String>,
|
||||||
|
pub duration: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
pub struct ServerOptions{
|
pub struct ServerOptions {
|
||||||
pub featured: Option<String>, // "featured",
|
pub featured: Option<String>, // "featured",
|
||||||
pub category: Option<String>, // "pmv"
|
pub category: Option<String>, // "pmv"
|
||||||
pub sites: Option<String>, //
|
pub sites: Option<String>, //
|
||||||
|
pub filter: Option<String>,
|
||||||
|
pub language: Option<String>, // "en"
|
||||||
|
pub requester: Option<Requester>,
|
||||||
|
pub network: Option<String>, //
|
||||||
|
pub stars: Option<String>, //
|
||||||
|
pub categories: Option<String>, //
|
||||||
|
pub duration: Option<String>, //
|
||||||
|
pub sort: Option<String>, //
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, Debug)]
|
#[derive(serde::Serialize, Debug)]
|
||||||
@@ -43,38 +57,46 @@ pub struct PageInfo {
|
|||||||
pub resultsPerPage: u32, // 10
|
pub resultsPerPage: u32, // 10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
#[derive(serde::Serialize, Debug, Clone)]
|
pub struct VideoEmbed {
|
||||||
pub struct VideoEmbed{
|
|
||||||
pub html: String,
|
pub html: String,
|
||||||
pub source: String,
|
pub source: String,
|
||||||
}
|
}
|
||||||
impl VideoEmbed {
|
impl VideoEmbed {
|
||||||
pub fn new(html: String, source: String) -> Self {
|
pub fn new(html: String, source: String) -> Self {
|
||||||
VideoEmbed {
|
VideoEmbed { html, source }
|
||||||
html,
|
|
||||||
source,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(serde::Serialize, Debug, Clone)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
pub struct VideoItem {
|
pub struct VideoItem {
|
||||||
pub duration: u32, // 110,
|
pub duration: u32, // 110,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub views: Option<u32>, // 14622653,
|
pub views: Option<u32>, // 14622653,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub rating: Option<f32>, // 0.0,
|
pub rating: Option<f32>, // 0.0,
|
||||||
pub id: String, // "c85017ca87477168d648727753c4ded8a35f173e22ef93743e707b296becb299",
|
pub id: String, // "c85017ca87477168d648727753c4ded8a35f173e22ef93743e707b296becb299",
|
||||||
pub title: String, // "20 Minutes of Adorable Kittens BEST Compilation",
|
pub title: String, // "20 Minutes of Adorable Kittens BEST Compilation",
|
||||||
pub url: String, // "https://www.youtube.com/watch?v=y0sF5xhGreA",
|
pub url: String, // "https://www.youtube.com/watch?v=y0sF5xhGreA",
|
||||||
pub channel: String, // "youtube",
|
pub channel: String, // "youtube",
|
||||||
pub thumb: String, // "https://i.ytimg.com/vi/y0sF5xhGreA/hqdefault.jpg",
|
pub thumb: String, // "https://i.ytimg.com/vi/y0sF5xhGreA/hqdefault.jpg",
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub uploader: Option<String>, // "The Pet Collective",
|
pub uploader: Option<String>, // "The Pet Collective",
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub uploaderUrl: Option<String>, // "https://www.youtube.com/@petcollective",
|
pub uploaderUrl: Option<String>, // "https://www.youtube.com/@petcollective",
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub verified: Option<bool>, // false,
|
pub verified: Option<bool>, // false,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub tags: Option<Vec<String>>, // [],
|
pub tags: Option<Vec<String>>, // [],
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub uploadedAt: Option<u64>, // 1741142954
|
pub uploadedAt: Option<u64>, // 1741142954
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub formats: Option<Vec<VideoFormat>>, // Additional HTTP headers if needed
|
pub formats: Option<Vec<VideoFormat>>, // Additional HTTP headers if needed
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub embed: Option<VideoEmbed>, // Optional embed information
|
pub embed: Option<VideoEmbed>, // Optional embed information
|
||||||
pub preview: Option<String>
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub preview: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub aspectRatio: Option<f32>,
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl VideoItem {
|
impl VideoItem {
|
||||||
@@ -102,10 +124,14 @@ impl VideoItem {
|
|||||||
uploadedAt: None,
|
uploadedAt: None,
|
||||||
formats: None, // Placeholder for formats
|
formats: None, // Placeholder for formats
|
||||||
embed: None, // Placeholder for embed information
|
embed: None, // Placeholder for embed information
|
||||||
preview: None
|
preview: None,
|
||||||
|
aspectRatio: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn tags(mut self, tags: Vec<String>) -> Self {
|
pub fn tags(mut self, tags: Vec<String>) -> Self {
|
||||||
|
if tags.is_empty(){
|
||||||
|
return self;
|
||||||
|
}
|
||||||
self.tags = Some(tags);
|
self.tags = Some(tags);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -134,10 +160,13 @@ impl VideoItem {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn formats(mut self, formats: Vec<VideoFormat>) -> Self {
|
pub fn formats(mut self, formats: Vec<VideoFormat>) -> Self {
|
||||||
|
if formats.is_empty(){
|
||||||
|
return self;
|
||||||
|
}
|
||||||
self.formats = Some(formats);
|
self.formats = Some(formats);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn add_format(mut self, format: VideoFormat){
|
pub fn add_format(mut self, format: VideoFormat) {
|
||||||
if let Some(formats) = self.formats.as_mut() {
|
if let Some(formats) = self.formats.as_mut() {
|
||||||
formats.push(format);
|
formats.push(format);
|
||||||
} else {
|
} else {
|
||||||
@@ -152,36 +181,62 @@ impl VideoItem {
|
|||||||
self.preview = Some(preview);
|
self.preview = Some(preview);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn aspect_ratio(mut self, aspect_ratio: f32) -> Self {
|
||||||
|
self.aspectRatio = Some(aspect_ratio);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, Debug, Clone)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
pub struct VideoFormat {
|
pub struct VideoFormat {
|
||||||
url: String,
|
pub url: String,
|
||||||
quality: String,
|
quality: String,
|
||||||
format: String,
|
format: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
format_id: Option<String>,
|
format_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
format_note: Option<String>,
|
format_note: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
filesize: Option<u32>,
|
filesize: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
asr: Option<u32>,
|
asr: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
fps: Option<u32>,
|
fps: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
width: Option<u32>,
|
width: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
height: Option<u32>,
|
height: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
tbr: Option<u32>,
|
tbr: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
language: Option<String>,
|
language: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
language_preference: Option<u32>,
|
language_preference: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
ext: Option<String>,
|
ext: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
vcodec: Option<String>,
|
vcodec: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
acodec: Option<String>,
|
acodec: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
dynamic_range: Option<String>,
|
dynamic_range: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
abr: Option<u32>,
|
abr: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
vbr: Option<u32>,
|
vbr: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
container: Option<String>,
|
container: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
protocol: Option<String>,
|
protocol: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
audio_ext: Option<String>,
|
audio_ext: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
video_ext: Option<String>,
|
video_ext: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
resolution: Option<String>,
|
resolution: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
http_headers: Option<HashMap<String, String>>,
|
http_headers: Option<HashMap<String, String>>,
|
||||||
|
|
||||||
}
|
}
|
||||||
impl VideoFormat {
|
impl VideoFormat {
|
||||||
pub fn new(url: String, quality: String, format: String) -> Self {
|
pub fn new(url: String, quality: String, format: String) -> Self {
|
||||||
@@ -189,7 +244,7 @@ impl VideoFormat {
|
|||||||
VideoFormat {
|
VideoFormat {
|
||||||
url,
|
url,
|
||||||
quality,
|
quality,
|
||||||
format: "mp4".to_string(), // Default format
|
format: format, // Default format
|
||||||
format_id: Some("mp4-1080".to_string()),
|
format_id: Some("mp4-1080".to_string()),
|
||||||
format_note: None,
|
format_note: None,
|
||||||
filesize: None,
|
filesize: None,
|
||||||
@@ -222,11 +277,103 @@ impl VideoFormat {
|
|||||||
headers.insert(key, value);
|
headers.insert(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn http_header(&mut self, key: String, value: String) -> Self {
|
||||||
pub fn protocol(mut self, protocol: String) -> Self {
|
if self.http_headers.is_none() {
|
||||||
self.protocol = Some(protocol);
|
self.http_headers = Some(HashMap::new());
|
||||||
|
}
|
||||||
|
if let Some(headers) = &mut self.http_headers {
|
||||||
|
headers.insert(key, value);
|
||||||
|
}
|
||||||
|
self.to_owned()
|
||||||
|
}
|
||||||
|
pub fn format_id(mut self, format_id: String) -> Self {
|
||||||
|
self.format_id = Some(format_id);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
pub fn format_note(mut self, format_note: String) -> Self {
|
||||||
|
self.format_note = Some(format_note);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
// pub fn filesize(mut self, filesize: u32) -> Self {
|
||||||
|
// self.filesize = Some(filesize);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn asr(mut self, asr: u32) -> Self {
|
||||||
|
// self.asr = Some(asr);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn fps(mut self, fps: u32) -> Self {
|
||||||
|
// self.fps = Some(fps);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn width(mut self, width: u32) -> Self {
|
||||||
|
// self.width = Some(width);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn height(mut self, height: u32) -> Self {
|
||||||
|
// self.height = Some(height);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn tbr(mut self, tbr: u32) -> Self {
|
||||||
|
// self.tbr = Some(tbr);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn language(mut self, language: String) -> Self {
|
||||||
|
// self.language = Some(language);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn language_preference(mut self, language_preference: u32) -> Self {
|
||||||
|
// self.language_preference = Some(language_preference);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn ext(mut self, ext: String) -> Self {
|
||||||
|
// self.ext = Some(ext);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn vcodec(mut self, vcodec: String) -> Self {
|
||||||
|
// self.vcodec = Some(vcodec);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn acodec(mut self, acodec: String) -> Self {
|
||||||
|
// self.acodec = Some(acodec);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn dynamic_range(mut self, dynamic_range: String) -> Self {
|
||||||
|
// self.dynamic_range = Some(dynamic_range);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn abr(mut self, abr: u32) -> Self {
|
||||||
|
// self.abr = Some(abr);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn vbr(mut self, vbr: u32) -> Self {
|
||||||
|
// self.vbr = Some(vbr);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn container(mut self, container: String) -> Self {
|
||||||
|
// self.container = Some(container);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn protocol(mut self, protocol: String) -> Self {
|
||||||
|
// self.protocol = Some(protocol);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn audio_ext(mut self, audio_ext: String) -> Self {
|
||||||
|
// self.audio_ext = Some(audio_ext);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn video_ext(mut self, video_ext: String) -> Self {
|
||||||
|
// self.video_ext = Some(video_ext);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn resolution(mut self, resolution: String) -> Self {
|
||||||
|
// self.resolution = Some(resolution);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// pub fn http_headers(mut self, http_headers: HashMap<String, String>) -> Self {
|
||||||
|
// self.http_headers = Some(http_headers);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
#[derive(serde::Serialize, Debug)]
|
#[derive(serde::Serialize, Debug)]
|
||||||
pub struct Videos {
|
pub struct Videos {
|
||||||
|
|||||||
@@ -16,16 +16,18 @@ stdout_logfile_maxbytes = 0
|
|||||||
stderr_logfile_maxbytes = 0
|
stderr_logfile_maxbytes = 0
|
||||||
|
|
||||||
[program:vnc]
|
[program:vnc]
|
||||||
command=/dockerstartup/vnc_startup.sh --wait
|
command=bash -c '[ "$PROXY" = "1" ] && exec /dockerstartup/vnc_startup.sh --wait || echo "Skipping vnc (PROXY != 1)"'
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=false
|
||||||
stdout_logfile_maxbytes = 0
|
stdout_logfile=/dev/stdout
|
||||||
stderr_logfile_maxbytes = 0
|
stderr_logfile=/dev/stderr
|
||||||
|
stdout_logfile_maxbytes = 0
|
||||||
[program:burpsuite]
|
stderr_logfile_maxbytes = 0
|
||||||
command=bash /app/supervisord/burpsuite.sh
|
|
||||||
autostart=true
|
[program:burpsuite]
|
||||||
autorestart=true
|
command=bash -c '[ "$PROXY" = "1" ] && exec /app/supervisord/burpsuite.sh || echo "Skipping burpsuite (PROXY != 1)"'
|
||||||
|
autostart=true
|
||||||
|
autorestart=false
|
||||||
stdout_logfile=/dev/stdout
|
stdout_logfile=/dev/stdout
|
||||||
stderr_logfile=/dev/stderr
|
stderr_logfile=/dev/stderr
|
||||||
stdout_logfile_maxbytes = 0
|
stdout_logfile_maxbytes = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user