use std::ffi::OsStr; use std::fmt; use std::fmt::{Debug, Formatter}; use async_sqlite::rusqlite::fallible_iterator::FallibleIterator; use async_sqlite::rusqlite::{OpenFlags, Row, named_params}; use async_sqlite::{Client, ClientBuilder, rusqlite}; use crate::persist::common::{ResultExt, concat_os_str}; use crate::persist::data_manager::DataManagerError; use crate::views::overview::{FilmOverview, SeriesOverview}; pub struct SqliteManager { client: Client, } impl SqliteManager { pub async fn new(data_dir: &OsStr) -> Result { let client = create_client(data_dir).await?; Ok(SqliteManager { client }) } // The order of the items is undefined. pub async fn films_overview(&self) -> Result, DataManagerError> { self .client .conn(|connection| { connection .prepare( " select films.uuid, films.name, films.original_name, films.release_date, films.runtime_minutes, coalesce(watched_status.watched, 0) as watched from films left join watched_status on films.uuid is watched_status.media_uuid ", )? .query(())? .map(row_to_film_overview) .collect() }) .await .map_err(convert_error) } // The order of the items is undefined. pub async fn series_overview(&self) -> Result, DataManagerError> { self .client .conn(|connection| { connection .prepare( " select series.uuid, series.name, series.original_name, min(episodes.release_date) as first_release_date from series, seasons, episodes where series.uuid is seasons.series and seasons.uuid is episodes.season group by series.uuid ", )? .query(())? .map(row_to_series_overview) .collect() }) .await .map_err(convert_error) } pub async fn set_film_watched_status( &self, uuid: &str, watched: bool, ) -> Result<(), DataManagerError> { self .client .conn({ let uuid = uuid.to_string(); move |connection| { connection.execute( " insert into watched_status (media_uuid, watched) values (:uuid, :watched) on conflict (media_uuid) do update set watched = :watched ", named_params! { ":uuid": uuid, ":watched": watched }, )?; Ok(()) } }) .await .map_err(convert_error) } } impl Debug for SqliteManager { fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { formatter.write_str("SqliteManager { Client, Client }") } } async fn create_client(data_dir: &OsStr) -> Result { let client = ClientBuilder::new() .path("") .flags(OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX) .open() .await; let data_dir = data_dir.to_os_string(); let client = client .and_then_async(async move |client| { client .conn(move |connection| { let shared_path_os_str = concat_os_str!(&data_dir, "/shared.sqlite"); let shared_path = shared_path_os_str .to_str() .expect("shared database path should be valid Unicode"); connection.execute("attach database :path as shared", &[(":path", shared_path)])?; let local_path_os_str = concat_os_str!(&data_dir, "/local.sqlite"); let local_path = local_path_os_str .to_str() .expect("local database path should be valid Unicode"); connection.execute("attach database :path as local", &[(":path", local_path)])?; Ok(()) }) .await .map(|()| client) }) .await; client.map_err(convert_error) } fn row_to_film_overview(row: &Row) -> rusqlite::Result { let uuid = row.get("uuid")?; let name = row.get("name")?; let original_name = row.get("original_name")?; let release_date = row.get("release_date")?; let runtime = row.get("runtime_minutes")?; let watched = row.get("watched")?; Ok(FilmOverview { uuid, name, original_name, release_date, runtime, watched, }) } fn row_to_series_overview(row: &Row) -> rusqlite::Result { let uuid = row.get("uuid")?; let name = row.get("name")?; let original_name = row.get("original_name")?; let first_release_date = row.get("first_release_date")?; Ok(SeriesOverview { uuid, name, original_name, first_release_date, }) } fn convert_error(async_sqlite_error: async_sqlite::Error) -> DataManagerError { match async_sqlite_error { async_sqlite::Error::Closed => { panic!("database connection should remain open as long as the application is running") } async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error { rusqlite::Error::SqliteFailure(sqlite_error, _) => match sqlite_error.code { rusqlite::ffi::ErrorCode::CannotOpen => DataManagerError::CannotOpenDB, _ => { println!("{sqlite_error:?}"); DataManagerError::UnknownDBError } }, rusqlite::Error::InvalidColumnIndex(_) => { panic!("column indices obtained from query should exist") } rusqlite::Error::InvalidColumnName(_) => { panic!("column names obtained from query should exist") } rusqlite::Error::InvalidColumnType(..) => { panic!("values obtained from query should have a type matching their column") } rusqlite::Error::InvalidParameterCount(..) => { panic!("number of bound parameters should match that in the statement") } rusqlite::Error::ExecuteReturnedResults => { panic!("execution of statement returned data when it shouldn't") } rusqlite::Error::MultipleStatement => { panic!("multiple statements present when there should be one") } _ => { println!("{rusqlite_error:?}"); DataManagerError::UnknownDBError } }, _ => { println!("{async_sqlite_error:?}"); DataManagerError::UnknownDBError } } }