You've already forked flix
Update dependencies and lints
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
[package]
|
||||
name = "flix-tmdb"
|
||||
version = "0.0.20"
|
||||
license-file.workspace = true
|
||||
|
||||
version = "0.1.0"
|
||||
description = "Clients and models for fetching data from TMDB"
|
||||
repository = "https://github.com/QuantumShade/flix"
|
||||
categories = []
|
||||
repository = "https://git.skrundz.dev/quantumshade/flix"
|
||||
keywords = ["flix"]
|
||||
categories = ["multimedia"]
|
||||
include = ["/src"]
|
||||
publish = true
|
||||
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license-file.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -32,6 +34,7 @@ sea-orm = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_test = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Collections API
|
||||
//! Collections API.
|
||||
|
||||
use std::rc::Rc;
|
||||
use alloc::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::api::exec_request;
|
||||
@@ -10,26 +10,38 @@ use crate::{Cache, CachePolicy, Config};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Collections API client
|
||||
/// TMDB Collections API client.
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
cache: Rc<dyn Cache>,
|
||||
policy: Rc<RwLock<CachePolicy>>,
|
||||
/// The client configuration.
|
||||
config: Arc<Config>,
|
||||
/// The request cache.
|
||||
cache: Arc<dyn Cache>,
|
||||
/// The cache policy to use for requests.
|
||||
policy: Arc<RwLock<CachePolicy>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
|
||||
/// Create a new client with the given configuration.
|
||||
#[inline]
|
||||
pub fn new(
|
||||
config: Arc<Config>,
|
||||
cache: Arc<dyn Cache>,
|
||||
policy: Arc<RwLock<CachePolicy>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
cache,
|
||||
policy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the details of the collection refered to by ID
|
||||
/// Fetch the details of the collection refered to by ID.
|
||||
///
|
||||
/// # Errors
|
||||
/// See [Error] for failure modes.
|
||||
#[inline]
|
||||
#[expect(clippy::impl_trait_in_params, reason = "turbofish is not expected")]
|
||||
pub async fn get_details(
|
||||
&self,
|
||||
id: impl Into<CollectionId>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Episodes API
|
||||
//! Episodes API.
|
||||
|
||||
use std::rc::Rc;
|
||||
use alloc::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use flix_model::numbers::{EpisodeNumber, SeasonNumber};
|
||||
@@ -12,26 +12,38 @@ use crate::{Cache, CachePolicy, Config};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Episodes API client
|
||||
/// TMDB Episodes API client.
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
cache: Rc<dyn Cache>,
|
||||
policy: Rc<RwLock<CachePolicy>>,
|
||||
/// The client configuration.
|
||||
config: Arc<Config>,
|
||||
/// The request cache.
|
||||
cache: Arc<dyn Cache>,
|
||||
/// The cache policy to use for requests.
|
||||
policy: Arc<RwLock<CachePolicy>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
|
||||
/// Create a new client with the given configuration.
|
||||
#[inline]
|
||||
pub fn new(
|
||||
config: Arc<Config>,
|
||||
cache: Arc<dyn Cache>,
|
||||
policy: Arc<RwLock<CachePolicy>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
cache,
|
||||
policy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the details of the episode refered to by ID, season number and episode number
|
||||
/// Fetch the details of the episode refered to by ID, season number and episode number.
|
||||
///
|
||||
/// # Errors
|
||||
/// See [Error] for failure modes.
|
||||
#[inline]
|
||||
#[expect(clippy::impl_trait_in_params, reason = "turbofish is not expected")]
|
||||
pub async fn get_details(
|
||||
&self,
|
||||
id: impl Into<ShowId>,
|
||||
|
||||
+49
-35
@@ -1,6 +1,5 @@
|
||||
//! TMDB API clients
|
||||
//! TMDB API clients.
|
||||
|
||||
use core::ops::Deref;
|
||||
use core::time::Duration;
|
||||
use std::sync::RwLock;
|
||||
|
||||
@@ -17,20 +16,26 @@ pub mod movies;
|
||||
pub mod seasons;
|
||||
pub mod shows;
|
||||
|
||||
/// A generic error wrapping Url and Reqwest errors
|
||||
/// A generic error wrapping Url and Reqwest errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[expect(clippy::error_impl_error, reason = "Error is a good name here")]
|
||||
#[expect(clippy::exhaustive_enums, reason = "unlikely to add new variants")]
|
||||
pub enum Error {
|
||||
/// Url error wrapper
|
||||
/// Url error wrapper.
|
||||
#[error("url parse error: {0}")]
|
||||
Url(#[from] url::ParseError),
|
||||
/// Reqwest error wrapper
|
||||
/// Reqwest error wrapper.
|
||||
#[error("reqwest error: {0}")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
/// Json error wrapper
|
||||
/// Json error wrapper.
|
||||
#[error("json error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Turn a [Config] and `path` into a [Request].
|
||||
///
|
||||
/// # Errors
|
||||
/// See [Error] for failure modes.
|
||||
fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<Request, Error> {
|
||||
let url = config.base_url.join(path)?;
|
||||
|
||||
@@ -38,7 +43,7 @@ fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<R
|
||||
header::AUTHORIZATION,
|
||||
format!("Bearer {}", config.bearer_token),
|
||||
);
|
||||
if let Some(ref user_agent) = config.user_agent {
|
||||
if let Some(user_agent) = &config.user_agent {
|
||||
builder = builder.header(header::USER_AGENT, user_agent);
|
||||
}
|
||||
if let Some(language) = language {
|
||||
@@ -48,22 +53,22 @@ fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<R
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
|
||||
/// Execute a [Request] and deserialize the response.
|
||||
///
|
||||
/// # Errors
|
||||
/// See [Error] for failure modes.
|
||||
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 (read_cache, write_cache) = policy.read().map_or((None, None), |guard| match *guard {
|
||||
CachePolicy::None => (None, None),
|
||||
CachePolicy::Full => (Some(cache), Some(cache)),
|
||||
CachePolicy::Read => (Some(cache), None),
|
||||
CachePolicy::Update => (None, Some(cache)),
|
||||
});
|
||||
|
||||
let path = request.url().path().to_owned();
|
||||
|
||||
@@ -73,24 +78,23 @@ async fn exec_request<T: DeserializeOwned>(
|
||||
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?
|
||||
}
|
||||
let response = if let Some(response) = response {
|
||||
response
|
||||
} else {
|
||||
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
|
||||
@@ -102,3 +106,13 @@ async fn exec_request<T: DeserializeOwned>(
|
||||
|
||||
Ok(serde_json::from_slice(&response)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Error;
|
||||
|
||||
#[test]
|
||||
fn use_types() {
|
||||
drop(Error::Url(url::ParseError::Overflow));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Movies API
|
||||
//! Movies API.
|
||||
|
||||
use std::rc::Rc;
|
||||
use alloc::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::api::exec_request;
|
||||
@@ -10,26 +10,38 @@ use crate::{Cache, CachePolicy, Config};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Movies API client
|
||||
/// TMDB Movies API client.
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
cache: Rc<dyn Cache>,
|
||||
policy: Rc<RwLock<CachePolicy>>,
|
||||
/// The client configuration.
|
||||
config: Arc<Config>,
|
||||
/// The request cache.
|
||||
cache: Arc<dyn Cache>,
|
||||
/// The cache policy to use for requests.
|
||||
policy: Arc<RwLock<CachePolicy>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
|
||||
/// Create a new client with the given configuration.
|
||||
#[inline]
|
||||
pub fn new(
|
||||
config: Arc<Config>,
|
||||
cache: Arc<dyn Cache>,
|
||||
policy: Arc<RwLock<CachePolicy>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
cache,
|
||||
policy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the details of the movie refered to by ID
|
||||
/// Fetch the details of the movie refered to by ID.
|
||||
///
|
||||
/// # Errors
|
||||
/// See [Error] for failure modes.
|
||||
#[inline]
|
||||
#[expect(clippy::impl_trait_in_params, reason = "turbofish is not expected")]
|
||||
pub async fn get_details(
|
||||
&self,
|
||||
id: impl Into<MovieId>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Seasons API
|
||||
//! Seasons API.
|
||||
|
||||
use std::rc::Rc;
|
||||
use alloc::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use flix_model::numbers::SeasonNumber;
|
||||
@@ -12,26 +12,38 @@ use crate::{Cache, CachePolicy, Config};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Seasons API client
|
||||
/// TMDB Seasons API client.
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
cache: Rc<dyn Cache>,
|
||||
policy: Rc<RwLock<CachePolicy>>,
|
||||
/// The client configuration.
|
||||
config: Arc<Config>,
|
||||
/// The request cache.
|
||||
cache: Arc<dyn Cache>,
|
||||
/// The cache policy to use for requests.
|
||||
policy: Arc<RwLock<CachePolicy>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
|
||||
/// Create a new client with the given configuration.
|
||||
#[inline]
|
||||
pub fn new(
|
||||
config: Arc<Config>,
|
||||
cache: Arc<dyn Cache>,
|
||||
policy: Arc<RwLock<CachePolicy>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
cache,
|
||||
policy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the details of the season refered to by ID and season number
|
||||
/// Fetch the details of the season refered to by ID and season number.
|
||||
///
|
||||
/// # Errors
|
||||
/// See [Error] for failure modes.
|
||||
#[inline]
|
||||
#[expect(clippy::impl_trait_in_params, reason = "turbofish is not expected")]
|
||||
pub async fn get_details(
|
||||
&self,
|
||||
id: impl Into<ShowId>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Shows API
|
||||
//! Shows API.
|
||||
|
||||
use std::rc::Rc;
|
||||
use alloc::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::api::exec_request;
|
||||
@@ -10,26 +10,38 @@ use crate::{Cache, CachePolicy, Config};
|
||||
|
||||
use super::{Error, make_request};
|
||||
|
||||
/// TMDB Shows API client
|
||||
/// TMDB Shows API client.
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
config: Rc<Config>,
|
||||
cache: Rc<dyn Cache>,
|
||||
policy: Rc<RwLock<CachePolicy>>,
|
||||
/// The client configuration.
|
||||
config: Arc<Config>,
|
||||
/// The request cache.
|
||||
cache: Arc<dyn Cache>,
|
||||
/// The cache policy to use for requests.
|
||||
policy: Arc<RwLock<CachePolicy>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Rc<Config>, cache: Rc<dyn Cache>, policy: Rc<RwLock<CachePolicy>>) -> Self {
|
||||
/// Create a new client with the given configuration.
|
||||
#[inline]
|
||||
pub fn new(
|
||||
config: Arc<Config>,
|
||||
cache: Arc<dyn Cache>,
|
||||
policy: Arc<RwLock<CachePolicy>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
cache,
|
||||
policy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Fetch the details of the show refered to by ID
|
||||
/// Fetch the details of the show refered to by ID.
|
||||
///
|
||||
/// # Errors
|
||||
/// See [Error] for failure modes.
|
||||
#[inline]
|
||||
#[expect(clippy::impl_trait_in_params, reason = "turbofish is not expected")]
|
||||
pub async fn get_details(
|
||||
&self,
|
||||
id: impl Into<ShowId>,
|
||||
|
||||
+35
-22
@@ -1,58 +1,72 @@
|
||||
//! Caching related types.
|
||||
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use bytes::Bytes;
|
||||
use redb::{Database, DatabaseError, ReadableDatabase, TableDefinition};
|
||||
use redb::{Database, DatabaseError, ReadableDatabase as _, TableDefinition};
|
||||
|
||||
/// The client cache policy
|
||||
/// The client cache policy.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum CachePolicy {
|
||||
/// Do not use a cache
|
||||
/// Do not use a cache.
|
||||
None,
|
||||
/// Use and update the cache
|
||||
/// Use and update the cache.
|
||||
Full,
|
||||
/// Use the cache but don't update it
|
||||
/// Use the cache but don't update it.
|
||||
Read,
|
||||
/// Ignore the cache but update it
|
||||
/// Ignore the cache but update it.
|
||||
Update,
|
||||
}
|
||||
|
||||
/// The trait representing a caching backend
|
||||
pub trait Cache {
|
||||
/// Get a cached value, or None
|
||||
/// The trait representing a caching backend.
|
||||
pub trait Cache: core::fmt::Debug + Send + Sync {
|
||||
/// Get a cached value, or None.
|
||||
fn get(&self, query: &str) -> Option<Bytes>;
|
||||
/// Set a value in the cache
|
||||
/// Set a value in the cache.
|
||||
fn set(&self, query: &str, response: &Bytes);
|
||||
}
|
||||
|
||||
const TABLE: TableDefinition<&str, (u64, &[u8])> = TableDefinition::new("tmdb_responses");
|
||||
/// The [`TableDefinition`] for TMDB response data.
|
||||
const TABLE: TableDefinition<'_, &str, (u64, &[u8])> = TableDefinition::new("tmdb_responses");
|
||||
|
||||
/// A [Cache] implementation using [redb] as the backend
|
||||
/// A [Cache] implementation using [redb] as the backend.
|
||||
#[derive(Debug)]
|
||||
pub struct RedbCache {
|
||||
/// The underlying database.
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl RedbCache {
|
||||
/// Create/open a [redb] database at the path
|
||||
/// Create/open a [redb] database at the path.
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails if creating/opening the database returns an error.
|
||||
#[inline]
|
||||
pub fn new(path: &Path) -> Result<Self, DatabaseError> {
|
||||
Ok(Self {
|
||||
db: Database::create(path)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function allowing for `.ok()?`
|
||||
/// 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()?;
|
||||
drop(
|
||||
table
|
||||
.insert(query, (timestamp, response.iter().as_slice()))
|
||||
.ok()?,
|
||||
);
|
||||
}
|
||||
write_txn.commit().ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Cache for RedbCache {
|
||||
#[inline]
|
||||
fn get(&self, query: &str) -> Option<Bytes> {
|
||||
let read_txn = self.db.begin_read().ok()?;
|
||||
let table = read_txn.open_table(TABLE).ok()?;
|
||||
@@ -62,8 +76,7 @@ impl Cache for RedbCache {
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
.map_or(0, |d| d.as_secs());
|
||||
|
||||
if now.saturating_sub(timestamp) >= 60 * 60 * 24 * 30 * 6 {
|
||||
None
|
||||
@@ -72,12 +85,12 @@ impl Cache for RedbCache {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set(&self, query: &str, response: &Bytes) {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
.map_or(0, |d| d.as_secs());
|
||||
|
||||
self.write(now, query, response);
|
||||
_ = self.write(now, query, response);
|
||||
}
|
||||
}
|
||||
|
||||
+57
-30
@@ -1,44 +1,63 @@
|
||||
use std::rc::Rc;
|
||||
//! TMDB client.
|
||||
|
||||
use alloc::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::{Cache, CachePolicy, Config, api};
|
||||
|
||||
/// The primary client that references all other clients
|
||||
/// The primary client that references all other clients.
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
/// The collection API client.
|
||||
collections: api::collections::Client,
|
||||
/// The movie API client.
|
||||
movies: api::movies::Client,
|
||||
/// The show API client.
|
||||
shows: api::shows::Client,
|
||||
/// The season API client.
|
||||
seasons: api::seasons::Client,
|
||||
/// The episode API client.
|
||||
episodes: api::episodes::Client,
|
||||
|
||||
cache_policy: Rc<RwLock<CachePolicy>>,
|
||||
/// The cache policy to use for all clients.
|
||||
cache_policy: Arc<RwLock<CachePolicy>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// 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));
|
||||
/// Create a new client with the given configuration.
|
||||
#[inline]
|
||||
pub fn new(config: Config, cache: Arc<dyn Cache>, cache_policy: CachePolicy) -> Self {
|
||||
let config = Arc::new(config);
|
||||
let cache_policy = Arc::new(RwLock::new(cache_policy));
|
||||
Self {
|
||||
collections: api::collections::Client::new(
|
||||
config.clone(),
|
||||
cache.clone(),
|
||||
cache_policy.clone(),
|
||||
Arc::clone(&config),
|
||||
Arc::clone(&cache),
|
||||
Arc::clone(&cache_policy),
|
||||
),
|
||||
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(),
|
||||
movies: api::movies::Client::new(
|
||||
Arc::clone(&config),
|
||||
Arc::clone(&cache),
|
||||
Arc::clone(&cache_policy),
|
||||
),
|
||||
shows: api::shows::Client::new(
|
||||
Arc::clone(&config),
|
||||
Arc::clone(&cache),
|
||||
Arc::clone(&cache_policy),
|
||||
),
|
||||
seasons: api::seasons::Client::new(
|
||||
Arc::clone(&config),
|
||||
Arc::clone(&cache),
|
||||
Arc::clone(&cache_policy),
|
||||
),
|
||||
episodes: api::episodes::Client::new(config, cache, Arc::clone(&cache_policy)),
|
||||
|
||||
cache_policy,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the [CachePolicy]
|
||||
/// Modify the [`CachePolicy`].
|
||||
#[inline]
|
||||
pub fn set_cache_policy(&self, new_policy: CachePolicy) {
|
||||
match self.cache_policy.write() {
|
||||
Ok(mut policy) => *policy = new_policy,
|
||||
@@ -48,31 +67,39 @@ impl Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Access the Collections API
|
||||
pub fn collections(&self) -> &api::collections::Client {
|
||||
/// Access the Collections API.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn collections(&self) -> &api::collections::Client {
|
||||
&self.collections
|
||||
}
|
||||
|
||||
/// Access the Movies API
|
||||
pub fn movies(&self) -> &api::movies::Client {
|
||||
/// Access the Movies API.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn movies(&self) -> &api::movies::Client {
|
||||
&self.movies
|
||||
}
|
||||
|
||||
/// Access the Shows API
|
||||
pub fn shows(&self) -> &api::shows::Client {
|
||||
/// Access the Shows API.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn shows(&self) -> &api::shows::Client {
|
||||
&self.shows
|
||||
}
|
||||
|
||||
/// Access the Seasons API
|
||||
pub fn seasons(&self) -> &api::seasons::Client {
|
||||
/// Access the Seasons API.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn seasons(&self) -> &api::seasons::Client {
|
||||
&self.seasons
|
||||
}
|
||||
|
||||
/// Access the Episodes API
|
||||
pub fn episodes(&self) -> &api::episodes::Client {
|
||||
/// Access the Episodes API.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn episodes(&self) -> &api::episodes::Client {
|
||||
&self.episodes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Client configuration.
|
||||
|
||||
use governor::clock::MonotonicClock;
|
||||
use governor::state::{InMemoryState, NotKeyed};
|
||||
use governor::{Quota, RateLimiter};
|
||||
@@ -5,27 +7,31 @@ use nonzero_ext::nonzero;
|
||||
use url::Url;
|
||||
use url_macro::url;
|
||||
|
||||
/// The client configuration
|
||||
/// The client configuration.
|
||||
#[derive(Debug)]
|
||||
#[expect(clippy::exhaustive_structs, reason = "must be constructed in full")]
|
||||
pub struct Config {
|
||||
/// The base URL of the API
|
||||
/// The base URL of the API.
|
||||
pub base_url: Url,
|
||||
/// The reqwest client that is used for every request
|
||||
/// The reqwest client that is used for every request.
|
||||
pub client: reqwest::Client,
|
||||
/// The rate limiter to use for the client
|
||||
/// The rate limiter to use for the client.
|
||||
pub limiter: RateLimiter<NotKeyed, InMemoryState, MonotonicClock>,
|
||||
/// The bearer token for readonly access to the API
|
||||
/// The bearer token for readonly access to the API.
|
||||
pub bearer_token: String,
|
||||
/// An optional user agent string to provide to the API
|
||||
/// An optional user agent string to provide to the API.
|
||||
pub user_agent: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create a new configuration using the provided bearer token
|
||||
/// Create a new configuration using the provided bearer token.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn new(bearer_token: String) -> Self {
|
||||
Self {
|
||||
base_url: url!("https://api.themoviedb.org"),
|
||||
client: reqwest::Client::new(),
|
||||
limiter: RateLimiter::direct(Quota::per_second(nonzero!(30u32))),
|
||||
limiter: RateLimiter::direct(Quota::per_second(nonzero!(30_u32))),
|
||||
bearer_token,
|
||||
user_agent: None,
|
||||
}
|
||||
|
||||
+20
-1
@@ -1,7 +1,9 @@
|
||||
//! flix-tmdb provides clients and models for fetching data from TMDB
|
||||
//! flix-tmdb provides clients and models for fetching data from TMDB.
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub mod api;
|
||||
pub mod model;
|
||||
|
||||
@@ -13,3 +15,20 @@ pub use client::Client;
|
||||
|
||||
mod config;
|
||||
pub use config::Config;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use super::{CachePolicy, Client, Config, RedbCache};
|
||||
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||
fn use_types() {
|
||||
let tmp = tempfile::tempdir().expect("temporary directory exists");
|
||||
let config = Config::new(String::new());
|
||||
let cache = Arc::new(RedbCache::new(&tmp.path().join("cache.redb")).expect("redb"));
|
||||
drop(Client::new(config, cache, CachePolicy::Full));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,48 @@
|
||||
//! TMDB collection model.
|
||||
|
||||
use super::id::{CollectionId, MovieId};
|
||||
|
||||
/// A deserialized Collection from the TMDB API
|
||||
/// A deserialized Collection from the TMDB API.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct Collection {
|
||||
/// The collection's TMDB ID
|
||||
/// The collection's TMDB ID.
|
||||
pub id: CollectionId,
|
||||
/// The collection's title
|
||||
/// The collection's title.
|
||||
#[serde(rename = "name")]
|
||||
pub title: String,
|
||||
/// The collection's overview
|
||||
/// The collection's overview.
|
||||
pub overview: String,
|
||||
/// The list of movies that are part of this collection
|
||||
/// The list of movies that are part of this collection.
|
||||
#[serde(rename = "parts")]
|
||||
pub movies: Vec<Item>,
|
||||
}
|
||||
|
||||
/// A deserialized collection item from the TMDB API
|
||||
/// A deserialized collection item from the TMDB API.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct Item {
|
||||
/// The movie's TMDB ID
|
||||
/// The movie's TMDB ID.
|
||||
pub id: MovieId,
|
||||
/// The movie's title
|
||||
/// The movie's title.
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Collection, CollectionId, Item, MovieId};
|
||||
|
||||
#[test]
|
||||
fn use_types() {
|
||||
drop(Collection {
|
||||
id: CollectionId::from_raw(0),
|
||||
title: String::new(),
|
||||
overview: String::new(),
|
||||
movies: Vec::new(),
|
||||
});
|
||||
drop(Item {
|
||||
id: MovieId::from_raw(0),
|
||||
title: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! TMDB episode model.
|
||||
|
||||
use core::time::Duration;
|
||||
|
||||
use flix_model::numbers::EpisodeNumber;
|
||||
@@ -7,22 +9,44 @@ use url::Url;
|
||||
|
||||
use super::{duration_from_minutes, still_url_from_path};
|
||||
|
||||
/// A deserialized Episode from the TMDB API
|
||||
/// A deserialized Episode from the TMDB API.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct Episode {
|
||||
/// The episode's number
|
||||
/// The episode's number.
|
||||
pub episode_number: EpisodeNumber,
|
||||
/// The episode's title
|
||||
/// The episode's title.
|
||||
#[serde(rename = "name")]
|
||||
pub title: String,
|
||||
/// The episode's overview
|
||||
/// The episode's overview.
|
||||
pub overview: String,
|
||||
/// The episode's air date
|
||||
/// The episode's air date.
|
||||
pub air_date: NaiveDate,
|
||||
/// The episode's runtime
|
||||
/// The episode's runtime.
|
||||
#[serde(deserialize_with = "duration_from_minutes")]
|
||||
pub runtime: Duration,
|
||||
/// The episode's still path
|
||||
/// The episode's still path.
|
||||
#[serde(deserialize_with = "still_url_from_path")]
|
||||
pub still_path: Option<Url>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::time::Duration;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::{Episode, EpisodeNumber};
|
||||
|
||||
#[test]
|
||||
fn use_types() {
|
||||
drop(Episode {
|
||||
episode_number: EpisodeNumber::new(0),
|
||||
title: String::new(),
|
||||
overview: String::new(),
|
||||
air_date: NaiveDate::default(),
|
||||
runtime: Duration::default(),
|
||||
still_path: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+63
-24
@@ -1,4 +1,6 @@
|
||||
//! Typed TMDB IDs
|
||||
//! Typed TMDB IDs.
|
||||
|
||||
#![expect(clippy::module_name_repetitions, reason = "ID types end with Id")]
|
||||
|
||||
use core::cmp::Ordering;
|
||||
use core::fmt;
|
||||
@@ -10,21 +12,25 @@ use sea_orm::sea_query::{ArrayType, Nullable, ValueType, ValueTypeErr};
|
||||
#[cfg(feature = "sea-orm")]
|
||||
use sea_orm::{ColIdx, ColumnType, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, Value};
|
||||
|
||||
/// The internal representation used by TMDB
|
||||
/// The internal representation used by TMDB.
|
||||
pub type TmdbRepr = u32;
|
||||
|
||||
/// An opaque type representing a TMDB ID
|
||||
/// An opaque type representing a TMDB ID.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(transparent)]
|
||||
#[repr(transparent)]
|
||||
pub struct Id<T> {
|
||||
/// Internal ID representation.
|
||||
id: TmdbRepr,
|
||||
/// Marker to indicate an invariant T.
|
||||
#[serde(skip_serializing, default)]
|
||||
_phantom: PhantomData<T>,
|
||||
_phantom: PhantomData<fn(T) -> T>,
|
||||
}
|
||||
|
||||
// Manual implementation since `T: Clone` is not required
|
||||
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||
impl<T> Clone for Id<T> {
|
||||
#[inline]
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
@@ -34,31 +40,40 @@ impl<T> Clone for Id<T> {
|
||||
impl<T> Copy for Id<T> {}
|
||||
|
||||
// Manual implementation since `T: PartialEq` is not required
|
||||
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||
impl<T> PartialEq for Id<T> {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
// Manual implementation since `T: Eq` is not required
|
||||
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||
impl<T> Eq for Id<T> {}
|
||||
|
||||
// Manual implementation since `T: PartialOrd` is not required
|
||||
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||
impl<T> PartialOrd for Id<T> {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
// Manual implementation since `T: Ord` is not required
|
||||
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||
impl<T> Ord for Id<T> {
|
||||
#[inline]
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.id.cmp(&other.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Manual implementation since `T: Hash` is not required
|
||||
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||
impl<T> Hash for Id<T> {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
@@ -66,6 +81,8 @@ impl<T> Hash for Id<T> {
|
||||
|
||||
impl<T> Id<T> {
|
||||
/// Allows the conversion from a raw value to [Id], though the use is discouraged.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn from_raw(raw: TmdbRepr) -> Self {
|
||||
Self {
|
||||
id: raw,
|
||||
@@ -74,13 +91,16 @@ impl<T> Id<T> {
|
||||
}
|
||||
|
||||
/// Allows extracting the raw value, though the use is discouraged.
|
||||
pub fn into_raw(self) -> TmdbRepr {
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn into_raw(self) -> TmdbRepr {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for Id<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Id")
|
||||
.field("T", &core::any::type_name::<T>())
|
||||
.field("id", &self.id)
|
||||
@@ -89,7 +109,9 @@ impl<T> fmt::Debug for Id<T> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "sea-orm")]
|
||||
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||
impl<T> ValueType for Id<T> {
|
||||
#[inline]
|
||||
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
|
||||
<TmdbRepr as ValueType>::try_from(v).map(|id| Self {
|
||||
id,
|
||||
@@ -97,14 +119,17 @@ impl<T> ValueType for Id<T> {
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn type_name() -> String {
|
||||
format!("Id<{}>", &core::any::type_name::<T>())
|
||||
format!("Id<{}>", core::any::type_name::<T>())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn array_type() -> ArrayType {
|
||||
TmdbRepr::array_type()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn column_type() -> ColumnType {
|
||||
TmdbRepr::column_type()
|
||||
}
|
||||
@@ -112,13 +137,16 @@ impl<T> ValueType for Id<T> {
|
||||
|
||||
#[cfg(feature = "sea-orm")]
|
||||
impl<T> From<Id<T>> for Value {
|
||||
#[inline]
|
||||
fn from(value: Id<T>) -> Self {
|
||||
value.id.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sea-orm")]
|
||||
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||
impl<T> TryGetable for Id<T> {
|
||||
#[inline]
|
||||
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
|
||||
TmdbRepr::try_get_by(res, index).map(|id| Self {
|
||||
id,
|
||||
@@ -129,6 +157,7 @@ impl<T> TryGetable for Id<T> {
|
||||
|
||||
#[cfg(feature = "sea-orm")]
|
||||
impl<T> TryFromU64 for Id<T> {
|
||||
#[inline]
|
||||
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
|
||||
TmdbRepr::try_from_u64(n).map(|id| Self {
|
||||
id,
|
||||
@@ -139,6 +168,7 @@ impl<T> TryFromU64 for Id<T> {
|
||||
|
||||
#[cfg(feature = "sea-orm")]
|
||||
impl<T> Nullable for Id<T> {
|
||||
#[inline]
|
||||
fn null() -> Value {
|
||||
TmdbRepr::null()
|
||||
}
|
||||
@@ -146,34 +176,34 @@ impl<T> Nullable for Id<T> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[cfg(feature = "sea-orm")]
|
||||
fn test_sea_orm() {
|
||||
#[expect(dead_code, reason = "structs test derive macros")]
|
||||
mod test_seaorm {
|
||||
use sea_orm::{
|
||||
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
|
||||
EnumIter, PrimaryKeyTrait,
|
||||
};
|
||||
|
||||
use super::Id;
|
||||
use super::super::Id;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "ids")]
|
||||
pub struct Model {
|
||||
#[expect(clippy::use_self, reason = "derive macros cause Self to be invalid")]
|
||||
struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
id: Id<Model>,
|
||||
nullable: Option<Id<Model>>,
|
||||
}
|
||||
|
||||
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
enum Relation {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serde() {
|
||||
fn serde() {
|
||||
use super::Id;
|
||||
|
||||
let id: Id<()> = Id::from_raw(1234);
|
||||
@@ -181,61 +211,70 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for the raw ID representation
|
||||
pub use self::TmdbRepr as RawId;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum Collection {}
|
||||
/// Type alias for a collection ID
|
||||
/// Type alias for a collection ID.
|
||||
pub type CollectionId = Id<Collection>;
|
||||
|
||||
impl From<CollectionId> for flix_model::id::CollectionId {
|
||||
#[inline]
|
||||
fn from(value: CollectionId) -> Self {
|
||||
Self::from_raw(value.into_raw().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<flix_model::id::CollectionId> for CollectionId {
|
||||
type Error = <RawId as TryFrom<flix_model::id::RawId>>::Error;
|
||||
type Error = <TmdbRepr as TryFrom<flix_model::id::RawId>>::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(value: flix_model::id::CollectionId) -> Result<Self, Self::Error> {
|
||||
value.into_raw().try_into().map(Self::from_raw)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum Movie {}
|
||||
/// Type alias for a movie ID
|
||||
/// Type alias for a movie ID.
|
||||
pub type MovieId = Id<Movie>;
|
||||
|
||||
impl From<MovieId> for flix_model::id::MovieId {
|
||||
#[inline]
|
||||
fn from(value: MovieId) -> Self {
|
||||
Self::from_raw(value.into_raw().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<flix_model::id::MovieId> for MovieId {
|
||||
type Error = <RawId as TryFrom<flix_model::id::RawId>>::Error;
|
||||
type Error = <TmdbRepr as TryFrom<flix_model::id::RawId>>::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(value: flix_model::id::MovieId) -> Result<Self, Self::Error> {
|
||||
value.into_raw().try_into().map(Self::from_raw)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum Show {}
|
||||
/// Type alias for a show ID
|
||||
/// Type alias for a show ID.
|
||||
pub type ShowId = Id<Show>;
|
||||
|
||||
impl From<ShowId> for flix_model::id::ShowId {
|
||||
#[inline]
|
||||
fn from(value: ShowId) -> Self {
|
||||
Self::from_raw(value.into_raw().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<flix_model::id::ShowId> for ShowId {
|
||||
type Error = <RawId as TryFrom<flix_model::id::RawId>>::Error;
|
||||
type Error = <TmdbRepr as TryFrom<flix_model::id::RawId>>::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(value: flix_model::id::ShowId) -> Result<Self, Self::Error> {
|
||||
value.into_raw().try_into().map(Self::from_raw)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Deserializable types from the TMDB API
|
||||
//! Deserializable types from the TMDB API.
|
||||
|
||||
use core::str::FromStr;
|
||||
use core::str::FromStr as _;
|
||||
use core::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use serde::{Deserialize as _, Deserializer};
|
||||
use url::Url;
|
||||
|
||||
pub mod id;
|
||||
@@ -20,14 +20,22 @@ pub use movie::*;
|
||||
pub use season::*;
|
||||
pub use show::*;
|
||||
|
||||
/// Deserializer for converting integer minutes to a [Duration].
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails if the value being deserialized isn't a [u64].
|
||||
fn duration_from_minutes<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let minutes = u64::deserialize(deserializer).unwrap_or(0);
|
||||
let minutes = u64::deserialize(deserializer)?;
|
||||
Ok(Duration::from_secs(minutes.saturating_mul(60)))
|
||||
}
|
||||
|
||||
/// Deserializer for converting a string to a [Url].
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails if the value being deserialized isn't a [`&str`].
|
||||
fn still_url_from_path<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
@@ -38,10 +46,6 @@ where
|
||||
let path = Option::<&str>::deserialize(deserializer)?;
|
||||
|
||||
Ok(path.and_then(|path| {
|
||||
Url::from_str(&format!(
|
||||
"{}{}{}",
|
||||
TMDB_IMAGE_BASE, TMDB_IMAGE_QUALITY, path
|
||||
))
|
||||
.ok()
|
||||
Url::from_str(&format!("{TMDB_IMAGE_BASE}{TMDB_IMAGE_QUALITY}{path}")).ok()
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! TMDB movie model.
|
||||
|
||||
use core::time::Duration;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
@@ -5,33 +7,35 @@ use chrono::NaiveDate;
|
||||
use super::duration_from_minutes;
|
||||
use super::id::{CollectionId, MovieId};
|
||||
|
||||
/// A deserialized Movie from the TMDB API
|
||||
/// A deserialized Movie from the TMDB API.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct Movie {
|
||||
/// The movie's TMDB ID
|
||||
/// The movie's TMDB ID.
|
||||
pub id: MovieId,
|
||||
/// The movie's collection, if it exists
|
||||
/// The movie's collection, if it exists.
|
||||
#[serde(rename = "belongs_to_collection")]
|
||||
pub collection: Option<InCollection>,
|
||||
/// The movie's title
|
||||
/// The movie's title.
|
||||
pub title: String,
|
||||
/// The movie's tagline
|
||||
/// The movie's tagline.
|
||||
pub tagline: String,
|
||||
/// The movie's overview
|
||||
/// The movie's overview.
|
||||
pub overview: String,
|
||||
/// The movie's release date
|
||||
/// The movie's release date.
|
||||
pub release_date: NaiveDate,
|
||||
/// The movie's runtime
|
||||
/// The movie's runtime.
|
||||
#[serde(deserialize_with = "duration_from_minutes")]
|
||||
pub runtime: Duration,
|
||||
}
|
||||
|
||||
/// A deserialized movie's collection from the TMDB API
|
||||
/// A deserialized movie's collection from the TMDB API.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct InCollection {
|
||||
/// The collection's TMDB ID
|
||||
/// The collection's TMDB ID.
|
||||
pub id: CollectionId,
|
||||
/// The collection's title
|
||||
/// The collection's title.
|
||||
#[serde(rename = "name")]
|
||||
pub title: String,
|
||||
}
|
||||
@@ -43,3 +47,28 @@ pub struct InCollection {
|
||||
// TODO: Company
|
||||
// pub companies: Vec<Company>
|
||||
// where: struct Company { id, name }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::time::Duration;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::{CollectionId, InCollection, Movie, MovieId};
|
||||
|
||||
#[test]
|
||||
fn use_types() {
|
||||
drop(Movie {
|
||||
id: MovieId::from_raw(0),
|
||||
collection: Some(InCollection {
|
||||
id: CollectionId::from_raw(0),
|
||||
title: String::new(),
|
||||
}),
|
||||
title: String::new(),
|
||||
tagline: String::new(),
|
||||
overview: String::new(),
|
||||
release_date: NaiveDate::default(),
|
||||
runtime: Duration::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,46 @@
|
||||
//! TMDB season model.
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use flix_model::numbers::SeasonNumber;
|
||||
|
||||
/// A deserialized Season from the TMDB API
|
||||
/// A deserialized Season from the TMDB API.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct Season {
|
||||
/// The season's number
|
||||
/// The season's number.
|
||||
pub season_number: SeasonNumber,
|
||||
/// The season's title
|
||||
/// The season's title.
|
||||
#[serde(rename = "name")]
|
||||
pub title: String,
|
||||
/// The season's overview
|
||||
/// The season's overview.
|
||||
pub overview: String,
|
||||
/// The season's air date
|
||||
/// The season's air date.
|
||||
pub air_date: NaiveDate,
|
||||
/// The number of episodes in this season
|
||||
/// The number of episodes in this season.
|
||||
pub episodes: Vec<FakeEpisode>,
|
||||
}
|
||||
|
||||
/// A placeholder struct for parsing the episodes list for a season
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
/// A placeholder struct for parsing the episodes list for a season.
|
||||
#[derive(Debug, Clone, Copy, serde::Deserialize)]
|
||||
#[non_exhaustive]
|
||||
#[expect(clippy::empty_structs_with_brackets, reason = "might add fields later")]
|
||||
pub struct FakeEpisode {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::{Season, SeasonNumber};
|
||||
|
||||
#[test]
|
||||
fn use_types() {
|
||||
drop(Season {
|
||||
season_number: SeasonNumber::new(0),
|
||||
title: String::new(),
|
||||
overview: String::new(),
|
||||
air_date: NaiveDate::default(),
|
||||
episodes: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
//! TMDB show model.
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::id::ShowId;
|
||||
|
||||
/// A deserialized Show from the TMDB API
|
||||
/// A deserialized Show from the TMDB API.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct Show {
|
||||
/// The show's TMDB ID
|
||||
/// The show's TMDB ID.
|
||||
pub id: ShowId,
|
||||
/// The show's title
|
||||
/// The show's title.
|
||||
#[serde(rename = "name")]
|
||||
pub title: String,
|
||||
/// The show's tagline
|
||||
/// The show's tagline.
|
||||
pub tagline: String,
|
||||
/// The show's overview
|
||||
/// The show's overview.
|
||||
pub overview: String,
|
||||
/// The show's first air date
|
||||
/// The show's first air date.
|
||||
pub first_air_date: NaiveDate,
|
||||
/// The show's last air date
|
||||
/// The show's last air date.
|
||||
pub last_air_date: NaiveDate,
|
||||
/// The total number of episodes in this show
|
||||
/// The total number of episodes in this show.
|
||||
pub number_of_episodes: u32,
|
||||
/// The number of seasons in this show
|
||||
/// The number of seasons in this show.
|
||||
pub number_of_seasons: u32,
|
||||
}
|
||||
|
||||
@@ -35,3 +38,24 @@ pub struct Show {
|
||||
// TODO: Company
|
||||
// pub companies: Vec<Company>
|
||||
// where: struct Company { id, name }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::{Show, ShowId};
|
||||
|
||||
#[test]
|
||||
fn use_types() {
|
||||
drop(Show {
|
||||
id: ShowId::from_raw(0),
|
||||
title: String::new(),
|
||||
tagline: String::new(),
|
||||
overview: String::new(),
|
||||
first_air_date: NaiveDate::default(),
|
||||
last_air_date: NaiveDate::default(),
|
||||
number_of_episodes: 0,
|
||||
number_of_seasons: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user