Initial commit

This commit is contained in:
2025-05-03 15:19:56 -06:00
commit 90c7b9d654
48 changed files with 3192 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "flix-tmdb"
version = "0.0.1"
categories = []
description = "Clients and models for fetching data from TMDB"
repository = "https://github.com/QuantumShade/flix"
authors.workspace = true
edition.workspace = true
license-file.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[dependencies]
chrono = { workspace = true, features = ["serde"] }
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
url = { workspace = true }
url-macro = { workspace = true }
+5
View File
@@ -0,0 +1,5 @@
# flix
[![Crates Version](https://img.shields.io/crates/v/flix.svg)](https://crates.io/crates/flix)
A library providing clients and models for fetching data from TMDB
+40
View File
@@ -0,0 +1,40 @@
use std::rc::Rc;
use crate::Config;
use crate::model::{Collection, CollectionId};
use super::{Error, make_request};
/// TMDB Collections API client
pub struct Client {
config: Rc<Config>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
}
}
impl Client {
/// Fetch the details of the collection refered to by ID
pub async fn get_details(
&self,
id: impl Into<CollectionId>,
language: Option<&str>,
) -> Result<Collection, Error> {
Ok(self
.config
.client
.execute(make_request(
&self.config,
&format!("/3/collection/{}", id.into()),
language,
)?)
.await?
.error_for_status()?
.json()
.await?)
}
}
+47
View File
@@ -0,0 +1,47 @@
use std::rc::Rc;
use crate::Config;
use crate::model::{Episode, ShowId};
use super::{Error, make_request};
/// TMDB Episodes API client
pub struct Client {
config: Rc<Config>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
}
}
impl Client {
/// Fetch the details of the episode refered to by ID, season number and episode number
pub async fn get_details(
&self,
id: impl Into<ShowId>,
season: impl Into<i32>,
episode: impl Into<i32>,
language: Option<&str>,
) -> Result<Episode, Error> {
Ok(self
.config
.client
.execute(make_request(
&self.config,
&format!(
"/3/tv/{}/season/{}/episode/{}",
id.into(),
season.into(),
episode.into()
),
language,
)?)
.await?
.error_for_status()?
.json()
.await?)
}
}
+58
View File
@@ -0,0 +1,58 @@
use std::rc::Rc;
use crate::Config;
use crate::model::{MovieGenre, ShowGenre};
use super::{Error, make_request};
/// TMDB Genre API client
pub struct Client {
config: Rc<Config>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
}
}
impl Client {
/// Fetch the list of all valid movie genres
pub async fn get_movie_genres(&self, language: Option<&str>) -> Result<Vec<MovieGenre>, Error> {
#[derive(Debug, serde::Deserialize)]
struct Genres {
genres: Vec<MovieGenre>,
}
let genres: Genres = self
.config
.client
.execute(make_request(&self.config, "/3/genre/movie/list", language)?)
.await?
.error_for_status()?
.json()
.await?;
Ok(genres.genres)
}
/// Fetch the list of all valid show genres
pub async fn get_tv_genres(&self, language: Option<&str>) -> Result<Vec<ShowGenre>, Error> {
#[derive(Debug, serde::Deserialize)]
struct Genres {
genres: Vec<ShowGenre>,
}
let genres: Genres = self
.config
.client
.execute(make_request(&self.config, "/3/genre/tv/list", language)?)
.await?
.error_for_status()?
.json()
.await?;
Ok(genres.genres)
}
}
+45
View File
@@ -0,0 +1,45 @@
use reqwest::Request;
use reqwest::header;
use crate::Config;
/// Collections API
pub mod collections;
/// Episodes API
pub mod episodes;
/// Genres API
pub mod genres;
/// Movies API
pub mod movies;
/// Seasons API
pub mod seasons;
/// Shows API
pub mod shows;
/// A generic error wrapping Url and Reqwest errors
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// Url error wrapper
#[error("url parse error: {0}")]
Url(#[from] url::ParseError),
/// Reqwest error wrapper
#[error("reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),
}
fn make_request(config: &Config, path: &str, language: Option<&str>) -> Result<Request, Error> {
let url = config.base_url.join(path)?;
let mut builder = config.client.get(url).header(
header::AUTHORIZATION,
format!("Bearer {}", config.bearer_token),
);
if let Some(ref user_agent) = config.user_agent {
builder = builder.header(header::USER_AGENT, user_agent);
}
if let Some(language) = language {
builder = builder.query(&[("language", language)]);
}
Ok(builder.build()?)
}
+40
View File
@@ -0,0 +1,40 @@
use std::rc::Rc;
use crate::Config;
use crate::model::{Movie, MovieId};
use super::{Error, make_request};
/// TMDB Movies API client
pub struct Client {
config: Rc<Config>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
}
}
impl Client {
/// Fetch the details of the movie refered to by ID
pub async fn get_details(
&self,
id: impl Into<MovieId>,
language: Option<&str>,
) -> Result<Movie, Error> {
Ok(self
.config
.client
.execute(make_request(
&self.config,
&format!("/3/movie/{}", id.into()),
language,
)?)
.await?
.error_for_status()?
.json()
.await?)
}
}
+41
View File
@@ -0,0 +1,41 @@
use std::rc::Rc;
use crate::Config;
use crate::model::{Season, ShowId};
use super::{Error, make_request};
/// TMDB Seasons API client
pub struct Client {
config: Rc<Config>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
}
}
impl Client {
/// Fetch the details of the season refered to by ID and season number
pub async fn get_details(
&self,
id: impl Into<ShowId>,
season: impl Into<i32>,
language: Option<&str>,
) -> Result<Season, Error> {
Ok(self
.config
.client
.execute(make_request(
&self.config,
&format!("/3/tv/{}/season/{}", id.into(), season.into()),
language,
)?)
.await?
.error_for_status()?
.json()
.await?)
}
}
+40
View File
@@ -0,0 +1,40 @@
use std::rc::Rc;
use crate::Config;
use crate::model::{Show, ShowId};
use super::{Error, make_request};
/// TMDB Shows API client
pub struct Client {
config: Rc<Config>,
}
impl Client {
/// Create a new client with the given configuration
pub fn new(config: Rc<Config>) -> Self {
Self { config }
}
}
impl Client {
/// Fetch the details of the show refered to by ID
pub async fn get_details(
&self,
id: impl Into<ShowId>,
language: Option<&str>,
) -> Result<Show, Error> {
Ok(self
.config
.client
.execute(make_request(
&self.config,
&format!("/3/tv/{}", id.into()),
language,
)?)
.await?
.error_for_status()?
.json()
.await?)
}
}
+65
View File
@@ -0,0 +1,65 @@
use std::rc::Rc;
use crate::{Config, api};
/// The primary client that references all other clients
pub struct Client {
genres: api::genres::Client,
collections: api::collections::Client,
movies: api::movies::Client,
shows: api::shows::Client,
seasons: api::seasons::Client,
episodes: api::episodes::Client,
}
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_with_config(config: Config) -> Self {
let config = Rc::new(config);
Self {
genres: api::genres::Client::new(config.clone()),
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()),
}
}
}
impl Client {
/// Access the Genres API
pub fn genres(&self) -> &api::genres::Client {
&self.genres
}
/// Access the Collections API
pub fn collections(&self) -> &api::collections::Client {
&self.collections
}
/// Access the Movies API
pub fn movies(&self) -> &api::movies::Client {
&self.movies
}
/// Access the Shows API
pub fn shows(&self) -> &api::shows::Client {
&self.shows
}
/// Access the Seasons API
pub fn seasons(&self) -> &api::seasons::Client {
&self.seasons
}
/// Access the Episodes API
pub fn episodes(&self) -> &api::episodes::Client {
&self.episodes
}
}
+26
View File
@@ -0,0 +1,26 @@
use url::Url;
use url_macro::url;
/// The client configuration
pub struct Config {
/// The base URL of the API
pub base_url: Url,
/// The reqwest client that is used for every request
pub client: reqwest::Client,
/// The bearer token for readonly access to the API
pub bearer_token: String,
/// 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
pub fn new(bearer_token: String) -> Self {
Self {
base_url: url!("https://api.themoviedb.org"),
client: reqwest::Client::new(),
bearer_token,
user_agent: None,
}
}
}
+12
View File
@@ -0,0 +1,12 @@
//! flix-tmdb provides clients and models for fetching data from TMDB
/// TMDB API clients
pub mod api;
/// Deserializable types from the TMDB API
pub mod model;
mod client;
pub use client::Client;
mod config;
pub use config::Config;
+22
View File
@@ -0,0 +1,22 @@
use super::{CollectionId, MovieId};
/// A deserialized Collection from the TMDB API
#[derive(Debug, serde::Deserialize)]
pub struct Collection {
/// The collection's TMDB ID
pub id: CollectionId,
/// The collection's name
pub name: String,
/// The collection's overview
pub overview: String,
/// 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
#[derive(Debug, serde::Deserialize)]
pub struct Item {
/// The movie's TMDB ID
pub id: MovieId,
}
+14
View File
@@ -0,0 +1,14 @@
use chrono::NaiveDate;
/// A deserialized Episode from the TMDB API
#[derive(Debug, serde::Deserialize)]
pub struct Episode {
/// The episode's number
pub episode_number: i32,
/// The episode's name
pub name: String,
/// The episode's overview
pub overview: String,
/// The episode's air date
pub air_date: NaiveDate,
}
+19
View File
@@ -0,0 +1,19 @@
use super::id::{MovieGenreId, ShowGenreId};
/// A deserialized movie Genre from the TMDB API
#[derive(Debug, serde::Deserialize)]
pub struct MovieGenre {
/// The genre's TMDB ID
pub id: MovieGenreId,
/// The genre's name
pub name: String,
}
/// A deserialized show Genre from the TMDB API
#[derive(Debug, serde::Deserialize)]
pub struct ShowGenre {
/// The genre's TMDB ID
pub id: ShowGenreId,
/// The genre's name
pub name: String,
}
+79
View File
@@ -0,0 +1,79 @@
use core::fmt;
use core::marker::PhantomData;
/// The TMDB ID type of a movie genre
pub type MovieGenreId = TmdbId<MovieGenre>;
/// The TMDB ID type of a show genre
pub type ShowGenreId = TmdbId<ShowGenre>;
/// The TMDB ID type of a collection
pub type CollectionId = TmdbId<Collection>;
/// The TMDB ID type of a movie
pub type MovieId = TmdbId<Movie>;
/// The TMDB ID type of a show
pub type ShowId = TmdbId<Show>;
pub enum MovieGenre {}
pub enum ShowGenre {}
pub enum Collection {}
pub enum Movie {}
pub enum Show {}
type Inner = i32;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
#[repr(transparent)]
pub struct TmdbId<T> {
inner: Inner,
#[serde(skip_serializing, default)]
_phantom: PhantomData<T>,
}
impl<T> TmdbId<T> {
pub fn inner(&self) -> Inner {
self.inner
}
}
impl<T> From<Inner> for TmdbId<T> {
fn from(value: Inner) -> Self {
Self {
inner: value,
_phantom: PhantomData,
}
}
}
impl<T> From<TmdbId<T>> for Inner {
fn from(value: TmdbId<T>) -> Self {
value.inner
}
}
impl<T> fmt::Debug for TmdbId<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
impl<T> fmt::Display for TmdbId<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
impl<T> Clone for TmdbId<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for TmdbId<T> {}
impl<T> PartialEq for TmdbId<T> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<T> Eq for TmdbId<T> {}
+17
View File
@@ -0,0 +1,17 @@
mod collection;
mod episode;
mod genre;
mod id;
mod movie;
mod season;
mod show;
pub use collection::*;
pub use episode::*;
pub use genre::*;
pub use movie::*;
pub use season::*;
pub use serde::*;
pub use show::*;
pub use id::{CollectionId, MovieGenreId, MovieId, ShowGenreId, ShowId};
+53
View File
@@ -0,0 +1,53 @@
use chrono::NaiveDate;
use super::{CollectionId, MovieGenre, MovieId};
/// A deserialized Movie from the TMDB API
#[derive(Debug, serde::Deserialize)]
pub struct Movie {
/// The movie's TMDB ID
pub id: MovieId,
/// The movie's collection, if it exists
#[serde(rename = "belongs_to_collection")]
pub collection: Option<InCollection>,
/// The movie's title
pub title: String,
/// The movie's overview
pub overview: String,
/// The list of genres the movie belongs to
pub genres: Vec<MovieGenre>,
/// The movie's release date
pub release_date: NaiveDate,
/// The movie's status
pub status: MovieStatus,
}
/// A deserialized movie's collection from the TMDB API
#[derive(Debug, serde::Deserialize)]
pub struct InCollection {
/// The collection's TMDB ID
pub id: CollectionId,
}
/// A deserialized movie status from the TMDB API
#[derive(Debug, serde::Deserialize)]
pub enum MovieStatus {
/// The movie was cancelled
#[serde(rename = "Canceled")]
Canceled,
/// The movie is in production
#[serde(rename = "In Production")]
InProduction,
/// The movie is planned
#[serde(rename = "Planned")]
Planned,
/// The movie is in post production
#[serde(rename = "Post Production")]
PostProduction,
/// The movie is released
#[serde(rename = "Released")]
Released,
/// The movie is rumored
#[serde(rename = "Rumored")]
Rumored,
}
+18
View File
@@ -0,0 +1,18 @@
use chrono::NaiveDate;
use super::Episode;
/// A deserialized Season from the TMDB API
#[derive(Debug, serde::Deserialize)]
pub struct Season {
/// The season's number
pub season_number: i32,
/// The season's name
pub name: String,
/// The season's overview
pub overview: String,
/// The season's air date
pub air_date: NaiveDate,
/// The list of episodes in this season
pub episodes: Vec<Episode>,
}
+47
View File
@@ -0,0 +1,47 @@
use chrono::NaiveDate;
use super::{ShowGenre, ShowId};
/// A deserialized Show from the TMDB API
#[derive(Debug, serde::Deserialize)]
pub struct Show {
/// The show's TMDB ID
pub id: ShowId,
/// The show's name
pub name: String,
/// The show's overview
pub overview: String,
/// The list of genres this show belongs to
pub genres: Vec<ShowGenre>,
/// The show's first air date
pub first_air_date: NaiveDate,
/// The show's last air date
pub last_air_date: NaiveDate,
/// The number of seasons in this show
pub number_of_seasons: i32,
/// The show's status
pub status: ShowStatus,
}
/// A deserialized show Status from the TMDB API
#[derive(Debug, serde::Deserialize)]
pub enum ShowStatus {
/// The show is returning
#[serde(rename = "Returning Series")]
Returning,
/// The show is planned
#[serde(rename = "Planned")]
Planned,
/// The show is in procuction
#[serde(rename = "In Production")]
InProduction,
/// The show has ended
#[serde(rename = "Ended")]
Ended,
/// The show is canceled
#[serde(rename = "Canceled")]
Canceled,
/// The show only released a pilot
#[serde(rename = "Pilot")]
Pilot,
}