Update all libraries to the new database format

This commit is contained in:
2026-01-21 22:05:50 -07:00
parent 0e2a8e425b
commit 66168c120f
33 changed files with 1025 additions and 794 deletions
+5 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "flix-tmdb"
version = "0.0.16"
version = "0.0.17"
edition.workspace = true
rust-version.workspace = true
description = "Clients and models for fetching data from TMDB"
@@ -13,13 +13,16 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
bytes = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
flix-model = { workspace = true, features = ["serde"] }
governor = { workspace = true, features = ["jitter", "std"] }
nonzero_ext = { workspace = true }
reqwest = { workspace = true, features = ["json", "query", "rustls"] }
redb = { workspace = true }
reqwest = { workspace = true, features = ["query", "rustls"] }
sea-orm = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
url-macro = { workspace = true }
+17 -26
View File
@@ -1,25 +1,30 @@
//! Collections API
use core::time::Duration;
use std::rc::Rc;
use std::sync::RwLock;
use governor::Jitter;
use crate::Config;
use crate::api::exec_request;
use crate::model::Collection;
use crate::model::id::CollectionId;
use crate::{Cache, CachePolicy, Config};
use super::{Error, make_request};
/// TMDB Collections API client
pub struct Client {
config: Rc<Config>,
cache: Rc<dyn Cache>,
policy: Rc<RwLock<CachePolicy>>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
Self {
config,
cache,
policy,
}
}
}
@@ -30,25 +35,11 @@ impl Client {
id: impl Into<CollectionId>,
language: Option<&str>,
) -> Result<Collection, Error> {
self.config
.limiter
.until_ready_with_jitter(Jitter::new(
Duration::from_millis(0),
Duration::from_millis(50),
))
.await;
Ok(self
.config
.client
.execute(make_request(
&self.config,
&format!("/3/collection/{}", id.into().into_raw()),
language,
)?)
.await?
.error_for_status()?
.json()
.await?)
let request = make_request(
&self.config,
&format!("/3/collection/{}", id.into().into_raw()),
language,
)?;
exec_request(&self.config, &*self.cache, &self.policy, request).await
}
}
+22 -31
View File
@@ -1,27 +1,32 @@
//! Episodes API
use core::time::Duration;
use std::rc::Rc;
use std::sync::RwLock;
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
use governor::Jitter;
use crate::Config;
use crate::api::exec_request;
use crate::model::Episode;
use crate::model::id::ShowId;
use crate::{Cache, CachePolicy, Config};
use super::{Error, make_request};
/// TMDB Episodes API client
pub struct Client {
config: Rc<Config>,
cache: Rc<dyn Cache>,
policy: Rc<RwLock<CachePolicy>>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
Self {
config,
cache,
policy,
}
}
}
@@ -34,30 +39,16 @@ impl Client {
episode: impl Into<EpisodeNumber>,
language: Option<&str>,
) -> Result<Episode, Error> {
self.config
.limiter
.until_ready_with_jitter(Jitter::new(
Duration::from_millis(0),
Duration::from_millis(50),
))
.await;
Ok(self
.config
.client
.execute(make_request(
&self.config,
&format!(
"/3/tv/{}/season/{}/episode/{}",
id.into().into_raw(),
season.into(),
episode.into()
),
language,
)?)
.await?
.error_for_status()?
.json()
.await?)
let request = make_request(
&self.config,
&format!(
"/3/tv/{}/season/{}/episode/{}",
id.into().into_raw(),
season.into(),
episode.into()
),
language,
)?;
exec_request(&self.config, &*self.cache, &self.policy, request).await
}
}
+65 -1
View File
@@ -1,9 +1,15 @@
//! TMDB API clients
use core::ops::Deref;
use core::time::Duration;
use std::sync::RwLock;
use governor::Jitter;
use reqwest::Request;
use reqwest::header;
use serde::de::DeserializeOwned;
use crate::Config;
use crate::{Cache, CachePolicy, Config};
pub mod collections;
pub mod episodes;
@@ -20,6 +26,9 @@ pub enum Error {
/// Reqwest error wrapper
#[error("reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),
/// Json error wrapper
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
}
fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<Request, Error> {
@@ -38,3 +47,58 @@ fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<R
Ok(builder.build()?)
}
async fn exec_request<T: DeserializeOwned>(
config: &Config,
cache: &dyn Cache,
policy: &RwLock<CachePolicy>,
request: Request,
) -> Result<T, Error> {
let (read_cache, write_cache) = if let Ok(guard) = policy.read() {
match guard.deref() {
CachePolicy::None => (None, None),
CachePolicy::Full => (Some(cache), Some(cache)),
CachePolicy::Read => (Some(cache), None),
CachePolicy::Update => (None, Some(cache)),
}
} else {
(None, None)
};
let path = request.url().path().to_owned();
// read the cache and fall back to reqwest
let mut response = None;
if let Some(cache) = read_cache {
response = cache.get(&path);
}
let needs_cache_write = response.is_none();
let response = match response {
Some(response) => response,
None => {
config
.limiter
.until_ready_with_jitter(Jitter::new(
Duration::from_millis(0),
Duration::from_millis(50),
))
.await;
config
.client
.execute(request)
.await?
.error_for_status()?
.bytes()
.await?
}
};
// write to the cache if needed
if let Some(cache) = write_cache
&& needs_cache_write
{
cache.set(&path, &response);
}
Ok(serde_json::from_slice(&response)?)
}
+17 -26
View File
@@ -1,25 +1,30 @@
//! Movies API
use core::time::Duration;
use std::rc::Rc;
use std::sync::RwLock;
use governor::Jitter;
use crate::Config;
use crate::api::exec_request;
use crate::model::Movie;
use crate::model::id::MovieId;
use crate::{Cache, CachePolicy, Config};
use super::{Error, make_request};
/// TMDB Movies API client
pub struct Client {
config: Rc<Config>,
cache: Rc<dyn Cache>,
policy: Rc<RwLock<CachePolicy>>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
Self {
config,
cache,
policy,
}
}
}
@@ -30,25 +35,11 @@ impl Client {
id: impl Into<MovieId>,
language: Option<&str>,
) -> Result<Movie, Error> {
self.config
.limiter
.until_ready_with_jitter(Jitter::new(
Duration::from_millis(0),
Duration::from_millis(50),
))
.await;
Ok(self
.config
.client
.execute(make_request(
&self.config,
&format!("/3/movie/{}", id.into().into_raw()),
language,
)?)
.await?
.error_for_status()?
.json()
.await?)
let request = make_request(
&self.config,
&format!("/3/movie/{}", id.into().into_raw()),
language,
)?;
exec_request(&self.config, &*self.cache, &self.policy, request).await
}
}
+17 -26
View File
@@ -1,27 +1,32 @@
//! Seasons API
use core::time::Duration;
use std::rc::Rc;
use std::sync::RwLock;
use flix_model::numbers::SeasonNumber;
use governor::Jitter;
use crate::Config;
use crate::api::exec_request;
use crate::model::Season;
use crate::model::id::ShowId;
use crate::{Cache, CachePolicy, Config};
use super::{Error, make_request};
/// TMDB Seasons API client
pub struct Client {
config: Rc<Config>,
cache: Rc<dyn Cache>,
policy: Rc<RwLock<CachePolicy>>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
Self {
config,
cache,
policy,
}
}
}
@@ -33,25 +38,11 @@ impl Client {
season: impl Into<SeasonNumber>,
language: Option<&str>,
) -> Result<Season, Error> {
self.config
.limiter
.until_ready_with_jitter(Jitter::new(
Duration::from_millis(0),
Duration::from_millis(50),
))
.await;
Ok(self
.config
.client
.execute(make_request(
&self.config,
&format!("/3/tv/{}/season/{}", id.into().into_raw(), season.into()),
language,
)?)
.await?
.error_for_status()?
.json()
.await?)
let request = make_request(
&self.config,
&format!("/3/tv/{}/season/{}", id.into().into_raw(), season.into()),
language,
)?;
exec_request(&self.config, &*self.cache, &self.policy, request).await
}
}
+17 -26
View File
@@ -1,25 +1,30 @@
//! Shows API
use core::time::Duration;
use std::rc::Rc;
use std::sync::RwLock;
use governor::Jitter;
use crate::Config;
use crate::api::exec_request;
use crate::model::Show;
use crate::model::id::ShowId;
use crate::{Cache, CachePolicy, Config};
use super::{Error, make_request};
/// TMDB Shows API client
pub struct Client {
config: Rc<Config>,
cache: Rc<dyn Cache>,
policy: Rc<RwLock<CachePolicy>>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
Self {
config,
cache,
policy,
}
}
}
@@ -30,25 +35,11 @@ impl Client {
id: impl Into<ShowId>,
language: Option<&str>,
) -> Result<Show, Error> {
self.config
.limiter
.until_ready_with_jitter(Jitter::new(
Duration::from_millis(0),
Duration::from_millis(50),
))
.await;
Ok(self
.config
.client
.execute(make_request(
&self.config,
&format!("/3/tv/{}", id.into().into_raw()),
language,
)?)
.await?
.error_for_status()?
.json()
.await?)
let request = make_request(
&self.config,
&format!("/3/tv/{}", id.into().into_raw()),
language,
)?;
exec_request(&self.config, &*self.cache, &self.policy, request).await
}
}
+83
View File
@@ -0,0 +1,83 @@
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use bytes::Bytes;
use redb::{Database, DatabaseError, ReadableDatabase, TableDefinition};
/// The client cache policy
pub enum CachePolicy {
/// Do not use a cache
None,
/// Use and update the cache
Full,
/// Use the cache but don't update it
Read,
/// Ignore the cache but update it
Update,
}
/// The trait representing a caching backend
pub trait Cache {
/// Get a cached value, or None
fn get(&self, query: &str) -> Option<Bytes>;
/// Set a value in the cache
fn set(&self, query: &str, response: &Bytes);
}
const TABLE: TableDefinition<&str, (u64, &[u8])> = TableDefinition::new("tmdb_responses");
/// A [Cache] implementation using [redb] as the backend
pub struct RedbCache {
db: Database,
}
impl RedbCache {
/// Create/open a [redb] database at the path
pub fn new(path: &Path) -> Result<Self, DatabaseError> {
Ok(Self {
db: Database::create(path)?,
})
}
/// Helper function allowing for `.ok()?`
fn write(&self, timestamp: u64, query: &str, response: &Bytes) -> Option<()> {
let write_txn = self.db.begin_write().ok()?;
{
let mut table = write_txn.open_table(TABLE).ok()?;
table
.insert(query, (timestamp, response.iter().as_slice()))
.ok()?;
}
write_txn.commit().ok()
}
}
impl Cache for RedbCache {
fn get(&self, query: &str) -> Option<Bytes> {
let read_txn = self.db.begin_read().ok()?;
let table = read_txn.open_table(TABLE).ok()?;
let result = table.get(query).ok()??;
let (timestamp, data) = result.value();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if now.saturating_sub(timestamp) >= 60 * 60 * 24 * 30 * 6 {
None
} else {
Some(Bytes::copy_from_slice(data))
}
}
fn set(&self, query: &str, response: &Bytes) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
self.write(now, query, response);
}
}
+33 -13
View File
@@ -1,6 +1,7 @@
use std::rc::Rc;
use std::sync::RwLock;
use crate::{Config, api};
use crate::{Cache, CachePolicy, Config, api};
/// The primary client that references all other clients
pub struct Client {
@@ -9,23 +10,42 @@ pub struct Client {
shows: api::shows::Client,
seasons: api::seasons::Client,
episodes: api::episodes::Client,
cache_policy: Rc<RwLock<CachePolicy>>,
}
impl Client {
/// Create a new client from a default configuration using the bearer token
pub fn new(bearer_token: String) -> Self {
Self::new_with_config(Config::new(bearer_token))
/// Create a new client with the given configuration
pub fn new(config: Config, cache: Rc<dyn Cache>, cache_policy: CachePolicy) -> Self {
let config = Rc::new(config);
let cache_policy = Rc::new(RwLock::new(cache_policy));
Self {
collections: api::collections::Client::new(
config.clone(),
cache.clone(),
cache_policy.clone(),
),
movies: api::movies::Client::new(config.clone(), cache.clone(), cache_policy.clone()),
shows: api::shows::Client::new(config.clone(), cache.clone(), cache_policy.clone()),
seasons: api::seasons::Client::new(config.clone(), cache.clone(), cache_policy.clone()),
episodes: api::episodes::Client::new(
config.clone(),
cache.clone(),
cache_policy.clone(),
),
cache_policy,
}
}
/// Create a new client with the given configuration
pub fn new_with_config(config: Config) -> Self {
let config = Rc::new(config);
Self {
collections: api::collections::Client::new(config.clone()),
movies: api::movies::Client::new(config.clone()),
shows: api::shows::Client::new(config.clone()),
seasons: api::seasons::Client::new(config.clone()),
episodes: api::episodes::Client::new(config.clone()),
/// Modify the [CachePolicy]
pub fn set_cache_policy(&self, new_policy: CachePolicy) {
match self.cache_policy.write() {
Ok(mut policy) => *policy = new_policy,
Err(mut poison) => {
**poison.get_mut() = new_policy;
self.cache_policy.clear_poison();
}
}
}
}
+3
View File
@@ -5,6 +5,9 @@
pub mod api;
pub mod model;
mod cache;
pub use cache::{Cache, CachePolicy, RedbCache};
mod client;
pub use client::Client;