uploaders

This commit is contained in:
Simon
2026-03-31 13:39:11 +00:00
parent 80207efa73
commit bdc7d61121
8 changed files with 913 additions and 4 deletions

View File

@@ -1,7 +1,9 @@
use crate::providers::{
ALL_PROVIDERS, DynProvider, build_status_response, panic_payload_to_string,
report_provider_error, resolve_provider_for_build, run_provider_guarded,
run_uploader_provider_guarded,
};
use crate::uploaders::{UploaderProfile, UploadersRequest};
use crate::util::cache::VideoCache;
use crate::util::discord::send_discord_error_report;
use crate::util::proxy::{Proxy, all_proxies_snapshot};
@@ -141,10 +143,61 @@ pub fn config(cfg: &mut web::ServiceConfig) {
// .route(web::get().to(videos_get))
.route(web::post().to(videos_post)),
)
.service(web::resource("/uploaders").route(web::post().to(uploaders_post)))
.service(web::resource("/test").route(web::get().to(test)))
.service(web::resource("/proxies").route(web::get().to(proxies)));
}
fn uploader_request_is_valid(request: &UploadersRequest) -> bool {
request.uploaderId.is_some() || request.uploaderName.is_some()
}
fn provider_hint_from_uploader_id(uploader_id: &str) -> Option<String> {
let (channel, _) = uploader_id.split_once(':')?;
Some(resolve_provider_for_build(channel).to_string())
}
fn uploader_provider_ids() -> Vec<String> {
let mut ids = ALL_PROVIDERS
.iter()
.filter_map(|(provider_id, _)| (*provider_id != "all").then(|| (*provider_id).to_string()))
.collect::<Vec<_>>();
ids.sort();
ids
}
fn uploader_match_sort_key(profile: &UploaderProfile) -> (u64, String, String) {
(
profile.videoCount,
profile.channel.clone().unwrap_or_default(),
profile.id.clone(),
)
}
async fn lookup_uploader_with_provider(
provider_id: &str,
provider: DynProvider,
cache: VideoCache,
pool: DbPool,
request: &UploadersRequest,
options: crate::videos::ServerOptions,
) -> Result<Option<UploaderProfile>, String> {
run_uploader_provider_guarded(
provider_id,
"uploaders_post.get_uploader",
provider.get_uploader(
cache,
pool,
request.uploaderId.clone(),
request.uploaderName.clone(),
request.query.clone(),
request.profileContent,
options,
),
)
.await
}
async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
#[cfg(feature = "debug")]
let trace_id = crate::util::flow_debug::next_trace_id("status");
@@ -491,6 +544,144 @@ async fn videos_post(
Ok(web::HttpResponse::Ok().json(&videos))
}
async fn uploaders_post(
uploader_request: web::types::Json<UploadersRequest>,
cache: web::types::State<VideoCache>,
pool: web::types::State<DbPool>,
requester: web::types::State<Requester>,
req: HttpRequest,
) -> Result<impl web::Responder, web::Error> {
let trace_id = crate::util::flow_debug::next_trace_id("uploaders");
let request = uploader_request.into_inner().normalized();
if !uploader_request_is_valid(&request) {
return Ok(web::HttpResponse::BadRequest().body(
"At least one of uploaderId or uploaderName must be provided",
));
}
let public_url_base = format!(
"{}://{}",
req.connection_info().scheme(),
req.connection_info().host()
);
let mut requester = requester.get_ref().clone();
requester.set_debug_trace_id(Some(trace_id.clone()));
let options = ServerOptions {
featured: None,
category: None,
sites: None,
filter: None,
language: None,
public_url_base: Some(public_url_base),
requester: Some(requester),
network: None,
stars: None,
categories: None,
duration: None,
sort: None,
sexuality: None,
};
crate::flow_debug!(
"trace={} uploaders request uploader_id={:?} uploader_name={:?} profile_content={} query={:?}",
trace_id,
&request.uploaderId,
&request.uploaderName,
request.profileContent,
&request.query
);
if let Some(uploader_id) = request.uploaderId.as_deref() {
if let Some(provider_id) = provider_hint_from_uploader_id(uploader_id) {
let Some(provider) = get_provider(&provider_id) else {
return Ok(web::HttpResponse::NotFound().finish());
};
let result = lookup_uploader_with_provider(
&provider_id,
provider,
cache.get_ref().clone(),
pool.get_ref().clone(),
&request,
options,
)
.await;
return match result {
Ok(Some(profile)) => Ok(web::HttpResponse::Ok().json(&profile)),
Ok(None) => Ok(web::HttpResponse::NotFound().finish()),
Err(_error) => {
crate::flow_debug!(
"trace={} uploaders targeted provider failed provider={} error={}",
trace_id,
&provider_id,
&_error
);
Ok(web::HttpResponse::InternalServerError().finish())
}
};
}
}
let mut matches = Vec::new();
let mut saw_error = false;
let requested_name = request
.uploaderName
.as_ref()
.map(|value| value.to_ascii_lowercase());
for provider_id in uploader_provider_ids() {
let Some(provider) = get_provider(&provider_id) else {
continue;
};
let result = lookup_uploader_with_provider(
&provider_id,
provider,
cache.get_ref().clone(),
pool.get_ref().clone(),
&request,
options.clone(),
)
.await;
match result {
Ok(Some(profile)) => {
if let Some(requested_name) = requested_name.as_deref() {
if profile.name.to_ascii_lowercase() != requested_name {
crate::flow_debug!(
"trace={} uploaders ignoring non_exact_match provider={} requested={} returned={}",
trace_id,
&provider_id,
requested_name,
&profile.name
);
continue;
}
}
matches.push(profile);
}
Ok(None) => {}
Err(_error) => {
saw_error = true;
crate::flow_debug!(
"trace={} uploaders provider failed provider={} error={}",
trace_id,
&provider_id,
&_error
);
}
}
}
if matches.is_empty() {
if saw_error {
return Ok(web::HttpResponse::InternalServerError().finish());
}
return Ok(web::HttpResponse::NotFound().finish());
}
matches.sort_by(|a, b| uploader_match_sort_key(b).cmp(&uploader_match_sort_key(a)));
Ok(web::HttpResponse::Ok().json(&matches[0]))
}
pub fn get_provider(channel: &str) -> Option<DynProvider> {
let provider = ALL_PROVIDERS.get(channel).cloned();
crate::flow_debug!(
@@ -539,3 +730,49 @@ pub async fn proxies() -> Result<impl web::Responder, web::Error> {
}
Ok(web::HttpResponse::Ok().json(&by_protocol))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uploaders_request_requires_id_or_name() {
let invalid = UploadersRequest::default();
let valid = UploadersRequest {
uploaderName: Some("Example".to_string()),
..UploadersRequest::default()
};
assert!(!uploader_request_is_valid(&invalid));
assert!(uploader_request_is_valid(&valid));
}
#[test]
fn uploader_provider_hint_uses_channel_prefix() {
assert_eq!(
provider_hint_from_uploader_id("hsex:xihongshiddd").as_deref(),
Some("hsex")
);
assert_eq!(provider_hint_from_uploader_id("plain-id"), None);
}
#[test]
fn uploader_match_prefers_higher_video_count() {
let a = UploaderProfile {
id: "a".to_string(),
name: "Example".to_string(),
channel: Some("alpha".to_string()),
videoCount: 3,
..UploaderProfile::default()
};
let b = UploaderProfile {
id: "b".to_string(),
name: "Example".to_string(),
channel: Some("beta".to_string()),
videoCount: 9,
..UploaderProfile::default()
};
assert!(uploader_match_sort_key(&b) > uploader_match_sort_key(&a));
}
}