use crate::hosting::*;
use crate::prelude::*;
#[cfg(test)]
use di::existing_as_self;
use di::{Injectable, Mut, ServiceCollection, singleton_as_self};
use gazelle_api::{GazelleClientFactory, GazelleClientOptions, GazelleClientTrait};
use rogue_logging::InitLog;
use std::fs::read_to_string;
use std::sync::Arc;
use tokio::sync::Semaphore;
use tokio::task::JoinSet;
pub struct HostBuilder {
pub services: ServiceCollection,
options: OptionsProvider,
}
impl Default for HostBuilder {
fn default() -> Self {
Self::new()
}
}
impl HostBuilder {
#[must_use]
pub(crate) fn new() -> Self {
let options = OptionsProvider::default();
Self::new_internal(options, None)
}
#[must_use]
pub fn new_cli() -> Self {
let args = Arc::new(ArgumentsProvider::new());
let yaml = read_config_file(&args);
let options = OptionsProvider::from_args(args.clone(), yaml);
Self::new_internal(options, Some(args))
}
#[must_use]
#[expect(
clippy::as_conversions,
reason = "required for Box<dyn GazelleClientTrait> and u16-to-usize coercions"
)]
fn new_internal(mut options: OptionsProvider, args: Option<Arc<ArgumentsProvider>>) -> Self {
let mut services = ServiceCollection::new();
services.register_options(&mut options);
if let Some(args) = args {
let args = args.clone();
services.add(singleton_as_self().from(move |_| args.clone()));
}
services
.add(singleton_as_self().from(|provider| {
let options = provider.get_required::<SharedOptions>();
let logger = Ref::new(
default_logger()
.with_verbosity(options.verbosity)
.with_time_format(options.log_time)
.create(),
);
logger.clone().init();
logger
}))
.add(SoxFactory::singleton())
.add(PathManager::transient())
.add(IdProvider::transient())
.add(SourceProvider::transient())
.add(singleton_as_self().from(|provider| {
let options = provider.get_required::<SharedOptions>();
let factory = GazelleClientFactory {
options: GazelleClientOptions {
url: options.indexer_url.clone(),
key: options.api_key.clone(),
user_agent: app_user_agent(true),
requests_allowed_per_duration: None,
request_limit_duration: None,
},
};
Ref::new(Box::new(factory.create()) as Box<dyn GazelleClientTrait + Send + Sync>)
}))
.add(JobRunner::transient())
.add(Publisher::transient())
.add(DebugSubscriber::transient())
.add(ProgressBarSubscriber::transient())
.add(TargetFormatProvider::transient())
.add(BatchCommand::transient())
.add(ConfigCommand::transient())
.add(DocsCommand::transient())
.add(InspectCommand::transient())
.add(QueueAddCommand::transient())
.add(QueueListCommand::transient())
.add(QueueRemoveCommand::transient())
.add(QueueSummaryCommand::transient())
.add(singleton_as_self().from(|provider| {
let options = provider.get_required::<CacheOptions>();
Ref::new(Queue::from_options(options))
}))
.add(SpectrogramCommand::transient())
.add(SpectrogramJobFactory::transient())
.add(singleton_as_self().from(|provider| {
let options = provider.get_required::<RunnerOptions>();
let cpus = options.cpus.expect("cpus should be set") as usize;
Arc::new(Semaphore::new(cpus))
}))
.add(singleton_as_self().from(|_| {
let set: JoinSet<Result<(), Failure<JobAction>>> = JoinSet::new();
RefMut::new(Mut::new(set))
}))
.add(TranscodeCommand::transient())
.add(TranscodeJobFactory::transient())
.add(AdditionalJobFactory::transient())
.add(UploadCommand::transient())
.add(VerifyCommand::transient())
.add(VersionCommand::transient());
HostBuilder { services, options }
}
#[must_use]
#[cfg(test)]
pub fn with_options<T: Send + Sync + 'static>(&mut self, options: T) -> &mut Self {
self.services.add(existing_as_self(options));
self
}
#[must_use]
#[cfg(test)]
pub fn with_mock_api(&mut self, album_config: AlbumConfig) -> &mut Self {
self.with_mock_client(album_config.api())
}
#[must_use]
#[cfg(test)]
#[expect(
clippy::as_conversions,
reason = "required for DI trait object registration"
)]
pub fn with_mock_client(&mut self, client: gazelle_api::MockGazelleClient) -> &mut Self {
let client: Ref<Box<dyn GazelleClientTrait + Send + Sync>> =
Ref::new(Box::new(client) as Box<dyn GazelleClientTrait + Send + Sync>);
self.services
.add(singleton_as_self().from(move |_| client.clone()));
self
}
#[cfg(test)]
pub async fn with_test_options(&mut self, test_dir: &TestDirectory) -> &mut Self {
use tokio::fs::create_dir_all;
let output_dir = test_dir.output();
let cache_dir = test_dir.cache();
create_dir_all(&output_dir)
.await
.expect("should be able to create output dir");
create_dir_all(&cache_dir)
.await
.expect("should be able to create cache dir");
self.with_options(SharedOptions {
content: vec![SAMPLE_SOURCES_DIR.clone()],
output: output_dir,
..SharedOptions::mock()
})
.with_options(CacheOptions { cache: cache_dir })
}
pub fn build(&self) -> Result<Host, BuildError> {
if self.options.has_errors() {
return Err(BuildError::Options(self.options.errors.clone()));
}
let services = self.services.build_provider()?;
Ok(Host::new(services))
}
#[cfg(test)]
#[must_use]
#[expect(clippy::panic, reason = "intentional panic for test failures")]
pub fn expect_build(&self) -> Host {
match self.build() {
Ok(host) => host,
Err(error) => panic!("{error}"),
}
}
}
fn read_config_file(args: &ArgumentsProvider) -> Option<String> {
if !args.get_command().uses_options("ConfigOptions") {
return None;
}
let options = args.get_args::<ConfigOptionsPartial>().ok()?;
let path = options
.config
.clone()
.unwrap_or_else(PathManager::default_config_path);
read_to_string(path.expand_tilde()).ok()
}