Files
SpotiFLAC-Mobile/rust_backend/crates/extensions/src/utility_host.rs
T
zarzet aa99726439 feat(backend): migrate production backend to Rust
Replace the Go backend with the Rust workspace and route Android/iOS through
UniFFI bindings. Include the migrated extension runtime, network, providers,
media metadata, downloads and library operations, with version 5.0.0+147.

Remove Go sources, adapters, native build selection and CI dependencies.
Build Rust unconditionally and lock the iOS Rust pod using a relative path.
Retain legacy source and migration evidence in a local ignored archive.

Validation: Android Kotlin compile and 53 native tests; Swift Rust-branch
equivalence and syntax; CocoaPods install; workflow, shell, Ruby, plist and
diff checks. Reuse the preceding 100 Rust tests, fmt/Clippy and five-ABI
build checkpoint; no new APK or iOS application build for this cleanup.

Known follow-up: URL/URLSearchParams globals are missing from the Rust JS
runtime. A controlled extension replay confirms a URL-resolution regression;
this commit does not fix that runtime gap.
2026-09-14 22:58:27 +07:00

124 lines
4.0 KiB
Rust

use crate::runtime::{Control, ExtensionServices};
use rquickjs::{Ctx, Function, Object, Value};
use spotiflac_core::matching;
pub(crate) use spotiflac_network::random_user_agent;
use std::sync::Arc;
pub(crate) fn register<'js>(
ctx: &Ctx<'js>,
host: &Object<'js>,
control: Arc<Control>,
services: &ExtensionServices,
) -> rquickjs::Result<()> {
let logs = Arc::clone(&services.logs);
let id = services.extension_id.clone();
host.set(
"managedConsole",
services.load_mode != crate::runtime::LoadMode::Initialize,
)?;
let auth = services.auth_registry.clone();
let provider_id = services.extension_id.clone();
host.set(
"providerPendingVerification",
Function::new(ctx.clone(), move || {
auth.as_ref()
.filter(|auth| auth.has_fresh_challenge(&provider_id))
.map(|_| provider_id.clone())
})?,
)?;
let progress = Arc::clone(&services.downloads.progress);
let progress_control = Arc::clone(&control);
host.set(
"providerProgress",
Function::new(ctx.clone(), move |percent: i32| {
let id = progress_control.item_id();
if !id.is_empty() && progress_control.check().is_ok() {
let _ = progress.set_progress(&id, f64::from(percent) / 100.0, 0, 0);
}
})?,
)?;
host.set(
"providerInteger",
Function::new(ctx.clone(), crate::provider::integer)?,
)?;
host.set(
"providerTrim",
Function::new(ctx.clone(), |value: String| value.trim().to_owned())?,
)?;
host.set(
"providerAudioTraits",
Function::new(ctx.clone(), crate::provider::audio_traits)?,
)?;
host.set(
"extensionLog",
Function::new(
ctx.clone(),
move |level: String, values: Vec<String>, count: usize| {
logs.extension(&id, &level, values, count)
},
)?,
)?;
host.set(
"logOpaqueType",
Function::new(ctx.clone(), |value: Value<'js>| {
if value.is_proxy() {
"<goja.Proxy>"
} else if value.is_promise() {
"<*goja.Promise>"
} else {
""
}
})?,
)?;
let comparison = Arc::clone(&control);
host.set(
"compareStrings",
Function::new(ctx.clone(), move |first: String, second: String| {
// The call boundary turns cancellation/timeout into its typed error.
matching::compare_strings(&first, &second, &|| {
comparison.check().map_err(|e| e.to_string())
})
.unwrap_or(0.0)
})?,
)?;
host.set(
"normalizeMatching",
Function::new(ctx.clone(), |value: String| matching::normalize(&value))?,
)?;
host.set(
"compareDuration",
Function::new(ctx.clone(), |first: f64, second: f64, tolerance: f64| {
// Goja's ToInteger clips infinities and overflow to the i64 endpoints.
matching::compare_duration(first as i64, second as i64, tolerance as i64)
})?,
)?;
let downloads = Arc::clone(&services.downloads);
let download_control = Arc::clone(&control);
host.set(
"downloadCancelled",
Function::new(ctx.clone(), move || {
let id = download_control.item_id();
!id.is_empty() && downloads.cancellation.is_cancelled(&id).unwrap_or(false)
})?,
)?;
host.set(
"requestCancelled",
Function::new(ctx.clone(), move || control.request_cancelled())?,
)?;
let version = services.app_version.clone();
host.set(
"appVersion",
Function::new(ctx.clone(), move || version.get())?,
)?;
let version = services.app_version.clone();
host.set(
"appUserAgent",
Function::new(ctx.clone(), move || version.user_agent())?,
)?;
host.set(
"randomUserAgent",
Function::new(ctx.clone(), random_user_agent)?,
)?;
Ok(())
}