zoodex/src/persist/sqlite_manager.rs

210 lines
6.2 KiB
Rust

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<SqliteManager, DataManagerError> {
let client = create_client(data_dir).await?;
Ok(SqliteManager { client })
}
// The order of the items is undefined.
pub async fn films_overview(&self) -> Result<Vec<FilmOverview>, DataManagerError> {
self
.client
.conn(|connection| {
connection
.prepare(
"
select films.uuid, films.name, films.original_name, films.release_date, films.runtime_minutes,
coalesce(watched_media.watched, 0) as watched
from films left join watched_media
on films.uuid is watched_media.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<Vec<SeriesOverview>, 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_media (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<Client, DataManagerError> {
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<FilmOverview> {
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<SeriesOverview> {
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
}
}
}