shooshtime

This commit is contained in:
Simon
2026-03-16 19:37:05 +00:00
parent 1f99eec5a3
commit 9751c25b95
4 changed files with 1418 additions and 4 deletions

View File

@@ -35,6 +35,7 @@ pub mod paradisehill;
pub mod porn00; pub mod porn00;
pub mod porn4fans; pub mod porn4fans;
pub mod pornzog; pub mod pornzog;
pub mod shooshtime;
pub mod sxyprn; pub mod sxyprn;
pub mod tnaflix; pub mod tnaflix;
pub mod tokyomotion; pub mod tokyomotion;
@@ -133,6 +134,10 @@ pub static ALL_PROVIDERS: Lazy<HashMap<&'static str, DynProvider>> = Lazy::new(|
"porn4fans", "porn4fans",
Arc::new(porn4fans::Porn4fansProvider::new()) as DynProvider, Arc::new(porn4fans::Porn4fansProvider::new()) as DynProvider,
); );
m.insert(
"shooshtime",
Arc::new(shooshtime::ShooshtimeProvider::new()) as DynProvider,
);
m.insert( m.insert(
"pornzog", "pornzog",
Arc::new(pornzog::PornzogProvider::new()) as DynProvider, Arc::new(pornzog::PornzogProvider::new()) as DynProvider,

View File

@@ -11,6 +11,7 @@ use error_chain::error_chain;
use futures::future::join_all; use futures::future::join_all;
use htmlentity::entity::{ICodedDataTrait, decode}; use htmlentity::entity::{ICodedDataTrait, decode};
use regex::Regex; use regex::Regex;
use scraper::{Html, Selector};
use std::collections::HashSet; use std::collections::HashSet;
error_chain! { error_chain! {
@@ -266,6 +267,36 @@ impl Porn4fansProvider {
text.replace("\\/", "/").replace("&amp;", "&") text.replace("\\/", "/").replace("&amp;", "&")
} }
fn decode_html_text(text: &str) -> String {
decode(text.as_bytes())
.to_string()
.unwrap_or_else(|_| text.to_string())
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.trim()
.to_string()
}
fn strip_tags(text: &str) -> String {
Regex::new(r"(?is)<[^>]+>")
.ok()
.map(|regex| regex.replace_all(text, "").to_string())
.unwrap_or_else(|| text.to_string())
}
fn push_unique_tag(values: &mut Vec<String>, value: String) {
let value = value.trim().to_string();
if value.is_empty()
|| values
.iter()
.any(|existing| existing.eq_ignore_ascii_case(&value))
{
return;
}
values.push(value);
}
fn extract_views(text: &str) -> Option<u32> { fn extract_views(text: &str) -> Option<u32> {
Regex::new(r"(?i)<svg[^>]+icon-eye[^>]*>.*?</svg>\s*<span>([^<]+)</span>") Regex::new(r"(?i)<svg[^>]+icon-eye[^>]*>.*?</svg>\s*<span>([^<]+)</span>")
.ok() .ok()
@@ -303,6 +334,34 @@ impl Porn4fansProvider {
None None
} }
fn collect_texts(document: &Html, selector: &str) -> Vec<String> {
let Ok(selector) = Selector::parse(selector) else {
return vec![];
};
let mut values = Vec::new();
for element in document.select(&selector) {
let raw_text = element.text().collect::<Vec<_>>().join(" ");
let cleaned = Self::decode_html_text(&Self::strip_tags(&raw_text));
Self::push_unique_tag(&mut values, cleaned);
}
values
}
fn extract_page_models_and_categories(text: &str) -> (Vec<String>, Vec<String>) {
let document = Html::parse_document(text);
let models = Self::collect_texts(&document, ".player-models-list a[href*=\"/models/\"]");
let mut categories =
Self::collect_texts(&document, ".categories-row a[href*=\"/categories/\"]");
for value in Self::collect_texts(&document, ".tags-row a[href*=\"/tags/\"]") {
Self::push_unique_tag(&mut categories, value);
}
(models, categories)
}
fn parse_video_cards_from_html(&self, html: &str) -> Vec<Porn4fansCard> { fn parse_video_cards_from_html(&self, html: &str) -> Vec<Porn4fansCard> {
if html.trim().is_empty() { if html.trim().is_empty() {
return vec![]; return vec![];
@@ -375,9 +434,17 @@ impl Porn4fansProvider {
None, None,
) )
.await .await
.ok() .ok();
.and_then(|text| Self::extract_direct_video_url_from_page(&text))
.unwrap_or_else(|| card.page_url.clone()); let (direct_url, models, categories) = match direct_url {
Some(text) => {
let url = Self::extract_direct_video_url_from_page(&text)
.unwrap_or_else(|| card.page_url.clone());
let (models, categories) = Self::extract_page_models_and_categories(&text);
(url, models, categories)
}
None => (card.page_url.clone(), vec![], vec![]),
};
let mut item = VideoItem::new( let mut item = VideoItem::new(
card.id, card.id,
@@ -393,6 +460,10 @@ impl Porn4fansProvider {
if let Some(rating) = card.rating { if let Some(rating) = card.rating {
item = item.rating(rating); item = item.rating(rating);
} }
if let Some(model) = models.first() {
item = item.uploader(model.clone());
}
item = item.tags(categories);
item item
} }
@@ -541,4 +612,33 @@ mod tests {
) )
); );
} }
#[test]
fn extracts_models_and_categories_from_video_page() {
let html = r#"
<div class="player-models-list">
<div class="player-model-item">
<a href="/models/piper-rockelle/"><span class="player-model-name">Piper Rockelle</span></a>
</div>
</div>
<ul class="categories-row">
<li class="visible"><a href="/categories/striptease/">Striptease</a></li>
<li class="visible"><a href="/categories/teen/">Teen</a></li>
</ul>
<ul class="tags-row">
<li class="visible"><a href="/tags/bathroom/">Bathroom</a></li>
</ul>
"#;
let (models, categories) = Porn4fansProvider::extract_page_models_and_categories(html);
assert_eq!(models, vec!["Piper Rockelle".to_string()]);
assert_eq!(
categories,
vec![
"Striptease".to_string(),
"Teen".to_string(),
"Bathroom".to_string()
]
);
}
} }

1310
src/providers/shooshtime.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,6 @@ use crate::util::requester::Requester;
fn normalize_image_url(endpoint: &str) -> String { fn normalize_image_url(endpoint: &str) -> String {
let endpoint = endpoint.trim_start_matches('/'); let endpoint = endpoint.trim_start_matches('/');
println!("Normalizing image URL: {endpoint}");
if endpoint.starts_with("http://") || endpoint.starts_with("https://") { if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
endpoint.to_string() endpoint.to_string()
} else if endpoint.starts_with("hanime-cdn.com/") || endpoint == "hanime-cdn.com" { } else if endpoint.starts_with("hanime-cdn.com/") || endpoint == "hanime-cdn.com" {