diff --git a/Cargo.lock b/Cargo.lock index e7952f5783240c6d92d87230ee8bc57425dff86b..78cb32db0d6699c3a0de00dde38c10c8abf018fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -547,8 +547,8 @@ dependencies = [ "time", "traitobject", "typeable", - "unicase", - "url", + "unicase 1.4.2", + "url 1.7.2", ] [[package]] @@ -562,6 +562,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.5.2" @@ -702,6 +713,7 @@ dependencies = [ "itertools", "rocket", "rocket_contrib", + "rocket_cors", "serde", "serde_json", "uuid", @@ -1140,6 +1152,22 @@ dependencies = [ "yansi", ] +[[package]] +name = "rocket_cors" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea20696dc46308d0ca06222905fe38e02b8e46c087af9c82ea85cdc386271076" +dependencies = [ + "log 0.4.11", + "regex", + "rocket", + "serde", + "serde_derive", + "unicase 2.6.0", + "unicase_serde", + "url 2.1.1", +] + [[package]] name = "rocket_http" version = "0.4.5" @@ -1359,6 +1387,25 @@ dependencies = [ "version_check 0.1.5", ] +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check 0.9.2", +] + +[[package]] +name = "unicase_serde" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef53697679d874d69f3160af80bc28de12730a985d57bdf2b47456ccb8b11f1" +dependencies = [ + "serde", + "unicase 2.6.0", +] + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -1405,11 +1452,22 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" dependencies = [ - "idna", + "idna 0.1.5", "matches", "percent-encoding 1.0.1", ] +[[package]] +name = "url" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +dependencies = [ + "idna 0.2.0", + "matches", + "percent-encoding 2.1.0", +] + [[package]] name = "uuid" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index ecb9665f03acd53377e9d9c686c5f4c0575f9ea5..960707e62296839dab0471bf49b8611d1318b267 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ env_logger = "0.7.1" futures = "0.3.5" itertools = "0.9.0" rocket = "0.4.5" +rocket_cors = "0.5.2" serde_json = "1.0.57" [dependencies.chrono] @@ -24,7 +25,7 @@ features = ["postgres", "chrono", "uuidv07"] [dependencies.rocket_contrib] version = "0.4.5" default-features = false -features = ["json", "diesel_postgres_pool"] +features = ["json", "diesel_postgres_pool", "serve"] [dependencies.serde] version = "1.0.115" diff --git a/schema.graphql b/schema.graphql deleted file mode 100644 index 1b2084a861704cd9196245fee8cca8a247c1228a..0000000000000000000000000000000000000000 --- a/schema.graphql +++ /dev/null @@ -1,131 +0,0 @@ -schema { - query: Query -} - -type Query { - apiVersion: String! - - genres(id: ID = null, limit: Int = null, offset: Int = null): [Genre!]! - people(id: ID = null, limit: Int = null, offset: Int = null): [Person!]! - titles(id: ID = null, limit: Int = null, offset: Int = null): [Title!]! -} - -type Genre { - id: ID! - tmdbId: Int - name: String! - - titles(limit: Int = null, offset: Int = null): [Title!]! -} - -type Person { - id: ID! - imdbId: ID - name: String! - - starring(limit: Int = null, offset: Int = null): [Title!]! -} - -type Cast { - id: ID! - category: String - characters: [String!]! - credit: String - - title: Title - person: Person -} - -type Description { - id: ID! - region: String - languages: [String!]! - kind: LocalizedKind! - overview: String! - tagline: String -} - -type ShowEpisode { - id: ID! - seasonNumber: String - episodeNumber: String - airDate: Date - - show: Title - episode: Title -} - -type Image { - id: ID! - kind: ImageKind! - mime: String! - src: String! -} - -type Media { - id: ID! - mime: String - codecs: [String!]! - languages: [String!]! - src: String! -} - -type Name { - id: ID! - region: String - languages: [String!]! - kind: LocalizedKind! - name: String! -} - -type Rating { - id: ID! - region: String - certification: String! -} - -type Subtitle { - id: ID! - format: String! - language: String - region: String - specifier: String - src: String! -} - -type Title { - id: ID! - imdbId: String - tmdbId: Int - tvdbId: Int - originalLanguage: String - runtime: Int - yearStart: Int - yearEnd: Int - - cast(limit: Int = null, offset: Int = null): [Cast!]! - descriptions(region: String = null, language: String = null, limit: Int = null, offset: Int = null): [Description!]! - genres: [Genre!]! - image: [Image!]! - media: [Media!]! - names(region: String = null, language: String = null, limit: Int = null, offset: Int = null): [Name!]! - ratings(region: String = null, limit: Int = null, offset: Int = null): [Rating!]! - subtitles: [Subtitle!]! - episodes(seasonNumber: Int = null, episodeNumber: Int = null, limit: Int = null, offset: Int = null): [ShowEpisode!]! - show: ShowEpisode -} - -scalar Date - -enum ImageKind { - still - backdrop - poster - logo -} - -enum LocalizedKind { - primary - original - localized -} \ No newline at end of file diff --git a/src/dto.rs b/src/dto.rs index 7fb4932968ca587356f87ceb51f4ff52e1948925..71eb572c54025cad0ac7ff356b67671ab573fc5f 100644 --- a/src/dto.rs +++ b/src/dto.rs @@ -1,21 +1,23 @@ use serde::{Deserialize, Serialize}; -use crate::models::{Genre, Person, Title, TitleCast, TitleDescription, TitleName, TitleImage, TitleMedium, TitleRating, TitleSubtitle}; + +use crate::models::*; #[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(rename_all = "camelCase")] pub struct TitleDto { pub ids: TitleIdDto, pub original_language: Option<String>, pub runtime: Option<i32>, pub year_start: Option<i32>, pub year_end: Option<i32>, - pub titles: Vec<TitleNameDto>, // + pub titles: Vec<TitleNameDto>, pub descriptions: Vec<TitleDescriptionDto>, - pub cast: Vec<TitleCastDto>, - pub genres: Vec<GenreDto>, // - pub ratings: Vec<TitleRatingDto>, // - pub images: Vec<TitleImageDto>, - pub media: Vec<TitleMediaDto>, - pub subtitles: Vec<TitleSubtitleDto>, + pub cast: Vec<CastDto>, + pub genres: Vec<GenreDto>, + pub ratings: Vec<RatingDto>, + pub images: Vec<ImageDto>, + pub media: Vec<MediaDto>, + pub subtitles: Vec<SubtitleDto>, pub created_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>, } @@ -25,12 +27,12 @@ impl TitleDto { src: Title, titles: Vec<TitleNameDto>, descriptions: Vec<TitleDescriptionDto>, - cast: Vec<TitleCastDto>, + cast: Vec<CastDto>, genres: Vec<GenreDto>, - ratings: Vec<TitleRatingDto>, - images: Vec<TitleImageDto>, - media: Vec<TitleMediaDto>, - subtitles: Vec<TitleSubtitleDto> + ratings: Vec<RatingDto>, + images: Vec<ImageDto>, + media: Vec<MediaDto>, + subtitles: Vec<SubtitleDto>, ) -> Self { TitleDto { ids: TitleIdDto { @@ -58,6 +60,7 @@ impl TitleDto { } #[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(rename_all = "camelCase")] pub struct TitleIdDto { pub uuid: uuid::Uuid, pub imdb: Option<String>, @@ -66,6 +69,7 @@ pub struct TitleIdDto { } #[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(rename_all = "camelCase")] pub struct TitleNameDto { pub region: Option<String>, pub languages: Vec<String>, @@ -85,6 +89,7 @@ impl From<TitleName> for TitleNameDto { } #[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(rename_all = "camelCase")] pub struct TitleDescriptionDto { pub region: Option<String>, pub languages: Vec<String>, @@ -106,6 +111,7 @@ impl From<TitleDescription> for TitleDescriptionDto { } #[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(rename_all = "camelCase")] pub struct GenreDto { pub id: uuid::Uuid, pub tmdb_id: Option<i32>, @@ -123,22 +129,17 @@ impl From<Genre> for GenreDto { } #[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct GenreWithTitlesDto { - pub genre: GenreDto, - pub titles: Vec<TitleDto>, -} - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct TitleCastDto { +#[serde(rename_all = "camelCase")] +pub struct CastDto { pub category: Option<String>, pub characters: Vec<String>, pub credit: Option<String>, pub person: PersonDto, } -impl TitleCastDto { +impl CastDto { pub fn of(cast: TitleCast, person: Person) -> Self { - TitleCastDto { + CastDto { category: cast.category, characters: cast.characters, credit: cast.credit, @@ -148,6 +149,7 @@ impl TitleCastDto { } #[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(rename_all = "camelCase")] pub struct PersonDto { pub id: uuid::Uuid, pub imdb_id: Option<String>, @@ -165,67 +167,82 @@ impl From<Person> for PersonDto { } #[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct TitleEpisodeDto { - pub show_id: uuid::Uuid, - pub episode_id: uuid::Uuid, - pub season_number: Option<String>, - pub episode_number: Option<String>, +#[serde(rename_all = "camelCase")] +pub struct EpisodeDto { + pub season: Option<String>, + pub episode: Option<String>, pub air_date: Option<chrono::NaiveDate>, + pub title: TitleDto, +} + +impl EpisodeDto { + pub fn of(episode: TitleEpisode, title: TitleDto) -> Self { + EpisodeDto { + season: episode.season_number, + episode: episode.episode_number, + air_date: episode.air_date, + title + } + } } #[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct TitleImageDto { +#[serde(rename_all = "camelCase")] +pub struct ImageDto { pub kind: String, pub mime: String, pub src: String, } -impl From<TitleImage> for TitleImageDto { +impl From<TitleImage> for ImageDto { fn from(src: TitleImage) -> Self { - TitleImageDto { + ImageDto { kind: src.kind, mime: src.mime, - src: src.src + src: src.src, } } } #[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct TitleMediaDto { +#[serde(rename_all = "camelCase")] +pub struct MediaDto { pub mime: String, pub codecs: Vec<String>, pub languages: Vec<String>, pub src: String, } -impl From<TitleMedium> for TitleMediaDto { +impl From<TitleMedium> for MediaDto { fn from(src: TitleMedium) -> Self { - TitleMediaDto { + MediaDto { mime: src.mime, codecs: src.codecs, languages: src.languages, - src: src.src + src: src.src, } } } #[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct TitleRatingDto { +#[serde(rename_all = "camelCase")] +pub struct RatingDto { pub region: Option<String>, pub certification: String, } -impl From<TitleRating> for TitleRatingDto { +impl From<TitleRating> for RatingDto { fn from(src: TitleRating) -> Self { - TitleRatingDto { + RatingDto { region: src.region, - certification: src.certification + certification: src.certification, } } } #[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct TitleSubtitleDto { +#[serde(rename_all = "camelCase")] +pub struct SubtitleDto { pub format: String, pub language: Option<String>, pub region: Option<String>, @@ -233,14 +250,21 @@ pub struct TitleSubtitleDto { pub src: String, } -impl From<TitleSubtitle> for TitleSubtitleDto { +impl From<TitleSubtitle> for SubtitleDto { fn from(src: TitleSubtitle) -> Self { - TitleSubtitleDto { + SubtitleDto { format: src.format, language: src.language, region: src.region, specifier: src.specifier, - src: src.src + src: src.src, } } +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GenreWithTitlesDto { + pub genre: GenreDto, + pub titles: Vec<TitleDto>, } \ No newline at end of file diff --git a/src/dto_helpers.rs b/src/dto_helpers.rs index 915376cbe86986798edd77298ab929acbfea84f7..2eae822d3ec8b7b7a1f241ef93bcc1fb1f796d0d 100644 --- a/src/dto_helpers.rs +++ b/src/dto_helpers.rs @@ -89,22 +89,22 @@ fn process_title( }).collect::<Vec<_>>(), cast.into_iter().map(|src| { let (cast, person) = src; - TitleCastDto::of(cast, person) + CastDto::of(cast, person) }).collect::<Vec<_>>(), genres.into_iter().map(|(_, src)| { GenreDto::from(src) }).collect::<Vec<_>>(), ratings.into_iter().map(|src| { - TitleRatingDto::from(src) + RatingDto::from(src) }).collect::<Vec<_>>(), images.into_iter().map(|src| { - TitleImageDto::from(src) + ImageDto::from(src) }).collect::<Vec<_>>(), media.into_iter().map(|src| { - TitleMediaDto::from(src) + MediaDto::from(src) }).collect::<Vec<_>>(), subtitles.into_iter().map(|src| { - TitleSubtitleDto::from(src) + SubtitleDto::from(src) }).collect::<Vec<_>>(), ) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d8b16169700c9f00cd3d6894e57cd19d53ed1926..adfe460f7b0e9eec3c3dbe49914c4ac2a5d500c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,10 +6,14 @@ extern crate rocket; #[macro_use] extern crate rocket_contrib; +use std::env; + use diesel::prelude::*; use dotenv::dotenv; use rocket_contrib::databases::diesel; use rocket_contrib::json::Json; +use rocket_contrib::serve::StaticFiles; +use rocket_cors::AllowedHeaders; use media_backend::dto::*; use media_backend::dto_helpers::{load_title, load_titles}; @@ -20,17 +24,17 @@ use media_backend::param_helpers::ParamUuid; struct MediaflixConnection(diesel::PgConnection); #[get("/api/v1/genres")] -fn list_genres(db: MediaflixConnection) -> QueryResult<Json<Vec<Genre>>> { +fn list_genres(db: MediaflixConnection) -> QueryResult<Json<Vec<GenreDto>>> { use media_backend::schema::*; - let query = genres::table.into_boxed(); - Ok(Json(query.load::<Genre>(&db.0)?)) + let data = genres::table.load::<Genre>(&db.0)?; + Ok(Json(data.into_iter().map(GenreDto::from).collect::<Vec<_>>())) } -#[get("/api/v1/genres/<id>")] -fn get_genre(db: MediaflixConnection, id: ParamUuid) -> QueryResult<Json<GenreWithTitlesDto>> { +#[get("/api/v1/genres/<genre_id>")] +fn get_genre(db: MediaflixConnection, genre_id: ParamUuid) -> QueryResult<Json<GenreWithTitlesDto>> { use media_backend::schema::*; let genre: Genre = genres::table - .find(id.uuid()) + .find(genre_id.uuid()) .first::<Genre>(&db.0)?; let titles: Vec<Title> = title_genres::table .filter(title_genres::genre_id.eq(genre.id)) @@ -54,23 +58,42 @@ fn list_titles(db: MediaflixConnection) -> QueryResult<Json<Vec<TitleDto>>> { Ok(Json(load_titles(&db.0, titles)?)) } -#[get("/api/v1/titles/<title>")] -fn get_title(db: MediaflixConnection, title: ParamUuid) -> QueryResult<Json<TitleDto>> { +#[get("/api/v1/titles/<title_id>")] +fn get_title(db: MediaflixConnection, title_id: ParamUuid) -> QueryResult<Json<TitleDto>> { use media_backend::schema::*; let title = load_title( &db.0, titles::table - .find(title.uuid()) + .find(title_id.uuid()) .first::<Title>(&db.0)?, )?; Ok(Json(title)) } -fn main() { +#[get("/api/v1/titles/<title_id>/episodes")] +fn list_episodes(db: MediaflixConnection, title_id: ParamUuid) -> QueryResult<Json<Vec<EpisodeDto>>> { + use media_backend::schema::*; + let titles: Vec<(TitleEpisode, Title)> = title_episodes::table + .filter(title_episodes::show_id.eq(title_id.uuid())) + .inner_join(titles::table.on(titles::id.eq(title_episodes::episode_id))) + .load::<(TitleEpisode, Title)>(&db.0)?; + let (episodes, titles): (Vec<TitleEpisode>, Vec<Title>) = titles.into_iter().unzip(); + let titles = load_titles(&db.0, titles)?; + Ok(Json(episodes.into_iter().zip(titles) + .map(|(episode, title)| EpisodeDto::of(episode, title)) + .collect::<Vec<_>>())) +} + +fn main() -> Result<(), Box<dyn std::error::Error>> { dotenv().ok(); rocket::ignite() .attach(MediaflixConnection::fairing()) + .attach(rocket_cors::CorsOptions { + allowed_headers: AllowedHeaders::some(&["Authorization", "Accept", "Accept-Language"]), + allow_credentials: true, + ..Default::default() + }.to_cors()?) .mount( "/", rocket::routes![ @@ -80,5 +103,8 @@ fn main() { get_title ], ) + .mount("/static", StaticFiles::from(env::var("MEDIAFLIX_PATH")?)) .launch(); + + Ok(()) } diff --git a/src/models.rs b/src/models.rs index 865b0e1b8b2e27e3346724212a24def8c6307354..3573ad982022932b9e57aa9cbfd0dceb451566c0 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,11 +1,10 @@ extern crate diesel; use diesel::*; -use serde::{Deserialize, Serialize}; use super::schema::*; -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[table_name = "genres"] pub struct Genre { pub id: uuid::Uuid, @@ -15,7 +14,7 @@ pub struct Genre { pub updated_at: chrono::DateTime<chrono::Utc>, } -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[table_name = "people"] pub struct Person { pub id: uuid::Uuid, @@ -25,7 +24,7 @@ pub struct Person { pub updated_at: chrono::DateTime<chrono::Utc>, } -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Title, foreign_key = "title_id")] #[belongs_to(Person, foreign_key = "person_id")] #[table_name = "title_casts"] @@ -40,7 +39,7 @@ pub struct TitleCast { pub person_id: uuid::Uuid, } -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Title, foreign_key = "title_id")] #[table_name = "title_descriptions"] pub struct TitleDescription { @@ -55,7 +54,7 @@ pub struct TitleDescription { pub title_id: uuid::Uuid, } -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Title, foreign_key = "show_id")] #[table_name = "title_episodes"] pub struct TitleEpisode { @@ -69,7 +68,7 @@ pub struct TitleEpisode { pub updated_at: chrono::DateTime<chrono::Utc>, } -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Title, foreign_key = "title_id")] #[belongs_to(Genre, foreign_key = "genre_id")] #[table_name = "title_genres"] @@ -81,7 +80,7 @@ pub struct TitleGenre { pub updated_at: chrono::DateTime<chrono::Utc>, } -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Title, foreign_key = "title_id")] #[table_name = "title_images"] pub struct TitleImage { @@ -94,7 +93,7 @@ pub struct TitleImage { pub title_id: uuid::Uuid, } -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Title, foreign_key = "title_id")] #[table_name = "title_media"] pub struct TitleMedium { @@ -108,7 +107,7 @@ pub struct TitleMedium { pub title_id: uuid::Uuid, } -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Title, foreign_key = "title_id")] #[table_name = "title_names"] pub struct TitleName { @@ -122,7 +121,7 @@ pub struct TitleName { pub title_id: uuid::Uuid, } -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Title, foreign_key = "title_id")] #[table_name = "title_ratings"] pub struct TitleRating { @@ -134,7 +133,7 @@ pub struct TitleRating { pub title_id: uuid::Uuid, } -#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Title, foreign_key = "title_id")] #[table_name = "title_subtitles"] pub struct TitleSubtitle { @@ -149,7 +148,7 @@ pub struct TitleSubtitle { pub title_id: uuid::Uuid, } -#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Identifiable, Queryable, PartialEq, Debug)] #[table_name = "titles"] pub struct Title { pub id: uuid::Uuid,