use crate::gltf_fetch::PendingGltfMulti;
use nightshade::prelude::{ehttp, serde_json};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
const DOWNLOAD_URL_PREFIX: &str = "https://api.sketchfab.com/v3/models/";
#[derive(Default, Clone)]
pub enum FetchStatus {
#[default]
Idle,
Working(String),
Failed(String),
}
#[derive(Deserialize)]
struct DownloadInfo {
#[serde(default)]
gltf: Option<DownloadFile>,
}
#[derive(Deserialize)]
struct DownloadFile {
url: String,
}
pub struct SketchfabBrowser {
pub token: String,
pub url: String,
status: Arc<Mutex<FetchStatus>>,
pending_gltf: Arc<Mutex<Option<PendingGltfMulti>>>,
}
impl Default for SketchfabBrowser {
fn default() -> Self {
Self {
token: String::new(),
url: String::new(),
status: Arc::new(Mutex::new(FetchStatus::Idle)),
pending_gltf: Arc::new(Mutex::new(None)),
}
}
}
impl SketchfabBrowser {
pub fn take_pending_gltf(&mut self) -> Option<PendingGltfMulti> {
self.pending_gltf.lock().ok().and_then(|mut p| p.take())
}
pub fn status(&self) -> FetchStatus {
self.status.lock().unwrap().clone()
}
pub fn can_fetch(&self) -> bool {
let working = matches!(&*self.status.lock().unwrap(), FetchStatus::Working(_));
!working && !self.token.trim().is_empty() && !self.url.trim().is_empty()
}
pub fn start_fetch(&self) {
let Some(uid) = extract_uid(&self.url) else {
*self.status.lock().unwrap() =
FetchStatus::Failed("could not parse model UID from URL".to_string());
return;
};
let token = self.token.trim().to_string();
if token.is_empty() {
return;
}
*self.status.lock().unwrap() = FetchStatus::Working("requesting download URL".into());
let download_endpoint = format!("{DOWNLOAD_URL_PREFIX}{uid}/download");
let mut request = ehttp::Request::get(&download_endpoint);
request
.headers
.insert("Authorization", format!("Token {token}"));
let status = Arc::clone(&self.status);
let pending = Arc::clone(&self.pending_gltf);
let display_name = format!("Sketchfab {uid}");
ehttp::fetch(request, move |result| match result {
Ok(response) if response.ok => {
match serde_json::from_slice::<DownloadInfo>(&response.bytes) {
Ok(info) => match info.gltf {
Some(gltf_file) => {
*status.lock().unwrap() =
FetchStatus::Working("downloading archive".into());
fetch_archive(gltf_file.url, display_name, status, pending);
}
None => {
*status.lock().unwrap() = FetchStatus::Failed(
"this asset does not provide a glTF download".to_string(),
);
}
},
Err(error) => {
*status.lock().unwrap() =
FetchStatus::Failed(format!("download response parse: {error}"));
}
}
}
Ok(response) => {
let detail = if response.status == 401 || response.status == 403 {
" (token rejected or asset is not downloadable for this account)"
} else {
""
};
*status.lock().unwrap() = FetchStatus::Failed(format!(
"HTTP {} on download endpoint{detail}",
response.status
));
}
Err(error) => {
*status.lock().unwrap() = FetchStatus::Failed(error);
}
});
}
}
fn fetch_archive(
url: String,
display_name: String,
status: Arc<Mutex<FetchStatus>>,
pending: Arc<Mutex<Option<PendingGltfMulti>>>,
) {
let request = ehttp::Request::get(&url);
ehttp::fetch(request, move |result| match result {
Ok(response) if response.ok => {
*status.lock().unwrap() = FetchStatus::Working("extracting archive".into());
match extract_gltf_archive(&response.bytes) {
Ok((gltf_bytes, resources)) => {
if let Ok(mut slot) = pending.lock() {
*slot = Some(PendingGltfMulti {
display_name,
gltf_bytes,
resources,
});
}
*status.lock().unwrap() = FetchStatus::Idle;
}
Err(error) => {
*status.lock().unwrap() =
FetchStatus::Failed(format!("archive extract: {error}"));
}
}
}
Ok(response) => {
*status.lock().unwrap() =
FetchStatus::Failed(format!("HTTP {} fetching archive", response.status));
}
Err(error) => {
*status.lock().unwrap() = FetchStatus::Failed(error);
}
});
}
type ExtractedGltf = (Vec<u8>, HashMap<String, Vec<u8>>);
fn extract_gltf_archive(bytes: &[u8]) -> Result<ExtractedGltf, String> {
use std::io::Read;
let cursor = std::io::Cursor::new(bytes);
let mut archive = zip::ZipArchive::new(cursor).map_err(|error| error.to_string())?;
let mut gltf_index: Option<usize> = None;
let mut gltf_path = String::new();
for index in 0..archive.len() {
let entry = archive.by_index(index).map_err(|error| error.to_string())?;
if entry.is_dir() {
continue;
}
let name = entry.name().to_string();
let lower = name.to_lowercase();
if lower.ends_with(".gltf") || lower.ends_with(".glb") {
gltf_index = Some(index);
gltf_path = name;
break;
}
}
let gltf_index = gltf_index.ok_or_else(|| "no .gltf or .glb entry in archive".to_string())?;
let mut gltf_bytes = Vec::new();
{
let mut entry = archive
.by_index(gltf_index)
.map_err(|error| error.to_string())?;
entry
.read_to_end(&mut gltf_bytes)
.map_err(|error| error.to_string())?;
}
let prefix = gltf_path
.rsplit_once('/')
.map(|(parent, _)| format!("{parent}/"))
.unwrap_or_default();
let mut resources = HashMap::new();
for index in 0..archive.len() {
if index == gltf_index {
continue;
}
let mut entry = archive.by_index(index).map_err(|error| error.to_string())?;
if entry.is_dir() {
continue;
}
let name = entry.name().to_string();
let key = name.strip_prefix(&prefix).unwrap_or(&name).to_string();
let mut bytes = Vec::new();
entry
.read_to_end(&mut bytes)
.map_err(|error| error.to_string())?;
resources.insert(key, bytes);
}
Ok((gltf_bytes, resources))
}
fn extract_uid(url: &str) -> Option<String> {
let chars: Vec<char> = url.chars().collect();
let mut index = 0;
while index + 32 <= chars.len() {
let window = &chars[index..index + 32];
if window.iter().all(|c| c.is_ascii_hexdigit()) {
let before_ok = index == 0 || !chars[index - 1].is_ascii_hexdigit();
let after_ok = index + 32 == chars.len() || !chars[index + 32].is_ascii_hexdigit();
if before_ok && after_ok {
return Some(window.iter().collect());
}
}
index += 1;
}
None
}