2026-01-21 13:20:47 +01:00
|
|
|
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};
|
|
|
|
|
use async_sqlite::{Client, ClientBuilder, rusqlite};
|
|
|
|
|
|
2026-01-22 12:02:51 +01:00
|
|
|
use crate::persist::common::{ResultExt, concat_os_str};
|
2026-01-21 13:20:47 +01:00
|
|
|
use crate::persist::data_manager::DataManagerError;
|
|
|
|
|
use crate::views::overview::{FilmOverview, SeriesOverview};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub struct SqliteManager {
|
2026-01-22 12:02:51 +01:00
|
|
|
client: Client,
|
2026-01-21 13:20:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SqliteManager {
|
|
|
|
|
pub async fn new(data_dir: &OsStr) -> Result<SqliteManager, DataManagerError> {
|
2026-01-22 12:02:51 +01:00
|
|
|
let client = create_client(data_dir).await?;
|
|
|
|
|
Ok(SqliteManager { client })
|
2026-01-21 13:20:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The order of the items is undefined.
|
|
|
|
|
pub async fn films_overview(&self) -> Result<Vec<FilmOverview>, DataManagerError> {
|
2026-01-22 14:09:17 +01:00
|
|
|
self
|
2026-01-22 12:02:51 +01:00
|
|
|
.client
|
2026-01-21 13:20:47 +01:00
|
|
|
.conn(|connection| {
|
2026-01-22 12:02:51 +01:00
|
|
|
connection
|
2026-01-21 13:20:47 +01:00
|
|
|
.prepare(
|
|
|
|
|
"
|
|
|
|
|
select uuid, name, original_name, release_date, runtime_minutes
|
|
|
|
|
from films
|
|
|
|
|
",
|
2026-01-22 14:09:17 +01:00
|
|
|
)?
|
|
|
|
|
.query(())?
|
2026-01-21 13:20:47 +01:00
|
|
|
.map(row_to_film_overview)
|
2026-01-22 12:02:51 +01:00
|
|
|
.collect()
|
2026-01-21 13:20:47 +01:00
|
|
|
})
|
2026-01-22 14:09:17 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(convert_error)
|
2026-01-21 13:20:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The order of the items is undefined.
|
|
|
|
|
pub async fn series_overview(&self) -> Result<Vec<SeriesOverview>, DataManagerError> {
|
2026-01-22 14:09:17 +01:00
|
|
|
self
|
2026-01-22 12:02:51 +01:00
|
|
|
.client
|
2026-01-21 13:20:47 +01:00
|
|
|
.conn(|connection| {
|
2026-01-22 12:02:51 +01:00
|
|
|
connection
|
2026-01-21 13:20:47 +01:00
|
|
|
.prepare(
|
|
|
|
|
"
|
|
|
|
|
select series.uuid, series.name, series.original_name,
|
|
|
|
|
min(episodes.release_date) as first_release_date
|
|
|
|
|
from series, seasons, episodes
|
|
|
|
|
where series.uuid = seasons.series and seasons.uuid = episodes.season
|
|
|
|
|
group by series.uuid
|
|
|
|
|
",
|
2026-01-22 14:09:17 +01:00
|
|
|
)?
|
|
|
|
|
.query(())?
|
2026-01-21 13:20:47 +01:00
|
|
|
.map(row_to_series_overview)
|
2026-01-22 12:02:51 +01:00
|
|
|
.collect()
|
2026-01-21 13:20:47 +01:00
|
|
|
})
|
2026-01-22 14:09:17 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(convert_error)
|
2026-01-21 13:20:47 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Debug for SqliteManager {
|
|
|
|
|
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
|
|
|
|
|
formatter.write_str("SqliteManager { Client, Client }")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 12:02:51 +01:00
|
|
|
async fn create_client(data_dir: &OsStr) -> Result<Client, DataManagerError> {
|
2026-01-21 13:20:47 +01:00
|
|
|
let client = ClientBuilder::new()
|
2026-01-22 12:02:51 +01:00
|
|
|
.path("")
|
|
|
|
|
.flags(OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX)
|
2026-01-21 13:20:47 +01:00
|
|
|
.open()
|
|
|
|
|
.await;
|
|
|
|
|
|
2026-01-22 12:02:51 +01:00
|
|
|
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");
|
2026-01-22 14:09:17 +01:00
|
|
|
connection.execute("attach database :path as shared", &[(":path", shared_path)])?;
|
2026-01-22 12:02:51 +01:00
|
|
|
|
|
|
|
|
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");
|
2026-01-22 14:09:17 +01:00
|
|
|
connection.execute("attach database :path as local", &[(":path", local_path)])?;
|
2026-01-22 12:02:51 +01:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
.await
|
2026-01-22 15:59:51 +01:00
|
|
|
.map(|()| client)
|
2026-01-22 12:02:51 +01:00
|
|
|
})
|
|
|
|
|
.await;
|
|
|
|
|
|
2026-01-22 14:09:17 +01:00
|
|
|
client.map_err(convert_error)
|
2026-01-21 13:20:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")?;
|
|
|
|
|
|
|
|
|
|
Ok(FilmOverview {
|
|
|
|
|
uuid,
|
|
|
|
|
name,
|
|
|
|
|
original_name,
|
|
|
|
|
release_date,
|
|
|
|
|
runtime,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 14:09:17 +01:00
|
|
|
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,
|
|
|
|
|
_ => 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")
|
|
|
|
|
}
|
|
|
|
|
_ => DataManagerError::UnknownDBError,
|
|
|
|
|
},
|
|
|
|
|
_ => DataManagerError::UnknownDBError,
|
|
|
|
|
}
|
2026-01-21 13:20:47 +01:00
|
|
|
}
|