You've already forked flix
Update all libraries to the new database format
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)?)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
pub mod api;
|
||||
pub mod model;
|
||||
|
||||
mod cache;
|
||||
pub use cache::{Cache, CachePolicy, RedbCache};
|
||||
|
||||
mod client;
|
||||
pub use client::Client;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user