use tokio::sync::{Mutex, RwLock, Semaphore};
use crate::action_cache::ActionCache;
use crate::config::{ConfigHandler, ConfigProxy};
use crate::database::Database;
use crate::error::NewsFlashError;
use crate::feed_api::FeedApi;
use crate::models::{ApiSecret, PluginID};
use crate::util::favicons::FavIconLoader;
use crate::{DefaultPortal, FeedApiImplementations};
use crate::{NewsFlash, database::DatabaseExt};
use crate::{NewsFlashResult, config::ConfigHandlerExt};
use std::io::{Error as IoError, ErrorKind as IoErrorKind};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
#[derive(Default)]
pub struct NewsFlashBuilder {
plugin_id: Option<PluginID>,
config_dir: Option<PathBuf>,
data_dir: Option<PathBuf>,
db: Option<Box<dyn DatabaseExt>>,
config_handler: Option<Box<dyn ConfigHandlerExt>>,
user_api_secret: Option<ApiSecret>,
#[cfg(test)]
api: Option<Box<dyn FeedApi>>,
}
impl NewsFlashBuilder {
pub fn plugin(mut self, id: PluginID) -> Self {
self.plugin_id.replace(id);
self
}
pub fn config_dir<P: AsRef<Path>>(mut self, path: P) -> Self {
self.config_dir.replace(path.as_ref().to_path_buf());
self
}
pub fn data_dir<P: AsRef<Path>>(mut self, path: P) -> Self {
self.data_dir.replace(path.as_ref().to_path_buf());
self
}
pub fn data_base(mut self, db: Box<dyn DatabaseExt>) -> Self {
self.db.replace(db);
self
}
pub fn config_handler(mut self, handler: Box<dyn ConfigHandlerExt>) -> Self {
self.config_handler.replace(handler);
self
}
pub fn api_secret(mut self, api_secret: Option<ApiSecret>) -> Self {
self.user_api_secret = api_secret;
self
}
#[cfg(test)]
pub fn api_mock(mut self, api: Box<dyn FeedApi>) -> Self {
self.api.replace(api);
self
}
pub fn try_load(mut self) -> NewsFlashResult<NewsFlash> {
let Some(config_dir) = self.config_dir.as_deref() else {
return Err(NewsFlashError::IO(IoError::new(
IoErrorKind::NotADirectory,
"need to specify a config directory",
)));
};
let handler = Box::new(ConfigHandler::default());
let config = ConfigProxy::open(handler, config_dir)?;
let plugin_id = config.get_backend().ok_or(NewsFlashError::LoadBackend)?;
self.plugin_id.replace(plugin_id);
self.create()
}
pub fn create(self) -> NewsFlashResult<NewsFlash> {
let Some(config_dir) = self.config_dir else {
return Err(NewsFlashError::IO(IoError::new(
IoErrorKind::NotADirectory,
"need to specify a config directory",
)));
};
tracing::trace!(?config_dir);
let Some(data_dir) = self.data_dir else {
return Err(NewsFlashError::IO(IoError::new(
IoErrorKind::NotADirectory,
"need to specify a data directory",
)));
};
tracing::trace!(?data_dir);
let handler: Box<dyn ConfigHandlerExt> = self.config_handler.unwrap_or(Box::new(ConfigHandler::default()));
let db: Box<dyn DatabaseExt> = if let Some(db) = self.db {
db
} else {
Box::new(Database::new(&data_dir)?)
};
let db = Arc::new(db);
let config = ConfigProxy::open(handler, &config_dir)?;
let download_semaphore = Arc::new(Semaphore::new(config.get_concurrent_downloads() as usize));
let config = Arc::new(RwLock::new(config));
#[cfg(not(test))]
let api = Self::load_backend(
self.plugin_id,
&config_dir,
db.clone(),
config.clone(),
download_semaphore.clone(),
self.user_api_secret,
)?;
#[cfg(test)]
let api = if let Some(api) = self.api {
api
} else {
Self::load_backend(
self.plugin_id,
&config_dir,
db.clone(),
config.clone(),
download_semaphore.clone(),
self.user_api_secret,
)?
};
let icons = FavIconLoader::new(&db, &download_semaphore);
let sync_cache = ActionCache::new();
let base = NewsFlash {
#[cfg(any(feature = "article-scraper", feature = "image-downloader"))]
data_dir,
db,
api: RwLock::new(api),
config,
download_semaphore,
icons,
#[cfg(feature = "article-scraper")]
scraper: RwLock::new(None),
sync_cache: Mutex::new(sync_cache),
is_sync_ongoing: Arc::new(AtomicBool::new(false)),
is_offline: Arc::new(AtomicBool::new(false)),
};
Ok(base)
}
fn load_backend<P: AsRef<Path>>(
plugin_id: Option<PluginID>,
config_dir: P,
db: Arc<Box<dyn DatabaseExt>>,
config: Arc<RwLock<ConfigProxy>>,
download_semaphore: Arc<Semaphore>,
user_api_secret: Option<ApiSecret>,
) -> NewsFlashResult<Box<dyn FeedApi>> {
if let Some(plugin_id) = &plugin_id {
tracing::info!(%plugin_id, "Loading backend");
if let Some(meta_data) = FeedApiImplementations::get(plugin_id) {
let portal = DefaultPortal::new(db, config, download_semaphore);
let portal = Box::new(portal);
let backend = meta_data.get_instance(config_dir.as_ref(), portal, user_api_secret)?;
Ok(backend)
} else {
tracing::error!(%plugin_id, "No meta object found");
Err(NewsFlashError::LoadBackend)
}
} else {
tracing::error!("need to specify plugin id or provide mocked feed API");
Err(NewsFlashError::LoadBackend)
}
}
}