diff --git a/Cargo.lock b/Cargo.lock index 6816106..ebc1eac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,9 +192,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "shlex", @@ -480,7 +480,7 @@ checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "flix" -version = "0.0.12" +version = "0.0.13" dependencies = [ "flix-db", "flix-fs", @@ -490,7 +490,7 @@ dependencies = [ [[package]] name = "flix-cli" -version = "0.0.12" +version = "0.0.13" dependencies = [ "anyhow", "chrono", @@ -501,11 +501,13 @@ dependencies = [ "serde", "tokio", "toml", + "tracing", + "tracing-subscriber", ] [[package]] name = "flix-db" -version = "0.0.12" +version = "0.0.13" dependencies = [ "chrono", "flix-model", @@ -518,7 +520,7 @@ dependencies = [ [[package]] name = "flix-fs" -version = "0.0.12" +version = "0.0.13" dependencies = [ "async-stream", "flix-model", @@ -530,7 +532,7 @@ dependencies = [ [[package]] name = "flix-model" -version = "0.0.12" +version = "0.0.13" dependencies = [ "seamantic", "serde", @@ -539,7 +541,7 @@ dependencies = [ [[package]] name = "flix-tmdb" -version = "0.0.12" +version = "0.0.13" dependencies = [ "chrono", "flix-model", @@ -888,7 +890,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] @@ -1077,6 +1079,15 @@ dependencies = [ "syn", ] +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1238,11 +1249,10 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -1681,7 +1691,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] @@ -1792,15 +1802,16 @@ dependencies = [ [[package]] name = "sea-orm" -version = "2.0.0-rc.16" +version = "2.0.0-rc.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6265b8190c0898db442eeb8333f277de801742f0072f984601a10bb3145237d3" +checksum = "296a092e16fbbcc3a9969eae3915ef11024b98a43f3b77ff5199bc626c462d7a" dependencies = [ "async-stream", "async-trait", "chrono", "derive_more", "futures-util", + "inventory", "itertools", "log", "ouroboros", @@ -1818,9 +1829,9 @@ dependencies = [ [[package]] name = "sea-orm-cli" -version = "2.0.0-rc.16" +version = "2.0.0-rc.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d9971a23ea89ab63b48d19923871415358f07a1db72b4dd9a40b6cae625c32" +checksum = "301f7ace977d940474b47a154a5fc453714d13e5d53bdb077330c6fc6212a31b" dependencies = [ "chrono", "glob", @@ -1836,9 +1847,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "2.0.0-rc.16" +version = "2.0.0-rc.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54373b4c855be7a5b0691e1fcbdd1d5bd25a9a54efdcf922f281356a7129808" +checksum = "e56ee8be52d15a801dc62a5a30d83dc718762401f7c7945b7f2730ef385b8b3d" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1850,9 +1861,9 @@ dependencies = [ [[package]] name = "sea-orm-migration" -version = "2.0.0-rc.16" +version = "2.0.0-rc.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82dbb9a68e0d64b5f5493f2924958e2149f1d4827bb381100d75886d1b1df281" +checksum = "b7444940e5b7db32f55dea6d04cfb312480e45ae07753b1391311eccd5753475" dependencies = [ "async-trait", "sea-orm", @@ -1926,9 +1937,9 @@ dependencies = [ [[package]] name = "seamantic" -version = "0.0.9" +version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9fffb0e1d1ffb6d1466333ccec4d13b388c0a59e34e41f10008db730e5a5de" +checksum = "6fdfda9081467f15961b506b46c1569ac166f69b1ac17835c59a3104c3b2a79e" dependencies = [ "sea-orm", "sea-orm-migration", @@ -2617,24 +2628,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-xid" @@ -2807,14 +2818,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index b4e5856..5897b26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,17 +35,17 @@ overflow-checks = true strip = "debuginfo" [workspace.dependencies] -flix = { path = "crates/flix", version = "=0.0.12", default-features = false } -flix-cli = { path = "crates/cli", version = "=0.0.12", default-features = false } -flix-db = { path = "crates/db", version = "=0.0.12", default-features = false } -flix-fs = { path = "crates/fs", version = "=0.0.12", default-features = false } -flix-model = { path = "crates/model", version = "=0.0.12", default-features = false } -flix-tmdb = { path = "crates/tmdb", version = "=0.0.12", default-features = false } +flix = { path = "crates/flix", version = "=0.0.13", default-features = false } +flix-cli = { path = "crates/cli", version = "=0.0.13", default-features = false } +flix-db = { path = "crates/db", version = "=0.0.13", default-features = false } +flix-fs = { path = "crates/fs", version = "=0.0.13", default-features = false } +flix-model = { path = "crates/model", version = "=0.0.13", default-features = false } +flix-tmdb = { path = "crates/tmdb", version = "=0.0.13", default-features = false } -seamantic = { version = "0.0.9", default-features = false } +seamantic = { version = "0.0.10", default-features = false } -sea-orm = { version = "2.0.0-rc.16", default-features = false } -sea-orm-migration = { version = "2.0.0-rc.16", default-features = false } +sea-orm = { version = "2.0.0-rc.17", default-features = false } +sea-orm-migration = { version = "2.0.0-rc.17", default-features = false } anyhow = { version = "^1", default-features = false } async-stream = { version = "^0.3", default-features = false } @@ -62,5 +62,7 @@ thiserror = { version = "^2", default-features = false } tokio = { version = "^1", default-features = false } tokio-stream = { version = "^0.1", default-features = false } toml = { version = "^0.9", default-features = false } +tracing = { version = "^0.1", default-features = false } +tracing-subscriber = { version = "^0.3", default-features = false } url = { version = "^2", default-features = false } url-macro = { version = "^0.2", default-features = false } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 69c5626..2fd496c 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flix-cli" -version = "0.0.12" +version = "0.0.13" categories = ["command-line-utilities"] description = "CLI for interacting with a flix database" @@ -51,7 +51,9 @@ clap = { workspace = true, features = [ "usage", ] } futures = { workspace = true } -sea-orm = { workspace = true, features = ["runtime-tokio"] } +sea-orm = { workspace = true, features = ["runtime-tokio", "debug-print"] } serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["rt", "fs", "macros"] } toml = { workspace = true, features = ["parse", "serde"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index bb3ad5f..d572d85 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -16,6 +16,10 @@ pub struct Cli { #[arg(short, long, value_name = "DATABASE", default_value = "./flix.db")] database: PathBuf, + /// Enable tracing + #[arg(short, long)] + pub trace: bool, + #[command(subcommand)] command: Command, } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a355f6f..303587a 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -28,6 +28,13 @@ async fn main() -> Result<()> { let client = Client::new(config.tmdb().bearer_token().to_owned()); + if cli.trace { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_test_writer() + .init(); + } + match cli.command() { Command::Init => exec_init(database_path).await?, Command::Add { command } => exec_add(client, database_path, command).await?, diff --git a/crates/cli/src/run/tmdb.rs b/crates/cli/src/run/tmdb.rs index eba2ebd..9c085d6 100644 --- a/crates/cli/src/run/tmdb.rs +++ b/crates/cli/src/run/tmdb.rs @@ -101,7 +101,7 @@ pub async fn add(client: Client, db: &DatabaseConnection, command: Command) -> R flix_id: Set(flix.id), last_update: Set(Utc::now().date_naive()), runtime: Set(movie.runtime.into()), - collection: Set(movie.collection.map(|c| c.id)), + collection_id: Set(movie.collection.map(|c| c.id)), } .insert(txn) .await?; @@ -206,8 +206,8 @@ pub async fn add(client: Client, db: &DatabaseConnection, command: Command) -> R for season in seasons { entity::info::seasons::ActiveModel { - show: Set(flix.id), - season: Set(season.season_number), + show_id: Set(flix.id), + season_number: Set(season.season_number), title: Set(season.title), overview: Set(season.overview), date: Set(season.air_date), @@ -229,9 +229,9 @@ pub async fn add(client: Client, db: &DatabaseConnection, command: Command) -> R for (season, episodes) in episodes { for episode in episodes { entity::info::episodes::ActiveModel { - show: Set(flix.id), - season: Set(season), - episode: Set(episode.episode_number), + show_id: Set(flix.id), + season_number: Set(season), + episode_number: Set(episode.episode_number), title: Set(episode.title), overview: Set(episode.overview), date: Set(episode.air_date), @@ -324,8 +324,8 @@ pub async fn add(client: Client, db: &DatabaseConnection, command: Command) -> R .transaction(|txn| { Box::pin(async move { entity::info::seasons::ActiveModel { - show: Set(show.flix_id), - season: Set(season_number), + show_id: Set(show.flix_id), + season_number: Set(season_number), title: Set(season.title), overview: Set(season.overview), date: Set(season.air_date), @@ -345,9 +345,9 @@ pub async fn add(client: Client, db: &DatabaseConnection, command: Command) -> R for episode in episodes { entity::info::episodes::ActiveModel { - show: Set(show.flix_id), - season: Set(season_number), - episode: Set(episode.episode_number), + show_id: Set(show.flix_id), + season_number: Set(season_number), + episode_number: Set(episode.episode_number), title: Set(episode.title), overview: Set(episode.overview), date: Set(episode.air_date), @@ -436,9 +436,9 @@ pub async fn add(client: Client, db: &DatabaseConnection, command: Command) -> R .transaction(|txn| { Box::pin(async move { entity::info::episodes::ActiveModel { - show: Set(flix_id), - season: Set(season), - episode: Set(episode_number), + show_id: Set(flix_id), + season_number: Set(season), + episode_number: Set(episode_number), title: Set(episode.title), overview: Set(episode.overview), date: Set(episode.air_date), diff --git a/crates/db/Cargo.toml b/crates/db/Cargo.toml index b50ffdb..6ef87df 100644 --- a/crates/db/Cargo.toml +++ b/crates/db/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flix-db" -version = "0.0.12" +version = "0.0.13" categories = [] description = "Types for storing persistent data about media" @@ -30,7 +30,11 @@ flix-tmdb = { workspace = true, optional = true, features = ["sea-orm"] } seamantic = { workspace = true, features = ["sqlite"] } chrono = { workspace = true } -sea-orm = { workspace = true, features = ["with-chrono"] } +sea-orm = { workspace = true, features = [ + "entity-registry", + "schema-sync", + "with-chrono", +] } sea-orm-migration = { workspace = true } [dev-dependencies] diff --git a/crates/db/src/connection.rs b/crates/db/src/connection.rs index 25d0391..6ed8c97 100644 --- a/crates/db/src/connection.rs +++ b/crates/db/src/connection.rs @@ -9,9 +9,12 @@ pub struct Connection(DatabaseConnection); impl Connection { /// Helper function for applying database migrations while wrapping a /// [DatabaseConnection] in a newtype - pub async fn try_from(database: DatabaseConnection) -> Result { - crate::migration::Migrator::up(&database, None).await?; - Ok(Self(database)) + pub async fn try_from(db: DatabaseConnection) -> Result { + crate::migration::Migrator::down(&db, None).await?; + db.get_schema_registry("flix_db::*").sync(&db).await?; + db.get_schema_registry("flix_db::*").sync(&db).await?; + crate::migration::Migrator::up(&db, None).await?; + Ok(Self(db)) } } @@ -20,3 +23,10 @@ impl AsRef for Connection { &self.0 } } + +#[cfg(test)] +impl Connection { + pub(crate) fn take(self) -> DatabaseConnection { + self.0 + } +} diff --git a/crates/db/src/entity/content.rs b/crates/db/src/entity/content.rs new file mode 100644 index 0000000..a2aa43b --- /dev/null +++ b/crates/db/src/entity/content.rs @@ -0,0 +1,794 @@ +//! This module contains entities for storing media file information + +/// Library entity +pub mod libraries { + use flix_model::id::LibraryId; + + use seamantic::model::path::PathBytes; + + use sea_orm::entity::prelude::*; + + /// The database representation of a library media folder + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_libraries")] + pub struct Model { + /// The library's ID + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + pub id: LibraryId, + /// The library's directory + pub directory: PathBytes, + + /// Collections that are part of this library + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub collections: HasMany, + /// Movies that are part of this library + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub movies: HasMany, + /// Shows that are part of this library + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub shows: HasMany, + /// Seasons that are part of this library + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub seasons: HasMany, + /// Episodes that are part of this library + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub episodes: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Collection entity +pub mod collections { + use flix_model::id::{CollectionId, LibraryId}; + + use seamantic::model::path::PathBytes; + + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a collection media folder + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_collections")] + pub struct Model { + /// The collection's ID + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + pub id: CollectionId, + /// The collection's parent + #[sea_orm(indexed)] + pub parent_id: Option, + /// The collection's slug + #[sea_orm(unique)] + pub slug: String, + /// The collection's library ID + pub library_id: LibraryId, + /// The collection's directory + pub directory: PathBytes, + /// The collection's poster path + pub relative_poster_path: Option, + + /// This collection's parent + #[sea_orm(self_ref, relation_enum = "Parent", from = "parent_id", to = "id")] + pub parent: HasOne, + /// The library this collection belongs to + #[sea_orm(belongs_to, from = "library_id", to = "id")] + pub library: HasOne, + /// The info for this collection + #[sea_orm(belongs_to, relation_enum = "Info", from = "id", to = "id")] + pub info: HasOne, + /// The watched info for this collection + #[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")] + pub watched: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Movie entity +pub mod movies { + use flix_model::id::{CollectionId, LibraryId, MovieId}; + + use seamantic::model::path::PathBytes; + + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a movie media folder + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_movies")] + pub struct Model { + /// The movie's ID + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + pub id: MovieId, + /// The movie's parent + #[sea_orm(indexed)] + pub parent_id: Option, + /// The movie's slug + #[sea_orm(unique)] + pub slug: String, + /// The movie's library + pub library_id: LibraryId, + /// The movie's directory + pub directory: PathBytes, + /// The movie's media path + pub relative_media_path: String, + /// The movie's poster path + pub relative_poster_path: Option, + + /// This movie's parent + #[sea_orm(belongs_to, from = "parent_id", to = "id")] + pub parent: HasOne, + /// The library this movie belongs to + #[sea_orm(belongs_to, from = "library_id", to = "id")] + pub library: HasOne, + /// The info for this movie + #[sea_orm(belongs_to, relation_enum = "Info", from = "id", to = "id")] + pub info: HasOne, + /// The watched info for this movie + #[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")] + pub watched: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Show entity +pub mod shows { + use flix_model::id::{CollectionId, LibraryId, ShowId}; + + use seamantic::model::path::PathBytes; + + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a show media folder + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_shows")] + pub struct Model { + /// The show's ID + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + pub id: ShowId, + /// The show's parent + #[sea_orm(indexed)] + pub parent_id: Option, + /// The show's slug + #[sea_orm(unique)] + pub slug: String, + /// The show's library + pub library_id: LibraryId, + /// The show's directory + pub directory: PathBytes, + /// The show's poster path + pub relative_poster_path: Option, + + /// This show's parent + #[sea_orm(belongs_to, from = "parent_id", to = "id")] + pub parent: HasOne, + /// The library this show belongs to + #[sea_orm(belongs_to, from = "library_id", to = "id")] + pub library: HasOne, + /// The info for this show + #[sea_orm(belongs_to, relation_enum = "Info", from = "id", to = "id")] + pub info: HasOne, + /// The watched info for this show + #[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")] + pub watched: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Season entity +pub mod seasons { + use flix_model::id::{LibraryId, ShowId}; + use flix_model::numbers::SeasonNumber; + + use seamantic::model::path::PathBytes; + + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a season media folder + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_seasons")] + pub struct Model { + /// The season's show's ID + #[sea_orm(primary_key, auto_increment = false)] + pub show_id: ShowId, + /// The season's number + #[sea_orm(primary_key, auto_increment = false)] + pub season_number: SeasonNumber, + /// The season's slug + #[sea_orm(unique)] + pub slug: String, + /// The season's library + pub library_id: LibraryId, + /// The season's directory + pub directory: PathBytes, + /// The season's poster path + pub relative_poster_path: Option, + + /// The library this season belongs to + #[sea_orm(belongs_to, from = "library_id", to = "id")] + pub library: HasOne, + /// The info for this season + #[sea_orm( + belongs_to, + relation_enum = "Info", + from = "(show_id, season_number)", + to = "(show_id, season_number)" + )] + pub info: HasOne, + /// The watched info for this season + #[sea_orm( + has_many, + relation_enum = "Watched", + from = "(show_id, season_number)", + to = "(show_id, season_number)" + )] + pub watched: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Episode entity +pub mod episodes { + use flix_model::id::{LibraryId, ShowId}; + use flix_model::numbers::{EpisodeNumber, SeasonNumber}; + + use seamantic::model::path::PathBytes; + + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a episode media folder + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_episodes")] + pub struct Model { + /// The episode's show's ID + #[sea_orm(primary_key, auto_increment = false)] + pub show_id: ShowId, + /// The episode's season's number + #[sea_orm(primary_key, auto_increment = false)] + pub season_number: SeasonNumber, + /// The episode's number + #[sea_orm(primary_key, auto_increment = false)] + pub episode_number: EpisodeNumber, + /// The number of additional contained episodes + pub count: u8, + /// The episode's slug + #[sea_orm(unique)] + pub slug: String, + /// The episode's library + pub library_id: LibraryId, + /// The episode's directory + pub directory: PathBytes, + /// The episode's media path + pub relative_media_path: String, + /// The episode's poster path + pub relative_poster_path: Option, + + /// The library this episode belongs to + #[sea_orm(belongs_to, from = "library_id", to = "id")] + pub library: HasOne, + /// The info for this episode + #[sea_orm( + belongs_to, + relation_enum = "Info", + from = "(show_id, season_number, episode_number)", + to = "(show_id, season_number, episode_number)" + )] + pub info: HasOne, + /// The watched info for this episode + #[sea_orm( + has_many, + relation_enum = "Watched", + from = "(show_id, season_number, episode_number)", + to = "(show_id, season_number, episode_number)" + )] + pub watched: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Macros for creating content entities +#[cfg(test)] +pub mod test { + macro_rules! make_content_library { + ($db:expr, $id:literal) => { + $crate::entity::content::libraries::ActiveModel { + id: Set(::flix_model::id::LibraryId::from_raw($id)), + directory: Set(::std::path::PathBuf::new().into()), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_content_library; + + macro_rules! make_content_collection { + ($db:expr, $lid:literal, $id:literal, $pid:expr) => { + $crate::entity::info::test::make_info_collection!($db, $id); + $crate::entity::content::collections::ActiveModel { + id: Set(::flix_model::id::CollectionId::from_raw($id)), + parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)), + slug: Set(concat!("C ", $id).to_string()), + library_id: Set(::flix_model::id::LibraryId::from_raw($lid)), + directory: Set(::std::path::PathBuf::new().into()), + relative_poster_path: Set(::core::option::Option::None), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_content_collection; + + macro_rules! make_content_movie { + ($db:expr, $lid:literal, $id:literal, $pid:expr) => { + $crate::entity::info::test::make_info_movie!($db, $id); + $crate::entity::content::movies::ActiveModel { + id: Set(::flix_model::id::MovieId::from_raw($id)), + parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)), + slug: Set(concat!("< ", $id).to_string()), + library_id: Set(::flix_model::id::LibraryId::from_raw($lid)), + directory: Set(::std::path::PathBuf::new().into()), + relative_media_path: Set(::std::string::String::new()), + relative_poster_path: Set(::core::option::Option::None), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_content_movie; + + macro_rules! make_content_show { + ($db:expr, $lid:literal, $id:literal, $pid:expr) => { + $crate::entity::info::test::make_info_show!($db, $id); + $crate::entity::content::shows::ActiveModel { + id: Set(::flix_model::id::ShowId::from_raw($id)), + parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)), + slug: Set(concat!("S ", $id).to_string()), + library_id: Set(::flix_model::id::LibraryId::from_raw($lid)), + directory: Set(::std::path::PathBuf::new().into()), + relative_poster_path: Set(::core::option::Option::None), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_content_show; + + macro_rules! make_content_season { + ($db:expr, $lid:literal, $show:literal, $season:literal) => { + $crate::entity::info::test::make_info_season!($db, $show, $season); + $crate::entity::content::seasons::ActiveModel { + show_id: Set(::flix_model::id::ShowId::from_raw($show)), + season_number: Set($season), + slug: Set(concat!("SS ", $show, $season).to_string()), + library_id: Set(::flix_model::id::LibraryId::from_raw($lid)), + directory: Set(::std::path::PathBuf::new().into()), + relative_poster_path: Set(::core::option::Option::None), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_content_season; + + macro_rules! make_content_episode { + ($db:expr, $lid:literal, $show:literal, $season:literal, $episode:literal) => { + make_content_episode!(@make, $db, $lid, $show, $season, $episode, 0); + }; + ($db:expr, $lid:literal, $show:literal, $season:literal, $episode:literal, >1) => { + make_content_episode!(@make, $db, $lid, $show, $season, $episode, 1); + }; + (@make, $db:expr, $lid:literal, $show:literal, $season:literal, $episode:literal, $count:literal) => { + $crate::entity::info::test::make_info_episode!($db, $show, $season, $episode); + $crate::entity::content::episodes::ActiveModel { + show_id: Set(::flix_model::id::ShowId::from_raw($show)), + season_number: Set($season), + episode_number: Set($episode), + count: Set($count), + slug: Set(concat!("SSE ", $show, $season, $episode).to_string()), + library_id: Set(::flix_model::id::LibraryId::from_raw($lid)), + directory: Set(::std::path::PathBuf::new().into()), + relative_media_path: Set(::std::string::String::new()), + relative_poster_path: Set(::core::option::Option::None), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_content_episode; +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use flix_model::id::{CollectionId, LibraryId, MovieId, ShowId}; + + use sea_orm::ActiveValue::{NotSet, Set}; + use sea_orm::entity::prelude::*; + use sea_orm::sqlx::error::ErrorKind; + + use crate::entity::content::test::{ + make_content_collection, make_content_episode, make_content_library, make_content_movie, + make_content_season, make_content_show, + }; + use crate::entity::info::test::{ + make_info_collection, make_info_episode, make_info_movie, make_info_season, make_info_show, + }; + use crate::tests::new_initialized_memory_db; + + use super::super::tests::get_error_kind; + use super::super::tests::{noneable, notsettable}; + + #[tokio::test] + async fn use_test_macros() { + let db = new_initialized_memory_db().await; + + make_content_library!(&db, 1); + make_content_collection!(&db, 1, 1, None); + make_content_movie!(&db, 1, 1, None); + make_content_show!(&db, 1, 1, None); + make_content_season!(&db, 1, 1, 1); + make_content_episode!(&db, 1, 1, 1, 1); + } + + #[tokio::test] + async fn test_round_trip_libraries() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_library { + ($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_library!(@insert, $db, $id $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.id, LibraryId::from_raw($id)); + assert_eq!(model.directory, Path::new(concat!("L Directory ", $id)).to_owned().into()); + }; + ($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_library!(@insert, $db, $id $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => { + super::libraries::ActiveModel { + id: notsettable!(id, LibraryId::from_raw($id) $(, $($skip),+)?), + directory: notsettable!(directory, Path::new(concat!("L Directory ", $id)).to_owned().into() $(, $($skip),+)?), + }.insert($db).await + }; + } + + assert_library!(&db, 1, Success); + assert_library!(&db, 1, UniqueViolation); + assert_library!(&db, 2, Success); + assert_library!(&db, 3, Success; id); + assert_library!(&db, 4, NotNullViolation; directory); + } + + #[tokio::test] + async fn test_round_trip_collections() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_collection { + ($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_collection!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.id, CollectionId::from_raw($id)); + assert_eq!(model.parent_id, $pid.map(CollectionId::from_raw)); + assert_eq!(model.slug, concat!("C Slug ", $id).to_string()); + assert_eq!(model.library_id, LibraryId::from_raw($lid)); + assert_eq!(model.directory, Path::new(concat!("C Directory ", $id)).to_owned().into()); + assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("C Poster ", $id).to_owned() $(, $($skip),+)?)); + }; + ($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_collection!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => { + super::collections::ActiveModel { + id: notsettable!(id, CollectionId::from_raw($id) $(, $($skip),+)?), + parent_id: notsettable!(parent_id, $pid.map(CollectionId::from_raw) $(, $($skip),+)?), + slug: notsettable!(slug, concat!("C Slug ", $id).to_string() $(, $($skip),+)?), + library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?), + directory: notsettable!(directory, Path::new(concat!("C Directory ", $id)).to_owned().into() $(, $($skip),+)?), + relative_poster_path: notsettable!(relative_poster_path, Some(concat!("C Poster ", $id).to_owned()) $(, $($skip),+)?), + }.insert($db).await + }; + } + + make_content_library!(&db, 1); + assert_collection!(&db, 1, None, 1, ForeignKeyViolation); + make_info_collection!(&db, 1); + assert_collection!(&db, 1, None, 1, Success); + make_info_collection!(&db, 2); + assert_collection!(&db, 2, None, 2, ForeignKeyViolation); + make_content_library!(&db, 2); + assert_collection!(&db, 2, None, 2, Success); + + assert_collection!(&db, 1, None, 1, UniqueViolation); + make_info_collection!(&db, 3); + make_info_collection!(&db, 4); + make_info_collection!(&db, 5); + make_info_collection!(&db, 6); + make_info_collection!(&db, 7); + make_info_collection!(&db, 8); + assert_collection!(&db, 3, None, 1, Success; id); + assert_collection!(&db, 4, None, 1, Success; parent_id); + assert_collection!(&db, 5, None, 1, NotNullViolation; slug); + assert_collection!(&db, 6, None, 1, NotNullViolation; library_id); + assert_collection!(&db, 7, None, 1, NotNullViolation; directory); + assert_collection!(&db, 8, None, 1, Success; relative_poster_path); + } + + #[tokio::test] + async fn test_round_trip_movies() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_movie { + ($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_movie!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.id, MovieId::from_raw($id)); + assert_eq!(model.parent_id, $pid.map(CollectionId::from_raw)); + assert_eq!(model.slug, concat!("M Slug ", $id).to_string()); + assert_eq!(model.library_id, LibraryId::from_raw($lid)); + assert_eq!(model.directory, Path::new(concat!("M Directory ", $id)).to_owned().into()); + assert_eq!(model.relative_media_path, concat!("M Media ", $id)); + assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("M Poster ", $id).to_owned() $(, $($skip),+)?)); + }; + ($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_movie!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => { + super::movies::ActiveModel { + id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?), + parent_id: notsettable!(parent_id, $pid.map(CollectionId::from_raw) $(, $($skip),+)?), + slug: notsettable!(slug, concat!("M Slug ", $id).to_string() $(, $($skip),+)?), + library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?), + directory: notsettable!(directory, Path::new(concat!("M Directory ", $id)).to_owned().into() $(, $($skip),+)?), + relative_media_path: notsettable!(relative_media_path, concat!("M Media ", $id).to_owned() $(, $($skip),+)?), + relative_poster_path: notsettable!(relative_poster_path, Some(concat!("M Poster ", $id).to_owned()) $(, $($skip),+)?), + }.insert($db).await + }; + } + + make_content_library!(&db, 1); + assert_movie!(&db, 1, None, 1, ForeignKeyViolation); + make_info_movie!(&db, 1); + assert_movie!(&db, 1, Some(1), 1, ForeignKeyViolation); + make_content_collection!(&db, 1, 1, None); + assert_movie!(&db, 1, Some(1), 1, Success); + assert_movie!(&db, 2, None, 2, ForeignKeyViolation); + make_info_movie!(&db, 2); + assert_movie!(&db, 2, None, 2, ForeignKeyViolation); + make_content_library!(&db, 2); + assert_movie!(&db, 2, None, 2, Success); + + assert_movie!(&db, 1, None, 1, UniqueViolation); + make_info_movie!(&db, 3); + make_info_movie!(&db, 4); + make_info_movie!(&db, 5); + make_info_movie!(&db, 6); + make_info_movie!(&db, 7); + make_info_movie!(&db, 8); + make_info_movie!(&db, 9); + assert_movie!(&db, 3, None, 1, Success; id); + assert_movie!(&db, 4, None, 1, Success; parent_id); + assert_movie!(&db, 5, None, 1, NotNullViolation; slug); + assert_movie!(&db, 6, None, 1, NotNullViolation; library_id); + assert_movie!(&db, 7, None, 1, NotNullViolation; directory); + assert_movie!(&db, 8, None, 1, NotNullViolation; relative_media_path); + assert_movie!(&db, 9, None, 1, Success; relative_poster_path); + } + + #[tokio::test] + async fn test_round_trip_shows() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_show { + ($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_show!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.id, ShowId::from_raw($id)); + assert_eq!(model.parent_id, $pid.map(CollectionId::from_raw)); + assert_eq!(model.slug, concat!("S Slug ", $id).to_string()); + assert_eq!(model.library_id, LibraryId::from_raw($lid)); + assert_eq!(model.directory, Path::new(concat!("S Directory ", $id)).to_owned().into()); + assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("S Poster ", $id).to_owned() $(, $($skip),+)?)); + }; + ($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_show!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => { + super::shows::ActiveModel { + id: notsettable!(id, ShowId::from_raw($id) $(, $($skip),+)?), + parent_id: notsettable!(parent_id, $pid.map(CollectionId::from_raw) $(, $($skip),+)?), + slug: notsettable!(slug, concat!("S Slug ", $id).to_string() $(, $($skip),+)?), + library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?), + directory: notsettable!(directory, Path::new(concat!("S Directory ", $id)).to_owned().into() $(, $($skip),+)?), + relative_poster_path: notsettable!(relative_poster_path, Some(concat!("S Poster ", $id).to_owned()) $(, $($skip),+)?), + }.insert($db).await + }; + } + + make_content_library!(&db, 1); + assert_show!(&db, 1, None, 1, ForeignKeyViolation); + make_info_show!(&db, 1); + assert_show!(&db, 1, Some(1), 1, ForeignKeyViolation); + make_content_collection!(&db, 1, 1, None); + assert_show!(&db, 1, Some(1), 1, Success); + assert_show!(&db, 2, None, 2, ForeignKeyViolation); + make_info_show!(&db, 2); + assert_show!(&db, 2, None, 2, ForeignKeyViolation); + make_content_library!(&db, 2); + assert_show!(&db, 2, None, 2, Success); + + assert_show!(&db, 1, None, 1, UniqueViolation); + make_info_show!(&db, 3); + make_info_show!(&db, 4); + make_info_show!(&db, 5); + make_info_show!(&db, 6); + make_info_show!(&db, 7); + make_info_show!(&db, 8); + assert_show!(&db, 3, None, 1, Success; id); + assert_show!(&db, 4, None, 1, Success; parent_id); + assert_show!(&db, 5, None, 1, NotNullViolation; slug); + assert_show!(&db, 6, None, 1, NotNullViolation; library_id); + assert_show!(&db, 7, None, 1, NotNullViolation; directory); + assert_show!(&db, 8, None, 1, Success; relative_poster_path); + } + + #[tokio::test] + async fn test_round_trip_seasons() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_season { + ($db:expr, $id:literal, $season:literal, $lid:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_season!(@insert, $db, $id, $season, $lid $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.show_id, ShowId::from_raw($id)); + assert_eq!(model.season_number, $season); + assert_eq!(model.slug, concat!("SS Slug ", $id, ",", $season).to_string()); + assert_eq!(model.library_id, LibraryId::from_raw($lid)); + assert_eq!(model.directory, Path::new(concat!("SS Directory ", $id, ",", $season)).to_owned().into()); + assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("SS Poster ", $id, ",", $season).to_owned() $(, $($skip),+)?)); + }; + ($db:expr, $id:literal, $season:literal, $lid:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_season!(@insert, $db, $id, $season, $lid $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal, $season:literal, $lid:literal $(; $($skip:ident),+)?) => { + super::seasons::ActiveModel { + show_id: notsettable!(show_id, ShowId::from_raw($id) $(, $($skip),+)?), + season_number: notsettable!(season_number, $season $(, $($skip),+)?), + slug: notsettable!(slug, concat!("SS Slug ", $id, ",", $season).to_string() $(, $($skip),+)?), + library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?), + directory: notsettable!(directory, Path::new(concat!("SS Directory ", $id, ",", $season)).to_owned().into() $(, $($skip),+)?), + relative_poster_path: notsettable!(relative_poster_path, Some(concat!("SS Poster ", $id, ",", $season).to_owned()) $(, $($skip),+)?), + }.insert($db).await + }; + } + + make_content_library!(&db, 1); + make_info_show!(&db, 1); + assert_season!(&db, 1, 1, 1, ForeignKeyViolation); + make_info_season!(&db, 1, 1); + assert_season!(&db, 1, 1, 1, Success); + + assert_season!(&db, 1, 1, 1, UniqueViolation); + make_info_season!(&db, 1, 3); + make_info_season!(&db, 1, 4); + make_info_season!(&db, 1, 5); + make_info_season!(&db, 1, 6); + make_info_season!(&db, 1, 7); + make_info_season!(&db, 1, 8); + assert_season!(&db, 1, 3, 1, NotNullViolation; show_id); + assert_season!(&db, 1, 4, 1, NotNullViolation; season_number); + assert_season!(&db, 1, 5, 1, NotNullViolation; slug); + assert_season!(&db, 1, 6, 1, NotNullViolation; library_id); + assert_season!(&db, 1, 7, 1, NotNullViolation; directory); + assert_season!(&db, 1, 8, 1, Success; relative_poster_path); + } + + #[tokio::test] + async fn test_round_trip_episodes() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_episode { + ($db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_episode!(@insert, $db, $id, $season, $episode, $lid $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.show_id, ShowId::from_raw($id)); + assert_eq!(model.season_number, $season); + assert_eq!(model.episode_number, $episode); + assert_eq!(model.slug, concat!("SS Slug ", $id, ",", $season, $episode).to_string()); + assert_eq!(model.library_id, LibraryId::from_raw($lid)); + assert_eq!(model.directory, Path::new(concat!("SS Directory ", $id, ",", $season, $episode)).to_owned().into()); + assert_eq!(model.relative_media_path, concat!("SS Media ", $id, ",", $season, $episode)); + assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("SS Poster ", $id, ",", $season, $episode).to_owned() $(, $($skip),+)?)); + }; + ($db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_episode!(@insert, $db, $id, $season, $episode, $lid $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal $(; $($skip:ident),+)?) => { + super::episodes::ActiveModel { + show_id: notsettable!(show_id, ShowId::from_raw($id) $(, $($skip),+)?), + season_number: notsettable!(season_number, $season $(, $($skip),+)?), + episode_number: notsettable!(episode_number, $episode $(, $($skip),+)?), + count: notsettable!(count, 0 $(, $($skip),+)?), + slug: notsettable!(slug, concat!("SS Slug ", $id, ",", $season, $episode).to_string() $(, $($skip),+)?), + library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?), + directory: notsettable!(directory, Path::new(concat!("SS Directory ", $id, ",", $season, $episode)).to_owned().into() $(, $($skip),+)?), + relative_media_path: notsettable!(relative_media_path, concat!("SS Media ", $id, ",", $season, $episode).to_owned() $(, $($skip),+)?), + relative_poster_path: notsettable!(relative_poster_path, Some(concat!("SS Poster ", $id, ",", $season, $episode).to_owned()) $(, $($skip),+)?), + }.insert($db).await + }; + } + + make_content_library!(&db, 1); + make_info_show!(&db, 1); + make_info_season!(&db, 1, 1); + assert_episode!(&db, 1, 1, 1, 1, ForeignKeyViolation); + make_info_episode!(&db, 1, 1, 1); + assert_episode!(&db, 1, 1, 1, 1, Success); + + assert_episode!(&db, 1, 1, 1, 1, UniqueViolation); + make_info_episode!(&db, 1, 1, 3); + make_info_episode!(&db, 1, 1, 4); + make_info_episode!(&db, 1, 1, 5); + make_info_episode!(&db, 1, 1, 6); + make_info_episode!(&db, 1, 1, 7); + make_info_episode!(&db, 1, 1, 8); + make_info_episode!(&db, 1, 1, 9); + make_info_episode!(&db, 1, 1, 10); + assert_episode!(&db, 1, 1, 3, 1, NotNullViolation; show_id); + assert_episode!(&db, 1, 1, 4, 1, NotNullViolation; season_number); + assert_episode!(&db, 1, 1, 5, 1, NotNullViolation; episode_number); + assert_episode!(&db, 1, 1, 6, 1, NotNullViolation; slug); + assert_episode!(&db, 1, 1, 7, 1, NotNullViolation; library_id); + assert_episode!(&db, 1, 1, 8, 1, NotNullViolation; directory); + assert_episode!(&db, 1, 1, 9, 1, NotNullViolation; relative_media_path); + assert_episode!(&db, 1, 1, 10, 1, Success; relative_poster_path); + } +} diff --git a/crates/db/src/entity/content/collections.rs b/crates/db/src/entity/content/collections.rs deleted file mode 100644 index c7e074e..0000000 --- a/crates/db/src/entity/content/collections.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! Collection entity - -use flix_model::id::{CollectionId, LibraryId}; - -use seamantic::model::path::PathBytes; - -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a collection media folder -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_collections")] -pub struct Model { - /// The collection's ID - #[sea_orm(primary_key, auto_increment = false)] - pub id: CollectionId, - /// The collection's parent - pub parent: Option, - /// The collection's slug - pub slug: String, - /// The collection's library ID - pub library: LibraryId, - /// The collection's directory - pub directory: PathBytes, - /// The collection's poster path - pub relative_poster_path: Option, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The parent collection of this collection - #[sea_orm( - belongs_to = "super::collections::Entity", - from = "Column::Parent", - to = "super::collections::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Parent, - /// The library this collection belongs to - #[sea_orm( - belongs_to = "super::libraries::Entity", - from = "Column::Library", - to = "super::libraries::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Library, - /// The media info for this collection - #[sea_orm( - belongs_to = "super::super::info::collections::Entity", - from = "Column::Id", - to = "super::super::info::collections::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - MediaInfo, - /// The watched info for this collection - #[sea_orm( - belongs_to = "super::super::watched::collections::Entity", - from = "Column::Id", - to = "super::super::watched::collections::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - WatchInfo, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Parent.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Library.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::MediaInfo.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WatchInfo.def() - } -} diff --git a/crates/db/src/entity/content/episodes.rs b/crates/db/src/entity/content/episodes.rs deleted file mode 100644 index 4941d7b..0000000 --- a/crates/db/src/entity/content/episodes.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Episode entity - -use flix_model::id::{LibraryId, ShowId}; - -use seamantic::model::path::PathBytes; - -use flix_model::numbers::{EpisodeNumber, SeasonNumber}; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a episode media folder -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_episodes")] -pub struct Model { - /// The episode's show's ID - #[sea_orm(primary_key, auto_increment = false)] - pub show: ShowId, - /// The episode's season's number - #[sea_orm(primary_key, auto_increment = false)] - pub season: SeasonNumber, - /// The episode's number - #[sea_orm(primary_key, auto_increment = false)] - pub episode: EpisodeNumber, - /// The number of additional contained episodes - pub count: u8, - /// The episode's slug - pub slug: String, - /// The episode's library - pub library: LibraryId, - /// The episode's directory - pub directory: PathBytes, - /// The episode's media path - pub relative_media_path: String, - /// The episode's poster path - pub relative_poster_path: Option, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The library this episode belongs to - #[sea_orm( - belongs_to = "super::libraries::Entity", - from = "Column::Library", - to = "super::libraries::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Library, - /// The media info for this episode - #[sea_orm( - belongs_to = "super::super::info::episodes::Entity", - from = "(Column::Show, Column::Season, Column::Episode)", - to = "(super::super::info::episodes::Column::Show, super::super::info::episodes::Column::Season, super::super::info::episodes::Column::Episode)", - on_update = "Cascade", - on_delete = "Cascade" - )] - MediaInfo, - /// The watched info for this episode - #[sea_orm( - belongs_to = "super::super::watched::episodes::Entity", - from = "(Column::Show, Column::Season, Column::Episode)", - to = "(super::super::watched::episodes::Column::Show, super::super::watched::episodes::Column::Season, super::super::watched::episodes::Column::Episode)", - on_update = "Cascade", - on_delete = "Cascade" - )] - WatchInfo, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Library.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::MediaInfo.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WatchInfo.def() - } -} diff --git a/crates/db/src/entity/content/libraries.rs b/crates/db/src/entity/content/libraries.rs deleted file mode 100644 index 9ac6808..0000000 --- a/crates/db/src/entity/content/libraries.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! Library entity - -use flix_model::id::LibraryId; - -use seamantic::model::path::PathBytes; - -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a library media folder -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_libraries")] -pub struct Model { - /// The library's ID - #[sea_orm(primary_key, auto_increment = false)] - pub id: LibraryId, - /// The library's directory - pub directory: PathBytes, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// All collections in this library - #[sea_orm(has_many = "super::collections::Entity")] - Collections, - #[sea_orm(has_many = "super::movies::Entity")] - /// All movies in this library - Movies, - #[sea_orm(has_many = "super::shows::Entity")] - /// All shows in this library - Shows, - #[sea_orm(has_many = "super::seasons::Entity")] - /// All seasons in this library - Seasons, - #[sea_orm(has_many = "super::episodes::Entity")] - /// All episodes in this library - Episodes, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Collections.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Movies.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Shows.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Seasons.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Episodes.def() - } -} diff --git a/crates/db/src/entity/content/mod.rs b/crates/db/src/entity/content/mod.rs deleted file mode 100644 index c8d4cac..0000000 --- a/crates/db/src/entity/content/mod.rs +++ /dev/null @@ -1,342 +0,0 @@ -//! This module contains entities for storing media file information - -pub mod libraries; - -pub mod collections; - -pub mod movies; - -pub mod episodes; -pub mod seasons; -pub mod shows; - -#[cfg(test)] -mod tests { - use std::path::Path; - - use flix_model::id::{CollectionId, LibraryId, MovieId, ShowId}; - - use sea_orm::ActiveModelTrait; - use sea_orm::ActiveValue::{NotSet, Set}; - use sea_orm::sqlx::error::ErrorKind; - use sea_orm_migration::MigratorTrait; - - use crate::migration::Migrator; - use crate::tests::new_memory_db; - - use super::super::tests::get_error_kind; - use super::super::tests::{ - make_flix_collection, make_flix_episode, make_flix_movie, make_flix_season, make_flix_show, - }; - use super::super::tests::{noneable, notsettable}; - - #[tokio::test] - async fn test_inserts() { - let db = new_memory_db().await; - Migrator::up(&db, None).await.expect("up"); - - // Libraries - macro_rules! assert_library { - ($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_library!(@insert, $db, $id $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.id, LibraryId::from_raw($id)); - assert_eq!(model.directory, Path::new(concat!("/L/", $id)).to_owned().into()); - }; - ($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_library!(@insert, $db, $id $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => { - super::libraries::ActiveModel { - id: notsettable!(id, LibraryId::from_raw($id) $(, $($skip),+)?), - directory: notsettable!(directory, Path::new(concat!("/L/", $id)).to_owned().into() $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_library!(&db, 1, Success); - assert_library!(&db, 1, UniqueViolation); - assert_library!(&db, 2, Success); - assert_library!(&db, 3, Success; id); - assert_library!(&db, 4, NotNullViolation; directory); - - // Collections - macro_rules! assert_collection { - ($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_collection!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.id, CollectionId::from_raw($id)); - assert_eq!(model.parent, $pid); - assert_eq!(model.slug, concat!("C/", $id).to_string()); - assert_eq!(model.library, LibraryId::from_raw($lid)); - assert_eq!(model.directory, Path::new(concat!("/C/", $id)).to_owned().into()); - assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("C/Poster", $id).to_owned() $(, $($skip),+)?)); - }; - ($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_collection!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => { - super::collections::ActiveModel { - id: notsettable!(id, CollectionId::from_raw($id) $(, $($skip),+)?), - parent: notsettable!(parent, $pid $(, $($skip),+)?), - slug: notsettable!(slug, concat!("C/", $id).to_string() $(, $($skip),+)?), - library: notsettable!(library, LibraryId::from_raw($lid) $(, $($skip),+)?), - directory: notsettable!(directory, Path::new(concat!("/C/", $id)).to_owned().into() $(, $($skip),+)?), - relative_poster_path: notsettable!(relative_poster_path, Some(concat!("C/Poster", $id).to_owned()) $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_collection!(&db, 1, None, 0, ForeignKeyViolation); - assert_collection!( - &db, - 1, - Some(CollectionId::from_raw(0)), - 1, - ForeignKeyViolation - ); - assert_collection!(&db, 1, None, 1, ForeignKeyViolation); - make_flix_collection!(&db, 1); - make_flix_collection!(&db, 2); - make_flix_collection!(&db, 3); - make_flix_collection!(&db, 4); - make_flix_collection!(&db, 8); - - assert_collection!(&db, 1, None, 1, Success); - assert_collection!(&db, 1, None, 1, UniqueViolation); - assert_collection!(&db, 2, None, 1, Success); - assert_collection!(&db, 3, None, 1, Success; id); - assert_collection!(&db, 4, None, 1, Success; parent); - assert_collection!(&db, 5, None, 1, NotNullViolation; slug); - assert_collection!(&db, 6, None, 1, NotNullViolation; library); - assert_collection!(&db, 7, None, 1, NotNullViolation; directory); - assert_collection!(&db, 8, None, 1, Success; relative_poster_path); - - // Movies - macro_rules! assert_movie { - ($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_movie!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.id, MovieId::from_raw($id)); - assert_eq!(model.parent, $pid); - assert_eq!(model.slug, concat!("M/", $id).to_string()); - assert_eq!(model.library, LibraryId::from_raw($lid)); - assert_eq!(model.directory, Path::new(concat!("/M/", $id)).to_owned().into()); - assert_eq!(model.relative_media_path, concat!("M/Media", $id)); - assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("M/Poster", $id).to_owned() $(, $($skip),+)?)); - }; - ($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_movie!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => { - super::movies::ActiveModel { - id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?), - parent: notsettable!(parent, $pid $(, $($skip),+)?), - slug: notsettable!(slug, concat!("M/", $id).to_string() $(, $($skip),+)?), - library: notsettable!(library, LibraryId::from_raw($lid) $(, $($skip),+)?), - directory: notsettable!(directory, Path::new(concat!("/M/", $id)).to_owned().into() $(, $($skip),+)?), - relative_media_path: notsettable!(relative_media_path, concat!("M/Media", $id).to_owned() $(, $($skip),+)?), - relative_poster_path: notsettable!(relative_poster_path, Some(concat!("M/Poster", $id).to_owned()) $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_movie!(&db, 1, None, 0, ForeignKeyViolation); - assert_movie!( - &db, - 1, - Some(CollectionId::from_raw(0)), - 1, - ForeignKeyViolation - ); - assert_movie!(&db, 1, None, 1, ForeignKeyViolation); - make_flix_movie!(&db, 1); - make_flix_movie!(&db, 2); - make_flix_movie!(&db, 3); - make_flix_movie!(&db, 4); - make_flix_movie!(&db, 9); - - assert_movie!(&db, 1, None, 1, Success); - assert_movie!(&db, 1, None, 1, UniqueViolation); - assert_movie!(&db, 2, None, 1, Success); - assert_movie!(&db, 3, None, 1, Success; id); - assert_movie!(&db, 4, None, 1, Success; parent); - assert_movie!(&db, 5, None, 1, NotNullViolation; slug); - assert_movie!(&db, 6, None, 1, NotNullViolation; library); - assert_movie!(&db, 7, None, 1, NotNullViolation; directory); - assert_movie!(&db, 8, None, 1, NotNullViolation; relative_media_path); - assert_movie!(&db, 9, None, 1, Success; relative_poster_path); - - // Shows - macro_rules! assert_show { - ($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_show!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.id, ShowId::from_raw($id)); - assert_eq!(model.parent, $pid); - assert_eq!(model.slug, concat!("S/", $id).to_string()); - assert_eq!(model.library, LibraryId::from_raw($lid)); - assert_eq!(model.directory, Path::new(concat!("/S/", $id)).to_owned().into()); - assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("S/Poster", $id).to_owned() $(, $($skip),+)?)); - }; - ($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_show!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => { - super::shows::ActiveModel { - id: notsettable!(id, ShowId::from_raw($id) $(, $($skip),+)?), - parent: notsettable!(parent, $pid $(, $($skip),+)?), - slug: notsettable!(slug, concat!("S/", $id).to_string() $(, $($skip),+)?), - library: notsettable!(library, LibraryId::from_raw($lid) $(, $($skip),+)?), - directory: notsettable!(directory, Path::new(concat!("/S/", $id)).to_owned().into() $(, $($skip),+)?), - relative_poster_path: notsettable!(relative_poster_path, Some(concat!("S/Poster", $id).to_owned()) $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_show!(&db, 1, None, 0, ForeignKeyViolation); - assert_show!( - &db, - 1, - Some(CollectionId::from_raw(0)), - 1, - ForeignKeyViolation - ); - assert_show!(&db, 1, None, 1, ForeignKeyViolation); - make_flix_show!(&db, 1); - make_flix_show!(&db, 2); - make_flix_show!(&db, 3); - make_flix_show!(&db, 4); - make_flix_show!(&db, 8); - - assert_show!(&db, 1, None, 1, Success); - assert_show!(&db, 1, None, 1, UniqueViolation); - assert_show!(&db, 2, None, 1, Success); - assert_show!(&db, 3, None, 1, Success; id); - assert_show!(&db, 4, None, 1, Success; parent); - assert_show!(&db, 5, None, 1, NotNullViolation; slug); - assert_show!(&db, 6, None, 1, NotNullViolation; library); - assert_show!(&db, 7, None, 1, NotNullViolation; directory); - assert_show!(&db, 8, None, 1, Success; relative_poster_path); - - // Seasons - macro_rules! assert_season { - ($db:expr, $id:literal, $season:literal, $lid:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_season!(@insert, $db, $id, $season, $lid $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.show, ShowId::from_raw($id)); - assert_eq!(model.season, $season); - assert_eq!(model.slug, concat!("S/S", $id).to_string()); - assert_eq!(model.library, LibraryId::from_raw($lid)); - assert_eq!(model.directory, Path::new(concat!("/S/S", $id)).to_owned().into()); - assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("S/S/Poster", $id).to_owned() $(, $($skip),+)?)); - }; - ($db:expr, $id:literal, $season:literal, $lid:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_season!(@insert, $db, $id, $season, $lid $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal, $season:literal, $lid:literal $(; $($skip:ident),+)?) => { - super::seasons::ActiveModel { - show: notsettable!(id, ShowId::from_raw($id) $(, $($skip),+)?), - season: notsettable!(season, $season $(, $($skip),+)?), - slug: notsettable!(slug, concat!("S/S", $id).to_string() $(, $($skip),+)?), - library: notsettable!(library, LibraryId::from_raw($lid) $(, $($skip),+)?), - directory: notsettable!(directory, Path::new(concat!("/S/S", $id)).to_owned().into() $(, $($skip),+)?), - relative_poster_path: notsettable!(relative_poster_path, Some(concat!("S/S/Poster", $id).to_owned()) $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_season!(&db, 1, 1, 0, ForeignKeyViolation); - assert_season!(&db, 1, 1, 1, ForeignKeyViolation); - make_flix_season!(&db, 1, 1); - make_flix_season!(&db, 1, 2); - make_flix_season!(&db, 2, 1); - make_flix_season!(&db, 3, 1); - make_flix_season!(&db, 8, 1); - - assert_season!(&db, 1, 1, 1, Success); - assert_season!(&db, 1, 2, 1, Success); - assert_season!(&db, 1, 1, 1, UniqueViolation); - assert_season!(&db, 2, 1, 1, Success); - assert_season!(&db, 3, 1, 1, Success; show); - assert_season!(&db, 4, 1, 1, NotNullViolation; season); - assert_season!(&db, 5, 1, 1, NotNullViolation; slug); - assert_season!(&db, 6, 1, 1, NotNullViolation; library); - assert_season!(&db, 7, 1, 1, NotNullViolation; directory); - assert_season!(&db, 8, 1, 1, Success; relative_poster_path); - - // Episodes - macro_rules! assert_episode { - ($db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_episode!(@insert, $db, $id, $season, $episode, $lid $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.show, ShowId::from_raw($id)); - assert_eq!(model.season, $season); - assert_eq!(model.episode, $episode); - assert_eq!(model.slug, concat!("S/S/E", $id).to_string()); - assert_eq!(model.library, LibraryId::from_raw($lid)); - assert_eq!(model.directory, Path::new(concat!("/S/S/E", $id)).to_owned().into()); - assert_eq!(model.relative_media_path, concat!("E/Media", $id)); - assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("S/S/E/Poster", $id).to_owned() $(, $($skip),+)?)); - }; - ($db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_episode!(@insert, $db, $id, $season, $episode, $lid $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal $(; $($skip:ident),+)?) => { - super::episodes::ActiveModel { - show: notsettable!(id, ShowId::from_raw($id) $(, $($skip),+)?), - season: notsettable!(season, $season $(, $($skip),+)?), - episode: notsettable!(episode, $episode $(, $($skip),+)?), - count: notsettable!(count, 0 $(, $($skip),+)?), - slug: notsettable!(slug, concat!("S/S/E", $id).to_string() $(, $($skip),+)?), - library: notsettable!(library, LibraryId::from_raw($lid) $(, $($skip),+)?), - directory: notsettable!(directory, Path::new(concat!("/S/S/E", $id)).to_owned().into() $(, $($skip),+)?), - relative_media_path: notsettable!(relative_media_path, concat!("E/Media", $id).to_owned() $(, $($skip),+)?), - relative_poster_path: notsettable!(relative_poster_path, Some(concat!("S/S/E/Poster", $id).to_owned()) $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_episode!(&db, 1, 1, 1, 0, ForeignKeyViolation); - assert_episode!(&db, 1, 1, 1, 1, ForeignKeyViolation); - make_flix_episode!(&db, 1, 1, 1); - make_flix_episode!(&db, 1, 1, 2); - make_flix_episode!(&db, 2, 1, 1); - make_flix_episode!(&db, 3, 1, 1); - make_flix_show!(&db, 11); - make_flix_season!(&db, 11, 1); - make_flix_episode!(&db, 11, 1, 1); - - assert_episode!(&db, 1, 1, 1, 1, Success); - assert_episode!(&db, 1, 1, 2, 1, Success); - assert_episode!(&db, 1, 1, 1, 1, UniqueViolation); - assert_episode!(&db, 2, 1, 1, 1, Success); - assert_episode!(&db, 3, 1, 1, 1, Success; show); - assert_episode!(&db, 4, 1, 1, 1, NotNullViolation; season); - assert_episode!(&db, 5, 1, 1, 1, NotNullViolation; episode); - assert_episode!(&db, 6, 1, 1, 1, NotNullViolation; count); - assert_episode!(&db, 7, 1, 1, 1, NotNullViolation; slug); - assert_episode!(&db, 8, 1, 1, 1, NotNullViolation; library); - assert_episode!(&db, 9, 1, 1, 1, NotNullViolation; directory); - assert_episode!(&db, 10, 1, 1, 1, NotNullViolation; relative_media_path); - assert_episode!(&db, 11, 1, 1, 1, Success; relative_poster_path); - } -} diff --git a/crates/db/src/entity/content/movies.rs b/crates/db/src/entity/content/movies.rs deleted file mode 100644 index 3f2cc00..0000000 --- a/crates/db/src/entity/content/movies.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Movie entity - -use flix_model::id::{CollectionId, LibraryId, MovieId}; - -use seamantic::model::path::PathBytes; - -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a movie media folder -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_movies")] -pub struct Model { - /// The movie's ID - #[sea_orm(primary_key, auto_increment = false)] - pub id: MovieId, - /// The movie's parent - pub parent: Option, - /// The movie's slug - pub slug: String, - /// The movie's library - pub library: LibraryId, - /// The movie's directory - pub directory: PathBytes, - /// The movie's media path - pub relative_media_path: String, - /// The movie's poster path - pub relative_poster_path: Option, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The parent collection of this collection - #[sea_orm( - belongs_to = "super::collections::Entity", - from = "Column::Parent", - to = "super::collections::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Parent, - /// The library this movie belongs to - #[sea_orm( - belongs_to = "super::libraries::Entity", - from = "Column::Library", - to = "super::libraries::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Library, - /// The media info for this movie - #[sea_orm( - belongs_to = "super::super::info::movies::Entity", - from = "Column::Id", - to = "super::super::info::movies::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - MediaInfo, - /// The watched info for this movie - #[sea_orm( - belongs_to = "super::super::watched::movies::Entity", - from = "Column::Id", - to = "super::super::watched::movies::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - WatchInfo, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Parent.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Library.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::MediaInfo.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WatchInfo.def() - } -} diff --git a/crates/db/src/entity/content/seasons.rs b/crates/db/src/entity/content/seasons.rs deleted file mode 100644 index 2a74029..0000000 --- a/crates/db/src/entity/content/seasons.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Season entity - -use flix_model::id::{LibraryId, ShowId}; - -use seamantic::model::path::PathBytes; - -use flix_model::numbers::SeasonNumber; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a season media folder -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_seasons")] -pub struct Model { - /// The season's show's ID - #[sea_orm(primary_key, auto_increment = false)] - pub show: ShowId, - /// The season's number - #[sea_orm(primary_key, auto_increment = false)] - pub season: SeasonNumber, - /// The season's slug - pub slug: String, - /// The season's library - pub library: LibraryId, - /// The season's directory - pub directory: PathBytes, - /// The season's poster path - pub relative_poster_path: Option, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The library this season belongs to - #[sea_orm( - belongs_to = "super::libraries::Entity", - from = "Column::Library", - to = "super::libraries::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Library, - /// The media info for this show - #[sea_orm( - belongs_to = "super::super::info::seasons::Entity", - from = "(Column::Show, Column::Season)", - to = "(super::super::info::seasons::Column::Show, super::super::info::seasons::Column::Season)", - on_update = "Cascade", - on_delete = "Cascade" - )] - MediaInfo, - /// The watched info for this show - #[sea_orm( - belongs_to = "super::super::watched::seasons::Entity", - from = "(Column::Show, Column::Season)", - to = "(super::super::watched::seasons::Column::Show, super::super::watched::seasons::Column::Season)", - on_update = "Cascade", - on_delete = "Cascade" - )] - WatchInfo, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Library.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::MediaInfo.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WatchInfo.def() - } -} diff --git a/crates/db/src/entity/content/shows.rs b/crates/db/src/entity/content/shows.rs deleted file mode 100644 index 6dd85e3..0000000 --- a/crates/db/src/entity/content/shows.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! Show entity - -use flix_model::id::{CollectionId, LibraryId, ShowId}; - -use seamantic::model::path::PathBytes; - -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a show media folder -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_shows")] -pub struct Model { - /// The show's ID - #[sea_orm(primary_key, auto_increment = false)] - pub id: ShowId, - /// The show's parent - pub parent: Option, - /// The show's slug - pub slug: String, - /// The show's library - pub library: LibraryId, - /// The show's directory - pub directory: PathBytes, - /// The show's poster path - pub relative_poster_path: Option, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The parent collection of this collection - #[sea_orm( - belongs_to = "super::collections::Entity", - from = "Column::Parent", - to = "super::collections::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Parent, - /// The library this show belongs to - #[sea_orm( - belongs_to = "super::libraries::Entity", - from = "Column::Library", - to = "super::libraries::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Library, - /// The media info for this show - #[sea_orm( - belongs_to = "super::super::info::shows::Entity", - from = "Column::Id", - to = "super::super::info::shows::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - MediaInfo, - /// The watched info for this show - #[sea_orm( - belongs_to = "super::super::watched::shows::Entity", - from = "Column::Id", - to = "super::super::watched::shows::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - WatchInfo, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Parent.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Library.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::MediaInfo.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WatchInfo.def() - } -} diff --git a/crates/db/src/entity/info.rs b/crates/db/src/entity/info.rs new file mode 100644 index 0000000..30471dd --- /dev/null +++ b/crates/db/src/entity/info.rs @@ -0,0 +1,524 @@ +//! This module contains entities for storing media information such as +//! titles and overviews + +/// Collection entity +pub mod collections { + use flix_model::id::CollectionId; + + use sea_orm::entity::prelude::*; + + /// The database representation of a flix collection + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_info_collections")] + pub struct Model { + /// The collection's ID + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + pub id: CollectionId, + /// The collection's title + #[sea_orm(indexed)] + pub title: String, + /// The collection's overview + pub overview: String, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Movie entity +pub mod movies { + use flix_model::id::MovieId; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + /// The database representation of a flix movie + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_info_movies")] + pub struct Model { + /// The movie's ID + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + pub id: MovieId, + /// The movie's title + #[sea_orm(indexed)] + pub title: String, + /// The movie's tagline + pub tagline: String, + /// The movie's overview + pub overview: String, + /// The movie's release date + #[sea_orm(indexed)] + pub date: NaiveDate, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Show entity +pub mod shows { + use flix_model::id::ShowId; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + /// The database representation of a flix show + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_info_shows")] + pub struct Model { + /// The show's ID + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + pub id: ShowId, + /// The show's title + #[sea_orm(indexed)] + pub title: String, + /// The show's tagline + pub tagline: String, + /// The show's overview + pub overview: String, + /// The show's air date + #[sea_orm(indexed)] + pub date: NaiveDate, + + /// Seasons that are part of this show + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub seasons: HasMany, + /// Episodes that are part of this show + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub episodes: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Season entity +pub mod seasons { + use flix_model::id::ShowId; + use flix_model::numbers::SeasonNumber; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + /// The database representation of a flix season + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_info_seasons")] + pub struct Model { + /// The season's show's ID + #[sea_orm(primary_key, auto_increment = false)] + pub show_id: ShowId, + /// The season's number + #[sea_orm(primary_key, auto_increment = false)] + pub season_number: SeasonNumber, + /// The season's title + pub title: String, + /// The season's overview + pub overview: String, + /// The season's air date + #[sea_orm(indexed)] + pub date: NaiveDate, + + /// The show this season belongs to + #[sea_orm(belongs_to, from = "show_id", to = "id")] + pub show: HasOne, + /// Episodes that are part of this season + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub episodes: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Episode entity +pub mod episodes { + use flix_model::id::ShowId; + use flix_model::numbers::{EpisodeNumber, SeasonNumber}; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + /// The database representation of a flix episode + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_info_episodes")] + pub struct Model { + /// The episode's show's ID + #[sea_orm(primary_key, auto_increment = false)] + pub show_id: ShowId, + /// The episode's season's number + #[sea_orm(primary_key, auto_increment = false)] + pub season_number: SeasonNumber, + /// The episode's number + #[sea_orm(primary_key, auto_increment = false)] + pub episode_number: EpisodeNumber, + /// The episode's title + pub title: String, + /// The episode's overview + pub overview: String, + /// The episode's air date + #[sea_orm(indexed)] + pub date: NaiveDate, + + /// The show this episode belongs to + #[sea_orm(belongs_to, from = "show_id", to = "id")] + pub show: HasOne, + /// The season this episode belongs to + #[sea_orm( + belongs_to, + from = "(show_id, season_number)", + to = "(show_id, season_number)" + )] + pub season: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Macros for creating info entities +#[cfg(test)] +pub mod test { + macro_rules! make_info_collection { + ($db:expr, $id:literal) => { + $crate::entity::info::collections::ActiveModel { + id: Set(::flix_model::id::CollectionId::from_raw($id)), + title: Set(::std::string::String::new()), + overview: Set(::std::string::String::new()), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_info_collection; + + macro_rules! make_info_movie { + ($db:expr, $id:literal) => { + $crate::entity::info::movies::ActiveModel { + id: Set(::flix_model::id::MovieId::from_raw($id)), + title: Set(::std::string::String::new()), + tagline: Set(::std::string::String::new()), + overview: Set(::std::string::String::new()), + date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_info_movie; + + macro_rules! make_info_show { + ($db:expr, $id:literal) => { + $crate::entity::info::shows::ActiveModel { + id: Set(::flix_model::id::ShowId::from_raw($id)), + title: Set(::std::string::String::new()), + tagline: Set(::std::string::String::new()), + overview: Set(::std::string::String::new()), + date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_info_show; + + macro_rules! make_info_season { + ($db:expr, $show:literal, $season:literal) => { + $crate::entity::info::seasons::ActiveModel { + show_id: Set(::flix_model::id::ShowId::from_raw($show)), + season_number: Set($season), + title: Set(::std::string::String::new()), + overview: Set(::std::string::String::new()), + date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_info_season; + + macro_rules! make_info_episode { + ($db:expr, $show:literal, $season:literal, $episode:literal) => { + $crate::entity::info::episodes::ActiveModel { + show_id: Set(::flix_model::id::ShowId::from_raw($show)), + season_number: Set($season), + episode_number: Set($episode), + title: Set(::std::string::String::new()), + overview: Set(::std::string::String::new()), + date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_info_episode; +} + +#[cfg(test)] +mod tests { + use flix_model::id::{CollectionId, MovieId, ShowId}; + + use chrono::NaiveDate; + use sea_orm::ActiveValue::{NotSet, Set}; + use sea_orm::entity::prelude::*; + use sea_orm::sqlx::error::ErrorKind; + + use crate::tests::new_initialized_memory_db; + + use super::super::tests::get_error_kind; + use super::super::tests::notsettable; + use super::test::{ + make_info_collection, make_info_episode, make_info_movie, make_info_season, make_info_show, + }; + + #[tokio::test] + async fn use_test_macros() { + let db = new_initialized_memory_db().await; + + make_info_collection!(&db, 1); + make_info_movie!(&db, 1); + make_info_show!(&db, 1); + make_info_season!(&db, 1, 1); + make_info_episode!(&db, 1, 1, 1); + } + + #[tokio::test] + async fn test_round_trip_collections() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_collection { + ($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_collection!(@insert, $db, $id $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.id, CollectionId::from_raw($id)); + assert_eq!(model.title, concat!("C Title ", $id)); + assert_eq!(model.overview, concat!("C Overview ", $id)); + }; + ($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_collection!(@insert, $db, $id $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => { + super::collections::ActiveModel { + id: notsettable!(id, CollectionId::from_raw($id) $(, $($skip),+)?), + title: notsettable!(title, concat!("C Title ", $id).to_string() $(, $($skip),+)?), + overview: notsettable!(overview, concat!("C Overview ", $id).to_string() $(, $($skip),+)?), + }.insert($db).await + }; + } + + assert_collection!(&db, 1, Success); + assert_collection!(&db, 1, UniqueViolation); + assert_collection!(&db, 2, Success); + + assert_collection!(&db, 3, Success; id); + assert_collection!(&db, 4, NotNullViolation; title); + assert_collection!(&db, 5, NotNullViolation; overview); + } + + #[tokio::test] + async fn test_round_trip_movies() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_movie { + ($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_movie!(@insert, $db, $id $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.id, MovieId::from_raw($id)); + assert_eq!(model.title, concat!("M Title ", $id)); + assert_eq!(model.tagline, concat!("M Tagline ", $id)); + assert_eq!(model.overview, concat!("M Overview ", $id)); + assert_eq!(model.date, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt")); + }; + ($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_movie!(@insert, $db, $id $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => { + super::movies::ActiveModel { + id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?), + title: notsettable!(title, concat!("M Title ", $id).to_string() $(, $($skip),+)?), + tagline: notsettable!(tagline, concat!("M Tagline ", $id).to_string() $(, $($skip),+)?), + overview: notsettable!(overview, concat!("M Overview ", $id).to_string() $(, $($skip),+)?), + date: notsettable!(date, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt") $(, $($skip),+)?), + }.insert($db).await + }; + } + + assert_movie!(&db, 1, Success); + assert_movie!(&db, 1, UniqueViolation); + assert_movie!(&db, 2, Success); + + assert_movie!(&db, 3, Success; id); + assert_movie!(&db, 4, NotNullViolation; title); + assert_movie!(&db, 5, NotNullViolation; tagline); + assert_movie!(&db, 6, NotNullViolation; overview); + assert_movie!(&db, 7, NotNullViolation; date); + } + + #[tokio::test] + async fn test_round_trip_shows() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_show { + ($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_show!(@insert, $db, $id $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.id, ShowId::from_raw($id)); + assert_eq!(model.title, concat!("S Title ", $id)); + assert_eq!(model.tagline, concat!("S Tagline ", $id)); + assert_eq!(model.overview, concat!("S Overview ", $id)); + assert_eq!(model.date, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt")); + }; + ($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_show!(@insert, $db, $id $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!( + get_error_kind(model).expect("get_error_kind"), + ErrorKind::$error + ); + }; + (@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => { + super::shows::ActiveModel { + id: notsettable!(id, ShowId::from_raw($id) $(, $($skip),+)?), + title: notsettable!(title, concat!("S Title ", $id).to_string() $(, $($skip),+)?), + tagline: notsettable!(tagline, concat!("S Tagline ", $id).to_string() $(, $($skip),+)?), + overview: notsettable!(overview, concat!("S Overview ", $id).to_string() $(, $($skip),+)?), + date: notsettable!(date, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt") $(, $($skip),+)?), + }.insert($db).await + }; + } + + assert_show!(&db, 1, Success); + assert_show!(&db, 1, UniqueViolation); + assert_show!(&db, 2, Success); + + assert_show!(&db, 3, Success; id); + assert_show!(&db, 4, NotNullViolation; title); + assert_show!(&db, 5, NotNullViolation; tagline); + assert_show!(&db, 6, NotNullViolation; overview); + assert_show!(&db, 7, NotNullViolation; date); + } + + #[tokio::test] + async fn test_round_trip_seasons() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_season { + ($db:expr, $show:literal, $season:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_season!(@insert, $db, $show, $season $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.show_id, ShowId::from_raw($show)); + assert_eq!(model.season_number, $season); + assert_eq!(model.title, concat!("SS Title ", $show, ",", $season)); + assert_eq!(model.overview, concat!("SS Overview ", $show, ",", $season)); + assert_eq!(model.date, NaiveDate::from_yo_opt($show + $season, 1).expect("from_yo_opt")); + }; + ($db:expr, $show:literal, $season:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_season!(@insert, $db, $show, $season $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!( + get_error_kind(model).expect("get_error_kind"), + ErrorKind::$error + ); + }; + (@insert, $db:expr, $show:literal, $season:literal $(; $($skip:ident),+)?) => { + super::seasons::ActiveModel { + show_id: notsettable!(show_id, ShowId::from_raw($show) $(, $($skip),+)?), + season_number: notsettable!(season_number, $season $(, $($skip),+)?), + title: notsettable!(title, concat!("SS Title ", $show, ",", $season).to_string() $(, $($skip),+)?), + overview: notsettable!(overview, concat!("SS Overview ", $show, ",", $season).to_string() $(, $($skip),+)?), + date: notsettable!(date, NaiveDate::from_yo_opt($show + $season, 1).expect("from_yo_opt") $(, $($skip),+)?), + }.insert($db).await + }; + } + + assert_season!(&db, 1, 1, ForeignKeyViolation); + make_info_show!(&db, 1); + make_info_show!(&db, 2); + + assert_season!(&db, 1, 1, Success); + assert_season!(&db, 1, 1, UniqueViolation); + assert_season!(&db, 2, 1, Success); + assert_season!(&db, 1, 2, Success); + + assert_season!(&db, 1, 3, NotNullViolation; show_id); + assert_season!(&db, 1, 4, NotNullViolation; season_number); + assert_season!(&db, 1, 5, NotNullViolation; title); + assert_season!(&db, 1, 6, NotNullViolation; overview); + assert_season!(&db, 1, 7, NotNullViolation; date); + } + + #[tokio::test] + async fn test_round_trip_episodes() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_episode { + ($db:expr, $show:literal, $season:literal, $episode:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_episode!(@insert, $db, $show, $season, $episode $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.show_id, ShowId::from_raw($show)); + assert_eq!(model.season_number, $season); + assert_eq!(model.episode_number, $episode); + assert_eq!(model.title, concat!("SSE Title ", $show, ",", $season, ",", $episode)); + assert_eq!(model.overview, concat!("SSE Overview ", $show, ",", $season, ",", $episode)); + assert_eq!(model.date, NaiveDate::from_yo_opt($show + $season, 1).expect("from_yo_opt")); + }; + ($db:expr, $show:literal, $season:literal, $episode:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_episode!(@insert, $db, $show, $season, $episode $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!( + get_error_kind(model).expect("get_error_kind"), + ErrorKind::$error + ); + }; + (@insert, $db:expr, $show:literal, $season:literal, $episode:literal $(; $($skip:ident),+)?) => { + super::episodes::ActiveModel { + show_id: notsettable!(show_id, ShowId::from_raw($show) $(, $($skip),+)?), + season_number: notsettable!(season_number, $season $(, $($skip),+)?), + episode_number: notsettable!(episode_number, $episode $(, $($skip),+)?), + title: notsettable!(title, concat!("SSE Title ", $show, ",", $season, ",", $episode).to_string() $(, $($skip),+)?), + overview: notsettable!(overview, concat!("SSE Overview ", $show, ",", $season, ",", $episode).to_string() $(, $($skip),+)?), + date: notsettable!(date, NaiveDate::from_yo_opt($show + $season, 1).expect("from_yo_opt") $(, $($skip),+)?), + }.insert($db).await + }; + } + + assert_episode!(&db, 1, 1, 1, ForeignKeyViolation); + make_info_show!(&db, 1); + make_info_show!(&db, 2); + assert_episode!(&db, 1, 1, 1, ForeignKeyViolation); + make_info_season!(&db, 1, 1); + make_info_season!(&db, 1, 2); + make_info_season!(&db, 2, 1); + + assert_episode!(&db, 1, 1, 1, Success); + assert_episode!(&db, 1, 1, 1, UniqueViolation); + assert_episode!(&db, 2, 1, 1, Success); + assert_episode!(&db, 1, 2, 1, Success); + assert_episode!(&db, 1, 1, 2, Success); + + assert_episode!(&db, 1, 1, 3, NotNullViolation; show_id); + assert_episode!(&db, 1, 1, 4, NotNullViolation; season_number); + assert_episode!(&db, 1, 1, 4, NotNullViolation; episode_number); + assert_episode!(&db, 1, 1, 5, NotNullViolation; title); + assert_episode!(&db, 1, 1, 6, NotNullViolation; overview); + assert_episode!(&db, 1, 1, 7, NotNullViolation; date); + } +} diff --git a/crates/db/src/entity/info/collections.rs b/crates/db/src/entity/info/collections.rs deleted file mode 100644 index 8e9fd3e..0000000 --- a/crates/db/src/entity/info/collections.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Collection entity - -use flix_model::id::CollectionId; - -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, -}; - -/// The database representation of a flix collection -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_info_collections")] -pub struct Model { - /// The collection's ID - #[sea_orm(primary_key, auto_increment = false)] - pub id: CollectionId, - /// The collection's title - pub title: String, - /// The collection's overview - pub overview: String, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation {} diff --git a/crates/db/src/entity/info/episodes.rs b/crates/db/src/entity/info/episodes.rs deleted file mode 100644 index e01f148..0000000 --- a/crates/db/src/entity/info/episodes.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Episode entity - -use flix_model::id::ShowId; - -use chrono::NaiveDate; -use flix_model::numbers::{EpisodeNumber, SeasonNumber}; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a flix episode -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_info_episodes")] -pub struct Model { - /// The episode's show's ID - #[sea_orm(primary_key, auto_increment = false)] - pub show: ShowId, - /// The episode's season's number - #[sea_orm(primary_key, auto_increment = false)] - pub season: SeasonNumber, - /// The episode's number - #[sea_orm(primary_key, auto_increment = false)] - pub episode: EpisodeNumber, - /// The episode's title - pub title: String, - /// The episode's overview - pub overview: String, - /// The episode's air date - pub date: NaiveDate, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The show this season belongs to - #[sea_orm( - belongs_to = "super::shows::Entity", - from = "Column::Show", - to = "super::shows::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Show, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Show.def() - } -} diff --git a/crates/db/src/entity/info/mod.rs b/crates/db/src/entity/info/mod.rs deleted file mode 100644 index 96c2e6f..0000000 --- a/crates/db/src/entity/info/mod.rs +++ /dev/null @@ -1,230 +0,0 @@ -//! This module contains entities for storing media information such as -//! titles and overviews - -pub mod collections; - -pub mod movies; - -pub mod episodes; -pub mod seasons; -pub mod shows; - -#[cfg(test)] -mod tests { - use flix_model::id::{CollectionId, MovieId, ShowId}; - - use chrono::NaiveDate; - use sea_orm::ActiveModelTrait; - use sea_orm::ActiveValue::{NotSet, Set}; - use sea_orm::sqlx::error::ErrorKind; - use sea_orm_migration::MigratorTrait; - - use crate::migration::Migrator; - use crate::tests::new_memory_db; - - use super::super::tests::get_error_kind; - use super::super::tests::notsettable; - - #[tokio::test] - async fn test_inserts() { - let db = new_memory_db().await; - Migrator::up(&db, None).await.expect("up"); - - // Collections - macro_rules! assert_collection { - ($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_collection!(@insert, $db, $id $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.id, CollectionId::from_raw($id)); - assert_eq!(model.title, concat!("C", $id)); - assert_eq!(model.overview, concat!("Collection", " ", $id)); - }; - ($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_collection!(@insert, $db, $id $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => { - super::collections::ActiveModel { - id: notsettable!(id, CollectionId::from_raw($id) $(, $($skip),+)?), - title: notsettable!(title, concat!("C", $id).to_string() $(, $($skip),+)?), - overview: notsettable!(overview, concat!("Collection", " ", $id).to_string() $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_collection!(&db, 1, Success); - assert_collection!(&db, 1, UniqueViolation); - assert_collection!(&db, 2, Success); - assert_collection!(&db, 3, Success; id); - assert_collection!(&db, 4, NotNullViolation; title); - assert_collection!(&db, 5, NotNullViolation; overview); - - // Movies - macro_rules! assert_movie { - ($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_movie!(@insert, $db, $id $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.id, MovieId::from_raw($id)); - assert_eq!(model.title, concat!("M", $id)); - assert_eq!(model.tagline, concat!("Watch Movie", " ", $id)); - assert_eq!(model.overview, concat!("Movie", " ", $id)); - assert_eq!(model.date, NaiveDate::from_yo_opt(2000, $id).expect("NaiveDate::from_yo_opt")); - }; - ($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_movie!(@insert, $db, $id $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => { - super::movies::ActiveModel { - id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?), - title: notsettable!(title, concat!("M", $id).to_string() $(, $($skip),+)?), - tagline: notsettable!(tagline, concat!("Watch Movie", " ", $id).to_string() $(, $($skip),+)?), - overview: notsettable!(overview, concat!("Movie", " ", $id).to_string() $(, $($skip),+)?), - date: notsettable!(date, NaiveDate::from_yo_opt(2000, $id).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_movie!(&db, 1, Success); - assert_movie!(&db, 1, UniqueViolation); - assert_movie!(&db, 2, Success); - assert_movie!(&db, 3, Success; id); - assert_movie!(&db, 4, NotNullViolation; title); - assert_movie!(&db, 5, NotNullViolation; tagline); - assert_movie!(&db, 6, NotNullViolation; overview); - assert_movie!(&db, 7, NotNullViolation; date); - - // Shows - macro_rules! assert_show { - ($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_show!(@insert, $db, $id $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.id, ShowId::from_raw($id)); - assert_eq!(model.title, concat!("S", $id)); - assert_eq!(model.tagline, concat!("Watch Show", " ", $id)); - assert_eq!(model.overview, concat!("Show", " ", $id)); - assert_eq!(model.date, NaiveDate::from_yo_opt(2000, $id).expect("NaiveDate::from_yo_opt")); - }; - ($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_show!(@insert, $db, $id $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!( - get_error_kind(model).expect("get_error_kind"), - ErrorKind::$error - ); - }; - (@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => { - super::shows::ActiveModel { - id: notsettable!(id, ShowId::from_raw($id) $(, $($skip),+)?), - title: notsettable!(title, concat!("S", $id).to_string() $(, $($skip),+)?), - tagline: notsettable!(tagline, concat!("Watch Show", " ", $id).to_string() $(, $($skip),+)?), - overview: notsettable!(overview, concat!("Show", " ", $id).to_string() $(, $($skip),+)?), - date: notsettable!(date, NaiveDate::from_yo_opt(2000, $id).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_show!(&db, 1, Success); - assert_show!(&db, 1, UniqueViolation); - assert_show!(&db, 2, Success); - assert_show!(&db, 3, Success; id); - assert_show!(&db, 4, NotNullViolation; title); - assert_show!(&db, 5, NotNullViolation; tagline); - assert_show!(&db, 6, NotNullViolation; overview); - assert_show!(&db, 7, NotNullViolation; date); - - // Seasons - macro_rules! assert_season { - ($db:expr, $show:literal, $season:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_season!(@insert, $db, $show, $season $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.show, ShowId::from_raw($show)); - assert_eq!(model.season, $season); - assert_eq!(model.title, concat!("S", $show, "S", $season)); - assert_eq!(model.overview, concat!("Show", " ", $show, " ", "Season", " ", $season)); - assert_eq!(model.date, NaiveDate::from_yo_opt(2000, $show + $season).expect("NaiveDate::from_yo_opt")); - }; - ($db:expr, $show:literal, $season:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_season!(@insert, $db, $show, $season $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!( - get_error_kind(model).expect("get_error_kind"), - ErrorKind::$error - ); - }; - (@insert, $db:expr, $show:literal, $season:literal $(; $($skip:ident),+)?) => { - super::seasons::ActiveModel { - show: notsettable!(show, ShowId::from_raw($show) $(, $($skip),+)?), - season: notsettable!(season, $season $(, $($skip),+)?), - title: notsettable!(title, concat!("S", $show, "S", $season).to_string() $(, $($skip),+)?), - overview: notsettable!(overview, concat!("Show", " ", $show, " ", "Season", " ", $season).to_string() $(, $($skip),+)?), - date: notsettable!(date, NaiveDate::from_yo_opt(2000, $show + $season).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_season!(&db, 1, 1, Success); - assert_season!(&db, 1, 1, UniqueViolation); - assert_season!(&db, 2, 1, Success); - assert_season!(&db, 0, 1, ForeignKeyViolation); - assert_season!(&db, 1, 2, Success); - assert_season!(&db, 1, 3, NotNullViolation; show); - assert_season!(&db, 1, 4, NotNullViolation; season); - assert_season!(&db, 1, 5, NotNullViolation; title); - assert_season!(&db, 1, 6, NotNullViolation; overview); - assert_season!(&db, 1, 7, NotNullViolation; date); - - // Episodes - macro_rules! assert_episode { - ($db:expr, $show:literal, $season:literal, $episode:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_episode!(@insert, $db, $show, $season, $episode $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.show, ShowId::from_raw($show)); - assert_eq!(model.season, $season); - assert_eq!(model.episode, $episode); - assert_eq!(model.title, concat!("S", $show, "S", $season, "E", $episode)); - assert_eq!(model.overview, concat!("Show", " ", $show, " ", "Season", " ", $season, " ", "Episode", " ", $episode)); - assert_eq!(model.date, NaiveDate::from_yo_opt(2000, $show + $season + $episode).expect("NaiveDate::from_yo_opt")); - }; - ($db:expr, $show:literal, $season:literal, $episode:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_episode!(@insert, $db, $show, $season, $episode $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!( - get_error_kind(model).expect("get_error_kind"), - ErrorKind::$error - ); - }; - (@insert, $db:expr, $show:literal, $season:literal, $episode:literal $(; $($skip:ident),+)?) => { - super::episodes::ActiveModel { - show: notsettable!(show, ShowId::from_raw($show) $(, $($skip),+)?), - season: notsettable!(season, $season $(, $($skip),+)?), - episode: notsettable!(episode, $episode $(, $($skip),+)?), - title: notsettable!(title, concat!("S", $show, "S", $season, "E", $episode).to_string() $(, $($skip),+)?), - overview: notsettable!(overview, concat!("Show", " ", $show, " ", "Season", " ", $season, " ", "Episode", " ", $episode).to_string() $(, $($skip),+)?), - date: notsettable!(date, NaiveDate::from_yo_opt(2000, $show + $season + $episode).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_episode!(&db, 1, 1, 1, Success); - assert_episode!(&db, 1, 1, 1, UniqueViolation); - assert_episode!(&db, 1, 2, 1, Success); - assert_episode!(&db, 2, 1, 1, Success); - assert_episode!(&db, 1, 0, 1, ForeignKeyViolation); - assert_episode!(&db, 0, 1, 1, ForeignKeyViolation); - assert_episode!(&db, 1, 1, 2, Success); - assert_episode!(&db, 1, 1, 3, NotNullViolation; show); - assert_episode!(&db, 1, 1, 4, NotNullViolation; season); - assert_episode!(&db, 1, 1, 4, NotNullViolation; episode); - assert_episode!(&db, 1, 1, 5, NotNullViolation; title); - assert_episode!(&db, 1, 1, 6, NotNullViolation; overview); - assert_episode!(&db, 1, 1, 7, NotNullViolation; date); - } -} diff --git a/crates/db/src/entity/info/movies.rs b/crates/db/src/entity/info/movies.rs deleted file mode 100644 index db1c4af..0000000 --- a/crates/db/src/entity/info/movies.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Movie entity - -use flix_model::id::MovieId; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, -}; - -/// The database representation of a flix movie -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_info_movies")] -pub struct Model { - /// The movie's ID - #[sea_orm(primary_key, auto_increment = false)] - pub id: MovieId, - /// The movie's title - pub title: String, - /// The movie's tagline - pub tagline: String, - /// The movie's overview - pub overview: String, - /// The movie's release date - pub date: NaiveDate, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation {} diff --git a/crates/db/src/entity/info/seasons.rs b/crates/db/src/entity/info/seasons.rs deleted file mode 100644 index 904f9c9..0000000 --- a/crates/db/src/entity/info/seasons.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Season entity - -use flix_model::id::ShowId; - -use chrono::NaiveDate; -use flix_model::numbers::SeasonNumber; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a flix season -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_info_seasons")] -pub struct Model { - /// The season's show's ID - #[sea_orm(primary_key, auto_increment = false)] - pub show: ShowId, - /// The season's number - #[sea_orm(primary_key, auto_increment = false)] - pub season: SeasonNumber, - /// The season's title - pub title: String, - /// The season's overview - pub overview: String, - /// The season's air date - pub date: NaiveDate, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The show this season belongs to - #[sea_orm( - belongs_to = "super::shows::Entity", - from = "Column::Show", - to = "super::shows::Column::Id", - on_update = "Cascade", - on_delete = "Cascade" - )] - Show, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Show.def() - } -} diff --git a/crates/db/src/entity/info/shows.rs b/crates/db/src/entity/info/shows.rs deleted file mode 100644 index a3ff0c9..0000000 --- a/crates/db/src/entity/info/shows.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Show entity - -use flix_model::id::ShowId; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a flix show -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_info_shows")] -pub struct Model { - /// The show's ID - #[sea_orm(primary_key, auto_increment = false)] - pub id: ShowId, - /// The show's title - pub title: String, - /// The show's tagline - pub tagline: String, - /// The show's overview - pub overview: String, - /// The show's air date - pub date: NaiveDate, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The seasons that are part of this show - #[sea_orm(has_many = "super::seasons::Entity")] - Seasons, - /// The episodes that are part of this show - #[sea_orm(has_many = "super::episodes::Entity")] - Episodes, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Seasons.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Episodes.def() - } -} diff --git a/crates/db/src/entity/mod.rs b/crates/db/src/entity/mod.rs index 7810fdb..6c49fd5 100644 --- a/crates/db/src/entity/mod.rs +++ b/crates/db/src/entity/mod.rs @@ -58,215 +58,16 @@ mod tests { /// Helper macro for writing tests for `ActiveModel` structs where /// toggling [sea_orm::ActiveValue] is needed macro_rules! noneable { - ($field:ident, $value:expr $(, $($skip:ident),+)?) => { - if noneable!(@skip, $field $(, $($skip),+)?) { - None - } else { - Some($value) - } - }; - (@skip, $field:ident $(, $skip:ident),*) => { - false $(|| stringify!($field) == stringify!($skip))* - }; - } + ($field:ident, $value:expr $(, $($skip:ident),+)?) => { + if noneable!(@skip, $field $(, $($skip),+)?) { + None + } else { + Some($value) + } + }; + (@skip, $field:ident $(, $skip:ident),*) => { + false $(|| stringify!($field) == stringify!($skip))* + }; + } pub(super) use noneable; - - /// Helper macro for creating a flix library - macro_rules! have_library { - ($db:expr, $id:literal) => { - $crate::entity::content::libraries::ActiveModel { - id: Set(::flix_model::id::LibraryId::from_raw($id)), - directory: Set(::std::path::PathBuf::new().into()), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use have_library; - - /// Helper macro for creating a flix collection - macro_rules! make_flix_collection { - ($db:expr, $id:literal) => { - $crate::entity::info::collections::ActiveModel { - id: Set(::flix_model::id::CollectionId::from_raw($id)), - title: Set(::std::string::String::new()), - overview: Set(::std::string::String::new()), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use make_flix_collection; - macro_rules! have_collection { - ($db:expr, $lid:literal, $id:literal, $pid:expr) => { - $crate::entity::tests::make_flix_collection!($db, $id); - $crate::entity::content::collections::ActiveModel { - id: Set(::flix_model::id::CollectionId::from_raw($id)), - parent: Set($pid.map(::flix_model::id::CollectionId::from_raw)), - slug: Set(::std::string::String::new()), - library: Set(::flix_model::id::LibraryId::from_raw($lid)), - directory: Set(::std::path::PathBuf::new().into()), - relative_poster_path: Set(::core::option::Option::None), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use have_collection; - - /// Helper macro for creating a flix movie - macro_rules! make_flix_movie { - ($db:expr, $id:literal) => { - $crate::entity::info::movies::ActiveModel { - id: Set(::flix_model::id::MovieId::from_raw($id)), - title: Set(::std::string::String::new()), - tagline: Set(::std::string::String::new()), - overview: Set(::std::string::String::new()), - date: Set( - ::chrono::NaiveDate::from_yo_opt(2000, $id).expect("NaiveDate::from_yo_opt") - ), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use make_flix_movie; - macro_rules! have_movie { - ($db:expr, $lid:literal, $id:literal, $pid:expr) => { - $crate::entity::tests::make_flix_movie!($db, $id); - $crate::entity::content::movies::ActiveModel { - id: Set(::flix_model::id::MovieId::from_raw($id)), - parent: Set($pid.map(::flix_model::id::CollectionId::from_raw)), - slug: Set(::std::string::String::new()), - library: Set(::flix_model::id::LibraryId::from_raw($lid)), - directory: Set(::std::path::PathBuf::new().into()), - relative_media_path: Set(::std::string::String::new()), - relative_poster_path: Set(::core::option::Option::None), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use have_movie; - - /// Helper macro for creating a flix show - macro_rules! make_flix_show { - ($db:expr, $id:literal) => { - $crate::entity::info::shows::ActiveModel { - id: Set(::flix_model::id::ShowId::from_raw($id)), - title: Set(::std::string::String::new()), - tagline: Set(::std::string::String::new()), - overview: Set(::std::string::String::new()), - date: Set( - ::chrono::NaiveDate::from_yo_opt(2000, $id).expect("NaiveDate::from_yo_opt") - ), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use make_flix_show; - macro_rules! have_show { - ($db:expr, $lid:literal, $id:literal, $pid:expr) => { - $crate::entity::tests::make_flix_show!($db, $id); - $crate::entity::content::shows::ActiveModel { - id: Set(::flix_model::id::ShowId::from_raw($id)), - parent: Set($pid.map(::flix_model::id::CollectionId::from_raw)), - slug: Set(::std::string::String::new()), - library: Set(::flix_model::id::LibraryId::from_raw($lid)), - directory: Set(::std::path::PathBuf::new().into()), - relative_poster_path: Set(::core::option::Option::None), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use have_show; - - /// Helper macro for creating a flix season - macro_rules! make_flix_season { - ($db:expr, $show:literal, $season:literal) => { - $crate::entity::info::seasons::ActiveModel { - show: Set(::flix_model::id::ShowId::from_raw($show)), - season: Set($season), - title: Set(::std::string::String::new()), - overview: Set(::std::string::String::new()), - date: Set(::chrono::NaiveDate::from_yo_opt(2000, $show + $season) - .expect("NaiveDate::from_yo_opt")), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use make_flix_season; - macro_rules! have_season { - ($db:expr, $lid:literal, $show:literal, $season:literal) => { - $crate::entity::tests::make_flix_season!($db, $show, $season); - $crate::entity::content::seasons::ActiveModel { - show: Set(::flix_model::id::ShowId::from_raw($show)), - season: Set($season), - slug: Set(::std::string::String::new()), - library: Set(::flix_model::id::LibraryId::from_raw($lid)), - directory: Set(::std::path::PathBuf::new().into()), - relative_poster_path: Set(::core::option::Option::None), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use have_season; - - /// Helper macro for creating a flix episode - macro_rules! make_flix_episode { - ($db:expr, $show:literal, $season:literal, $episode:literal) => { - $crate::entity::info::episodes::ActiveModel { - show: Set(::flix_model::id::ShowId::from_raw($show)), - season: Set($season), - episode: Set($episode), - title: Set(::std::string::String::new()), - overview: Set(::std::string::String::new()), - date: Set(::chrono::NaiveDate::from_yo_opt(2000, $show + $season) - .expect("NaiveDate::from_yo_opt")), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use make_flix_episode; - macro_rules! have_episode { - ($db:expr, $lid:literal, $show:literal, $season:literal, $episode:literal) => { - have_episode!(@make, $db, $lid, $show, $season, $episode, 0); - }; - ($db:expr, $lid:literal, $show:literal, $season:literal, $episode:literal, >1) => { - have_episode!(@make, $db, $lid, $show, $season, $episode, 1); - }; - (@make, $db:expr, $lid:literal, $show:literal, $season:literal, $episode:literal, $count:literal) => { - $crate::entity::tests::make_flix_episode!($db, $show, $season, $episode); - $crate::entity::content::episodes::ActiveModel { - show: Set(::flix_model::id::ShowId::from_raw($show)), - season: Set($season), - episode: Set($episode), - count: Set($count), - slug: Set(::std::string::String::new()), - library: Set(::flix_model::id::LibraryId::from_raw($lid)), - directory: Set(::std::path::PathBuf::new().into()), - relative_media_path: Set(::std::string::String::new()), - relative_poster_path: Set(::core::option::Option::None), - } - .insert($db) - .await - .expect("insert"); - }; - } - pub(super) use have_episode; } diff --git a/crates/db/src/entity/tmdb.rs b/crates/db/src/entity/tmdb.rs new file mode 100644 index 0000000..1e7dcc1 --- /dev/null +++ b/crates/db/src/entity/tmdb.rs @@ -0,0 +1,629 @@ +//! This module contains entities for storing dynamic data from TMDB + +/// Collection entity +pub mod collections { + use flix_model::id::CollectionId as FlixId; + use flix_tmdb::model::id::CollectionId; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a tmdb collection + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_tmdb_collections")] + pub struct Model { + /// The collection's TMDB ID + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + pub tmdb_id: CollectionId, + /// The collection's ID + #[sea_orm(unique)] + pub flix_id: FlixId, + /// The date of the last update + pub last_update: NaiveDate, + /// The number of movies in the collection + pub movie_count: u16, + + /// Movies that are in this collection + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub movies: HasMany, + /// The info for this collection + #[sea_orm(belongs_to, from = "flix_id", to = "id")] + pub info: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Movie entity +pub mod movies { + use flix_model::id::MovieId as FlixId; + use flix_tmdb::model::id::{CollectionId, MovieId}; + + use seamantic::model::duration::Seconds; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a tmdb movie + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_tmdb_movies")] + pub struct Model { + /// The movie's TMDB ID + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + pub tmdb_id: MovieId, + /// The movie's ID + #[sea_orm(unique)] + pub flix_id: FlixId, + /// The date of the last update + pub last_update: NaiveDate, + /// The movie's runtime in seconds + pub runtime: Seconds, + /// The TMDB ID of the collection this movie belongs to + #[sea_orm(indexed)] + pub collection_id: Option, + + /// The info for this collection + #[sea_orm(belongs_to, from = "collection_id", to = "tmdb_id")] + pub collection: HasOne, + /// The info for this movie + #[sea_orm(belongs_to, from = "flix_id", to = "id")] + pub info: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Show entity +pub mod shows { + use flix_model::id::ShowId as FlixId; + use flix_tmdb::model::id::ShowId; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a tmdb show + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_tmdb_shows")] + pub struct Model { + /// The show's TMDB ID + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + pub tmdb_id: ShowId, + /// The show's ID + #[sea_orm(unique)] + pub flix_id: FlixId, + /// The movie's runtime in seconds + pub last_update: NaiveDate, + /// The number of seasons the show has + pub number_of_seasons: u32, + + /// Seasons that are part of this show + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub seasons: HasMany, + /// Episodes that are part of this show + #[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")] + pub episodes: HasMany, + /// The info for this show + #[sea_orm(belongs_to, from = "flix_id", to = "id")] + pub info: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Season entity +pub mod seasons { + use flix_model::id::ShowId as FlixId; + use flix_model::numbers::SeasonNumber; + use flix_tmdb::model::id::ShowId; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a tmdb season + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_tmdb_seasons")] + pub struct Model { + /// The season's show's TMDB ID + #[sea_orm(primary_key, auto_increment = false)] + pub tmdb_show: ShowId, + /// The season's TMDB season number + #[sea_orm(primary_key, auto_increment = false)] + pub tmdb_season: SeasonNumber, + /// The season's show's ID + #[sea_orm(unique_key = "flix")] + pub flix_show: FlixId, + /// The season's number + #[sea_orm(unique_key = "flix")] + pub flix_season: SeasonNumber, + /// The date of the last update + pub last_update: NaiveDate, + + /// The show this season belongs to + #[sea_orm(belongs_to, from = "tmdb_show", to = "tmdb_id")] + pub show: HasOne, + /// The info for this season + #[sea_orm( + belongs_to, + from = "(flix_show, flix_season)", + to = "(show_id, season_number)" + )] + pub info: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Season entity +pub mod episodes { + use flix_model::id::ShowId as FlixId; + use flix_model::numbers::{EpisodeNumber, SeasonNumber}; + use flix_tmdb::model::id::ShowId; + use seamantic::model::duration::Seconds; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a tmdb episode + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_tmdb_episodes")] + pub struct Model { + /// The episode's show's TMDB ID + #[sea_orm(primary_key, auto_increment = false)] + pub tmdb_show: ShowId, + /// The episode's season's TMDB season number + #[sea_orm(primary_key, auto_increment = false)] + pub tmdb_season: SeasonNumber, + /// The episode's TMDB episode number + #[sea_orm(primary_key, auto_increment = false)] + pub tmdb_episode: EpisodeNumber, + /// The episode's show's ID + #[sea_orm(unique_key = "flix")] + pub flix_show: FlixId, + /// The episode's season's number + #[sea_orm(unique_key = "flix")] + pub flix_season: SeasonNumber, + /// The episode's number + #[sea_orm(unique_key = "flix")] + pub flix_episode: EpisodeNumber, + /// The date of the last update + pub last_update: NaiveDate, + /// The episode's runtime in seconds + pub runtime: Seconds, + + /// The show this episode belongs to + #[sea_orm(belongs_to, from = "tmdb_show", to = "tmdb_id")] + pub show: HasOne, + /// The season this episode belongs to + #[sea_orm( + belongs_to, + from = "(tmdb_show, tmdb_season)", + to = "(tmdb_show, tmdb_season)" + )] + pub season: HasOne, + /// The info for this episode + #[sea_orm( + belongs_to, + from = "(flix_show, flix_season, flix_episode)", + to = "(show_id, season_number, episode_number)" + )] + pub info: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Macros for creating tmdb entities +#[cfg(test)] +pub mod test { + macro_rules! make_tmdb_collection { + ($db:expr, $id:literal, $flix_id:literal) => { + $crate::entity::tmdb::collections::ActiveModel { + tmdb_id: Set(::flix_tmdb::model::id::CollectionId::from_raw($id)), + flix_id: Set(::flix_model::id::CollectionId::from_raw($flix_id)), + last_update: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + movie_count: Set(::core::default::Default::default()), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_tmdb_collection; + + macro_rules! make_tmdb_movie { + ($db:expr, $id:literal, $flix_id:literal) => { + $crate::entity::tmdb::movies::ActiveModel { + tmdb_id: Set(::flix_tmdb::model::id::MovieId::from_raw($id)), + flix_id: Set(::flix_model::id::MovieId::from_raw($flix_id)), + last_update: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + runtime: Set(::core::default::Default::default()), + collection_id: Set(None), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_tmdb_movie; + + macro_rules! make_tmdb_show { + ($db:expr, $id:literal, $flix_id:literal) => { + $crate::entity::tmdb::shows::ActiveModel { + tmdb_id: Set(::flix_tmdb::model::id::ShowId::from_raw($id)), + flix_id: Set(::flix_model::id::ShowId::from_raw($flix_id)), + last_update: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + number_of_seasons: Set(::core::default::Default::default()), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_tmdb_show; + + macro_rules! make_tmdb_season { + ($db:expr, $show:literal, $season:literal, $flix_show:literal, $flix_season:literal) => { + $crate::entity::tmdb::seasons::ActiveModel { + tmdb_show: Set(::flix_tmdb::model::id::ShowId::from_raw($show)), + tmdb_season: Set($season), + flix_show: Set(::flix_model::id::ShowId::from_raw($flix_show)), + flix_season: Set($flix_season), + last_update: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_tmdb_season; + + macro_rules! make_tmdb_episode { + ($db:expr, $show:literal, $season:literal, $episode:literal, $flix_show:literal, $flix_season:literal, $flix_episode:literal) => { + $crate::entity::tmdb::episodes::ActiveModel { + tmdb_show: Set(::flix_tmdb::model::id::ShowId::from_raw($show)), + tmdb_season: Set($season), + tmdb_episode: Set($episode), + flix_show: Set(::flix_model::id::ShowId::from_raw($flix_show)), + flix_season: Set($flix_season), + flix_episode: Set($flix_episode), + last_update: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + runtime: Set(::core::default::Default::default()), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_tmdb_episode; +} + +#[cfg(test)] +mod tests { + use core::time::Duration; + + use flix_model::id::{CollectionId, MovieId, ShowId}; + use flix_tmdb::model::id::{ + CollectionId as TmdbCollectionId, MovieId as TmdbMovieId, ShowId as TmdbShowId, + }; + + use chrono::NaiveDate; + use sea_orm::ActiveValue::{NotSet, Set}; + use sea_orm::entity::prelude::*; + use sea_orm::sqlx::error::ErrorKind; + + use crate::entity::info::test::{ + make_info_collection, make_info_episode, make_info_movie, make_info_season, make_info_show, + }; + use crate::tests::new_initialized_memory_db; + + use super::super::tests::get_error_kind; + use super::super::tests::notsettable; + use super::test::{ + make_tmdb_collection, make_tmdb_episode, make_tmdb_movie, make_tmdb_season, make_tmdb_show, + }; + + #[tokio::test] + async fn use_test_macros() { + let db = new_initialized_memory_db().await; + + make_info_collection!(&db, 1); + make_info_movie!(&db, 1); + make_info_show!(&db, 1); + make_info_season!(&db, 1, 1); + make_info_episode!(&db, 1, 1, 1); + + make_tmdb_collection!(&db, 1, 1); + make_tmdb_movie!(&db, 1, 1); + make_tmdb_show!(&db, 1, 1); + make_tmdb_season!(&db, 1, 1, 1, 1); + make_tmdb_episode!(&db, 1, 1, 1, 1, 1, 1); + } + + #[tokio::test] + async fn test_round_trip_collections() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_collection { + ($db:expr, $id:literal, $tid:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_collection!(@insert, $db, $id, $tid $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.tmdb_id, TmdbCollectionId::from_raw($tid)); + assert_eq!(model.flix_id, CollectionId::from_raw($id)); + assert_eq!(model.last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt")); + assert_eq!(model.movie_count, $id); + }; + ($db:expr, $id:literal, $tid:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_collection!(@insert, $db, $id, $tid $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal, $tid:literal $(; $($skip:ident),+)?) => { + super::collections::ActiveModel { + tmdb_id: notsettable!(tmdb_id, TmdbCollectionId::from_raw($tid) $(, $($skip),+)?), + flix_id: notsettable!(flix_id, CollectionId::from_raw($id) $(, $($skip),+)?), + last_update: notsettable!(last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt") $(, $($skip),+)?), + movie_count: notsettable!(movie_count, $id $(, $($skip),+)?), + }.insert($db).await + }; + } + + assert_collection!(&db, 1, 1, ForeignKeyViolation); + make_info_collection!(&db, 1); + assert_collection!(&db, 1, 1, Success); + assert_collection!(&db, 1, 1, UniqueViolation); + + assert_collection!(&db, 1, 2, UniqueViolation); + assert_collection!(&db, 2, 1, UniqueViolation); + make_info_collection!(&db, 2); + assert_collection!(&db, 2, 2, Success); + + make_info_collection!(&db, 3); + assert_collection!(&db, 3, 3, Success; tmdb_id); + assert_collection!(&db, 4, 4, NotNullViolation; flix_id); + assert_collection!(&db, 5, 5, NotNullViolation; last_update); + assert_collection!(&db, 6, 6, NotNullViolation; movie_count); + } + + #[tokio::test] + async fn test_round_trip_movies() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_movie { + ($db:expr, $id:literal, $tid:literal, $cid:expr, Success $(; $($skip:ident),+)?) => { + let model = assert_movie!(@insert, $db, $id, $tid, $cid $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.tmdb_id, TmdbMovieId::from_raw($tid)); + assert_eq!(model.flix_id, MovieId::from_raw($id)); + assert_eq!(model.last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt")); + assert_eq!(model.runtime, Duration::from_secs($tid).into()); + assert_eq!(model.collection_id, $cid.map(TmdbCollectionId::from_raw)); + }; + ($db:expr, $id:literal, $tid:literal, $cid:expr, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_movie!(@insert, $db, $id, $tid, $cid $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal, $tid:literal, $cid:expr $(; $($skip:ident),+)?) => { + super::movies::ActiveModel { + tmdb_id: notsettable!(tmdb_id, TmdbMovieId::from_raw($tid) $(, $($skip),+)?), + flix_id: notsettable!(flix_id, MovieId::from_raw($id) $(, $($skip),+)?), + last_update: notsettable!(last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt") $(, $($skip),+)?), + runtime: notsettable!(runtime, Duration::from_secs($tid).into() $(, $($skip),+)?), + collection_id: notsettable!(collection_id, $cid.map(TmdbCollectionId::from_raw) $(, $($skip),+)?), + }.insert($db).await + }; + } + + assert_movie!(&db, 1, 1, None, ForeignKeyViolation); + make_info_movie!(&db, 1); + assert_movie!(&db, 1, 1, None, Success); + assert_movie!(&db, 1, 1, None, UniqueViolation); + + make_info_movie!(&db, 2); + assert_movie!(&db, 2, 2, Some(2), ForeignKeyViolation); + make_info_collection!(&db, 2); + make_tmdb_collection!(&db, 2, 2); + assert_movie!(&db, 2, 2, Some(2), Success); + assert_movie!(&db, 1, 2, None, UniqueViolation); + assert_movie!(&db, 2, 1, None, UniqueViolation); + + make_info_movie!(&db, 3); + assert_movie!(&db, 3, 3, None, Success; tmdb_id); + assert_movie!(&db, 4, 4, None, NotNullViolation; flix_id); + assert_movie!(&db, 5, 5, None, NotNullViolation; last_update); + assert_movie!(&db, 6, 6, None, NotNullViolation; runtime); + assert_movie!(&db, 7, 7, None, ForeignKeyViolation; collection_id); + } + + #[tokio::test] + async fn test_round_trip_shows() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_show { + ($db:expr, $id:literal, $tid:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_show!(@insert, $db, $id, $tid $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.tmdb_id, TmdbShowId::from_raw($tid)); + assert_eq!(model.flix_id, ShowId::from_raw($id)); + assert_eq!(model.last_update, NaiveDate::from_yo_opt(2000, $tid).expect("NaiveDate::from_yo_opt")); + assert_eq!(model.number_of_seasons, $id); + }; + ($db:expr, $id:literal, $tid:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_show!(@insert, $db, $id, $tid $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!( + get_error_kind(model).expect("get_error_kind"), + ErrorKind::$error + ); + }; + (@insert, $db:expr, $id:literal, $tid:literal $(; $($skip:ident),+)?) => { + super::shows::ActiveModel { + tmdb_id: notsettable!(tmdb_id, TmdbShowId::from_raw($tid) $(, $($skip),+)?), + flix_id: notsettable!(flix_id, ShowId::from_raw($id) $(, $($skip),+)?), + last_update: notsettable!(last_update, NaiveDate::from_yo_opt(2000, $tid).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), + number_of_seasons: notsettable!(number_of_seasons, $id $(, $($skip),+)?), + }.insert($db).await + }; + } + + assert_show!(&db, 1, 1, ForeignKeyViolation); + make_info_show!(&db, 1); + assert_show!(&db, 1, 1, Success); + assert_show!(&db, 1, 1, UniqueViolation); + + assert_show!(&db, 1, 2, UniqueViolation); + assert_show!(&db, 2, 1, UniqueViolation); + make_info_show!(&db, 2); + assert_show!(&db, 2, 2, Success); + + make_info_show!(&db, 3); + assert_show!(&db, 3, 3, Success; tmdb_id); + assert_show!(&db, 4, 4, NotNullViolation; flix_id); + assert_show!(&db, 5, 5, NotNullViolation; last_update); + assert_show!(&db, 6, 6, NotNullViolation; number_of_seasons); + } + + #[tokio::test] + async fn test_round_trip_seasons() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_season { + ($db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_season!(@insert, $db, $show, $season, $tshow, $tseason $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.tmdb_show, TmdbShowId::from_raw($tshow)); + assert_eq!(model.tmdb_season, $tseason); + assert_eq!(model.flix_show, ShowId::from_raw($show)); + assert_eq!(model.flix_season, $season); + assert_eq!(model.last_update, NaiveDate::from_yo_opt(2000, $tshow).expect("NaiveDate::from_yo_opt")); + }; + ($db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_season!(@insert, $db, $show, $season, $tshow, $tseason $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!( + get_error_kind(model).expect("get_error_kind"), + ErrorKind::$error + ); + }; + (@insert, $db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal $(; $($skip:ident),+)?) => { + super::seasons::ActiveModel { + tmdb_show: notsettable!(tmdb_show, TmdbShowId::from_raw($tshow) $(, $($skip),+)?), + tmdb_season: notsettable!(tmdb_season, $tseason $(, $($skip),+)?), + flix_show: notsettable!(flix_show, ShowId::from_raw($show) $(, $($skip),+)?), + flix_season: notsettable!(flix_season, $season $(, $($skip),+)?), + last_update: notsettable!(last_update, NaiveDate::from_yo_opt(2000, $tshow).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), + }.insert($db).await + }; + } + + make_info_show!(&db, 1); + make_tmdb_show!(&db, 1, 1); + + assert_season!(&db, 1, 1, 1, 1, ForeignKeyViolation); + make_info_season!(&db, 1, 1); + assert_season!(&db, 1, 1, 1, 1, Success); + + assert_season!(&db, 1, 1, 1, 1, UniqueViolation); + assert_season!(&db, 1, 1, 2, 1, UniqueViolation); + assert_season!(&db, 2, 1, 1, 1, UniqueViolation); + make_info_season!(&db, 1, 2); + assert_season!(&db, 1, 2, 1, 2, Success); + + assert_season!(&db, 1, 3, 1, 3, NotNullViolation; tmdb_show); + assert_season!(&db, 1, 4, 1, 4, NotNullViolation; tmdb_season); + assert_season!(&db, 1, 5, 1, 5, NotNullViolation; flix_show); + assert_season!(&db, 1, 6, 1, 6, NotNullViolation; flix_season); + assert_season!(&db, 1, 7, 1, 7, NotNullViolation; last_update); + } + + #[tokio::test] + async fn test_round_trip_episodes() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_episode { + ($db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_episode!(@insert, $db, $show, $season, $episode, $tshow, $tseason, $tepisode $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.tmdb_show, TmdbShowId::from_raw($tshow)); + assert_eq!(model.tmdb_season, $tseason); + assert_eq!(model.tmdb_episode, $tepisode); + assert_eq!(model.flix_show, ShowId::from_raw($show)); + assert_eq!(model.flix_season, $season); + assert_eq!(model.flix_episode, $episode); + assert_eq!(model.last_update, NaiveDate::from_yo_opt(2000, $tshow).expect("NaiveDate::from_yo_opt")); + assert_eq!(model.runtime, Duration::from_secs($tshow).into()); + }; + ($db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_episode!(@insert, $db, $show, $season, $episode, $tshow, $tseason, $tepisode $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!( + get_error_kind(model).expect("get_error_kind"), + ErrorKind::$error + ); + }; + (@insert, $db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal $(; $($skip:ident),+)?) => { + super::episodes::ActiveModel { + tmdb_show: notsettable!(tmdb_show, TmdbShowId::from_raw($tshow) $(, $($skip),+)?), + tmdb_season: notsettable!(tmdb_season, $tseason $(, $($skip),+)?), + tmdb_episode: notsettable!(tmdb_episode, $tepisode $(, $($skip),+)?), + flix_show: notsettable!(flix_show, ShowId::from_raw($show) $(, $($skip),+)?), + flix_season: notsettable!(flix_season, $season $(, $($skip),+)?), + flix_episode: notsettable!(flix_episode, $episode $(, $($skip),+)?), + last_update: notsettable!(last_update, NaiveDate::from_yo_opt(2000, $tshow).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), + runtime: notsettable!(runtime, Duration::from_secs($tshow).into() $(, $($skip),+)?), + }.insert($db).await + }; + } + + make_info_show!(&db, 1); + make_info_season!(&db, 1, 1); + make_tmdb_show!(&db, 1, 1); + make_tmdb_season!(&db, 1, 1, 1, 1); + + assert_episode!(&db, 1, 1, 1, 1, 1, 1, ForeignKeyViolation); + make_info_episode!(&db, 1, 1, 1); + assert_episode!(&db, 1, 1, 1, 1, 1, 1, Success); + + assert_episode!(&db, 1, 1, 1, 1, 1, 1, UniqueViolation); + assert_episode!(&db, 1, 1, 1, 1, 2, 1, UniqueViolation); + assert_episode!(&db, 1, 1, 1, 2, 1, 1, UniqueViolation); + assert_episode!(&db, 1, 2, 1, 1, 1, 1, UniqueViolation); + assert_episode!(&db, 2, 1, 1, 1, 1, 1, UniqueViolation); + make_info_episode!(&db, 1, 1, 2); + assert_episode!(&db, 1, 1, 2, 1, 1, 2, Success); + + assert_episode!(&db, 1, 1, 3, 1, 1, 3, NotNullViolation; tmdb_show); + assert_episode!(&db, 1, 1, 3, 1, 1, 4, NotNullViolation; tmdb_season); + assert_episode!(&db, 1, 1, 3, 1, 1, 5, NotNullViolation; tmdb_episode); + assert_episode!(&db, 1, 1, 3, 1, 1, 6, NotNullViolation; flix_show); + assert_episode!(&db, 1, 1, 3, 1, 1, 7, NotNullViolation; flix_season); + assert_episode!(&db, 1, 1, 3, 1, 1, 8, NotNullViolation; flix_episode); + assert_episode!(&db, 1, 1, 3, 1, 1, 9, NotNullViolation; last_update); + assert_episode!(&db, 1, 1, 3, 1, 1, 10, NotNullViolation; runtime); + } +} diff --git a/crates/db/src/entity/tmdb/collections.rs b/crates/db/src/entity/tmdb/collections.rs deleted file mode 100644 index ebe15d3..0000000 --- a/crates/db/src/entity/tmdb/collections.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Collection entity - -use flix_model::id::CollectionId as FlixId; -use flix_tmdb::model::id::CollectionId; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a tmdb collection -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_tmdb_collections")] -pub struct Model { - /// The collection's TMDB ID - #[sea_orm(primary_key, auto_increment = false)] - pub tmdb_id: CollectionId, - /// The collection's ID - pub flix_id: FlixId, - /// The date of the last update - pub last_update: NaiveDate, - /// The number of movies in the collection - pub movie_count: u16, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The movies that are part of this collection - #[sea_orm(has_many = "super::movies::Entity")] - Movies, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Movies.def() - } -} diff --git a/crates/db/src/entity/tmdb/episodes.rs b/crates/db/src/entity/tmdb/episodes.rs deleted file mode 100644 index f377ee7..0000000 --- a/crates/db/src/entity/tmdb/episodes.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Season entity - -use flix_model::id::ShowId as FlixId; -use flix_model::numbers::{EpisodeNumber, SeasonNumber}; -use flix_tmdb::model::id::ShowId; -use seamantic::model::duration::Seconds; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, -}; - -/// The database representation of a tmdb episode -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_tmdb_episodes")] -pub struct Model { - /// The episode's show's TMDB ID - #[sea_orm(primary_key, auto_increment = false)] - pub tmdb_show: ShowId, - /// The episode's season's TMDB season number - #[sea_orm(primary_key, auto_increment = false)] - pub tmdb_season: SeasonNumber, - /// The episode's TMDB episode number - #[sea_orm(primary_key, auto_increment = false)] - pub tmdb_episode: EpisodeNumber, - /// The episode's show's ID - pub flix_show: FlixId, - /// The episode's season's number - pub flix_season: SeasonNumber, - /// The episode's number - pub flix_episode: EpisodeNumber, - /// The date of the last update - pub last_update: NaiveDate, - /// The episode's runtime in seconds - pub runtime: Seconds, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation {} diff --git a/crates/db/src/entity/tmdb/mod.rs b/crates/db/src/entity/tmdb/mod.rs deleted file mode 100644 index 3a91d95..0000000 --- a/crates/db/src/entity/tmdb/mod.rs +++ /dev/null @@ -1,278 +0,0 @@ -//! This module contains entities for storing dynamic data from TMDB - -pub mod collections; - -pub mod movies; - -pub mod episodes; -pub mod seasons; -pub mod shows; - -#[cfg(test)] -mod tests { - use core::time::Duration; - - use flix_model::id::{CollectionId, MovieId, ShowId}; - use flix_tmdb::model::id::{ - CollectionId as TmdbCollectionId, MovieId as TmdbMovieId, ShowId as TmdbShowId, - }; - - use chrono::NaiveDate; - use sea_orm::ActiveModelTrait; - use sea_orm::ActiveValue::{NotSet, Set}; - use sea_orm::sqlx::error::ErrorKind; - use sea_orm_migration::MigratorTrait; - - use crate::migration::Migrator; - use crate::tests::new_memory_db; - - use super::super::tests::get_error_kind; - use super::super::tests::notsettable; - use super::super::tests::{ - make_flix_collection, make_flix_episode, make_flix_movie, make_flix_season, make_flix_show, - }; - - #[tokio::test] - async fn test_inserts() { - let db = new_memory_db().await; - Migrator::up(&db, None).await.expect("up"); - - // Collections - macro_rules! assert_collection { - ($db:expr, $id:literal, $tid:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_collection!(@insert, $db, $id, $tid $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.tmdb_id, TmdbCollectionId::from_raw($tid)); - assert_eq!(model.flix_id, CollectionId::from_raw($id)); - assert_eq!(model.last_update, NaiveDate::from_yo_opt(2000, $tid).expect("NaiveDate::from_yo_opt")); - assert_eq!(model.movie_count, $id); - }; - ($db:expr, $id:literal, $tid:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_collection!(@insert, $db, $id, $tid $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal, $tid:literal $(; $($skip:ident),+)?) => { - super::collections::ActiveModel { - tmdb_id: notsettable!(tmdb_id, TmdbCollectionId::from_raw($tid) $(, $($skip),+)?), - flix_id: notsettable!(flix_id, CollectionId::from_raw($id) $(, $($skip),+)?), - last_update: notsettable!(last_update, NaiveDate::from_yo_opt(2000, $tid).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - movie_count: notsettable!(movie_count, $id $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_collection!(&db, 1, 1, ForeignKeyViolation); - make_flix_collection!(&db, 1); - make_flix_collection!(&db, 2); - make_flix_collection!(&db, 3); - - assert_collection!(&db, 1, 1, Success); - assert_collection!(&db, 1, 1, UniqueViolation); - assert_collection!(&db, 1, 2, UniqueViolation); - assert_collection!(&db, 2, 1, UniqueViolation); - assert_collection!(&db, 2, 2, Success); - assert_collection!(&db, 3, 3, Success; tmdb_id); - assert_collection!(&db, 4, 4, NotNullViolation; flix_id); - assert_collection!(&db, 5, 5, NotNullViolation; last_update); - assert_collection!(&db, 6, 6, NotNullViolation; movie_count); - - // Movies - macro_rules! assert_movie { - ($db:expr, $id:literal, $tid:literal, $cid:expr, Success $(; $($skip:ident),+)?) => { - let model = assert_movie!(@insert, $db, $id, $tid, $cid $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.tmdb_id, TmdbMovieId::from_raw($tid)); - assert_eq!(model.flix_id, MovieId::from_raw($id)); - assert_eq!(model.last_update, NaiveDate::from_yo_opt(2000, $tid).expect("NaiveDate::from_yo_opt")); - assert_eq!(model.runtime, Duration::from_secs($tid).into()); - assert_eq!(model.collection, $cid); - }; - ($db:expr, $id:literal, $tid:literal, $cid:expr, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_movie!(@insert, $db, $id, $tid, $cid $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal, $tid:literal, $cid:expr $(; $($skip:ident),+)?) => { - super::movies::ActiveModel { - tmdb_id: notsettable!(tmdb_id, TmdbMovieId::from_raw($tid) $(, $($skip),+)?), - flix_id: notsettable!(flix_id, MovieId::from_raw($id) $(, $($skip),+)?), - last_update: notsettable!(last_update, NaiveDate::from_yo_opt(2000, $tid).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - runtime: notsettable!(runtime, Duration::from_secs($tid).into() $(, $($skip),+)?), - collection: notsettable!(collection, $cid $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_movie!( - &db, - 1, - 1, - Some(TmdbCollectionId::from_raw(1)), - ForeignKeyViolation - ); - make_flix_movie!(&db, 1); - make_flix_movie!(&db, 2); - make_flix_movie!(&db, 3); - - assert_movie!(&db, 1, 1, Some(TmdbCollectionId::from_raw(1)), Success); - assert_movie!(&db, 1, 1, None, UniqueViolation); - assert_movie!(&db, 1, 2, None, UniqueViolation); - assert_movie!(&db, 2, 1, None, UniqueViolation); - assert_movie!(&db, 2, 2, Some(TmdbCollectionId::from_raw(1)), Success); - assert_movie!(&db, 3, 3, None, Success; tmdb_id); - assert_movie!(&db, 4, 4, None, NotNullViolation; flix_id); - assert_movie!(&db, 5, 5, None, NotNullViolation; last_update); - assert_movie!(&db, 6, 6, None, NotNullViolation; runtime); - assert_movie!(&db, 7, 7, None, ForeignKeyViolation; collection); // Must be `Set(None)` - - // Shows - macro_rules! assert_show { - ($db:expr, $id:literal, $tid:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_show!(@insert, $db, $id, $tid $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.tmdb_id, TmdbShowId::from_raw($tid)); - assert_eq!(model.flix_id, ShowId::from_raw($id)); - assert_eq!(model.last_update, NaiveDate::from_yo_opt(2000, $tid).expect("NaiveDate::from_yo_opt")); - assert_eq!(model.number_of_seasons, $id); - }; - ($db:expr, $id:literal, $tid:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_show!(@insert, $db, $id, $tid $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!( - get_error_kind(model).expect("get_error_kind"), - ErrorKind::$error - ); - }; - (@insert, $db:expr, $id:literal, $tid:literal $(; $($skip:ident),+)?) => { - super::shows::ActiveModel { - tmdb_id: notsettable!(tmdb_id, TmdbShowId::from_raw($tid) $(, $($skip),+)?), - flix_id: notsettable!(flix_id, ShowId::from_raw($id) $(, $($skip),+)?), - last_update: notsettable!(last_update, NaiveDate::from_yo_opt(2000, $tid).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - number_of_seasons: notsettable!(number_of_seasons, $id $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_show!(&db, 1, 1, ForeignKeyViolation); - make_flix_show!(&db, 1); - make_flix_show!(&db, 2); - make_flix_show!(&db, 3); - - assert_show!(&db, 1, 1, Success); - assert_show!(&db, 1, 1, UniqueViolation); - assert_show!(&db, 1, 2, UniqueViolation); - assert_show!(&db, 2, 1, UniqueViolation); - assert_show!(&db, 2, 2, Success); - assert_show!(&db, 3, 3, Success; tmdb_id); - assert_show!(&db, 4, 4, NotNullViolation; flix_id); - assert_show!(&db, 5, 5, NotNullViolation; last_update); - assert_show!(&db, 6, 6, NotNullViolation; number_of_seasons); - - // Seasons - macro_rules! assert_season { - ($db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_season!(@insert, $db, $show, $season, $tshow, $tseason $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.tmdb_show, TmdbShowId::from_raw($tshow)); - assert_eq!(model.tmdb_season, $tseason); - assert_eq!(model.flix_show, ShowId::from_raw($show)); - assert_eq!(model.flix_season, $season); - assert_eq!(model.last_update, NaiveDate::from_yo_opt(2000, $tshow).expect("NaiveDate::from_yo_opt")); - }; - ($db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_season!(@insert, $db, $show, $season, $tshow, $tseason $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!( - get_error_kind(model).expect("get_error_kind"), - ErrorKind::$error - ); - }; - (@insert, $db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal $(; $($skip:ident),+)?) => { - super::seasons::ActiveModel { - tmdb_show: notsettable!(tmdb_show, TmdbShowId::from_raw($tshow) $(, $($skip),+)?), - tmdb_season: notsettable!(tmdb_season, $tseason $(, $($skip),+)?), - flix_show: notsettable!(flix_show, ShowId::from_raw($show) $(, $($skip),+)?), - flix_season: notsettable!(flix_season, $season $(, $($skip),+)?), - last_update: notsettable!(last_update, NaiveDate::from_yo_opt(2000, $tshow).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_season!(&db, 1, 1, 1, 1, ForeignKeyViolation); - make_flix_season!(&db, 1, 1); - make_flix_season!(&db, 1, 2); - - assert_season!(&db, 1, 1, 1, 1, Success); - assert_season!(&db, 1, 1, 1, 1, UniqueViolation); - assert_season!(&db, 1, 1, 2, 1, UniqueViolation); - assert_season!(&db, 2, 1, 1, 1, UniqueViolation); - assert_season!(&db, 1, 2, 1, 2, Success); - assert_season!(&db, 1, 3, 1, 3, NotNullViolation; tmdb_show); - assert_season!(&db, 1, 4, 1, 4, NotNullViolation; tmdb_season); - assert_season!(&db, 1, 5, 1, 5, NotNullViolation; flix_show); - assert_season!(&db, 1, 6, 1, 6, NotNullViolation; flix_season); - assert_season!(&db, 1, 7, 1, 7, NotNullViolation; last_update); - - // Episodes - macro_rules! assert_episode { - ($db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_episode!(@insert, $db, $show, $season, $episode, $tshow, $tseason, $tepisode $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.tmdb_show, TmdbShowId::from_raw($tshow)); - assert_eq!(model.tmdb_season, $tseason); - assert_eq!(model.tmdb_episode, $tepisode); - assert_eq!(model.flix_show, ShowId::from_raw($show)); - assert_eq!(model.flix_season, $season); - assert_eq!(model.flix_episode, $episode); - assert_eq!(model.last_update, NaiveDate::from_yo_opt(2000, $tshow).expect("NaiveDate::from_yo_opt")); - assert_eq!(model.runtime, Duration::from_secs($tshow).into()); - }; - ($db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_episode!(@insert, $db, $show, $season, $episode, $tshow, $tseason, $tepisode $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!( - get_error_kind(model).expect("get_error_kind"), - ErrorKind::$error - ); - }; - (@insert, $db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal $(; $($skip:ident),+)?) => { - super::episodes::ActiveModel { - tmdb_show: notsettable!(tmdb_show, TmdbShowId::from_raw($tshow) $(, $($skip),+)?), - tmdb_season: notsettable!(tmdb_season, $tseason $(, $($skip),+)?), - tmdb_episode: notsettable!(tmdb_episode, $tepisode $(, $($skip),+)?), - flix_show: notsettable!(flix_show, ShowId::from_raw($show) $(, $($skip),+)?), - flix_season: notsettable!(flix_season, $season $(, $($skip),+)?), - flix_episode: notsettable!(flix_episode, $episode $(, $($skip),+)?), - last_update: notsettable!(last_update, NaiveDate::from_yo_opt(2000, $tshow).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - runtime: notsettable!(runtime, Duration::from_secs($tshow).into() $(, $($skip),+)?), - }.insert($db).await - }; - } - assert_episode!(&db, 1, 1, 1, 1, 1, 1, ForeignKeyViolation); - make_flix_episode!(&db, 1, 1, 1); - make_flix_episode!(&db, 1, 1, 2); - - assert_episode!(&db, 1, 1, 1, 1, 1, 1, Success); - assert_episode!(&db, 1, 1, 1, 1, 1, 1, UniqueViolation); - assert_episode!(&db, 1, 1, 1, 1, 2, 1, UniqueViolation); - assert_episode!(&db, 1, 1, 1, 2, 1, 1, UniqueViolation); - assert_episode!(&db, 1, 2, 1, 1, 1, 1, UniqueViolation); - assert_episode!(&db, 2, 1, 1, 1, 1, 1, UniqueViolation); - assert_episode!(&db, 1, 1, 2, 1, 1, 2, Success); - assert_episode!(&db, 1, 1, 3, 1, 1, 3, NotNullViolation; tmdb_show); - assert_episode!(&db, 1, 1, 3, 1, 1, 4, NotNullViolation; tmdb_season); - assert_episode!(&db, 1, 1, 3, 1, 1, 5, NotNullViolation; tmdb_episode); - assert_episode!(&db, 1, 1, 3, 1, 1, 6, NotNullViolation; flix_show); - assert_episode!(&db, 1, 1, 3, 1, 1, 7, NotNullViolation; flix_season); - assert_episode!(&db, 1, 1, 3, 1, 1, 8, NotNullViolation; flix_episode); - assert_episode!(&db, 1, 1, 3, 1, 1, 9, NotNullViolation; last_update); - assert_episode!(&db, 1, 1, 3, 1, 1, 10, NotNullViolation; runtime); - } -} diff --git a/crates/db/src/entity/tmdb/movies.rs b/crates/db/src/entity/tmdb/movies.rs deleted file mode 100644 index 23a34e3..0000000 --- a/crates/db/src/entity/tmdb/movies.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Movie entity - -use flix_model::id::MovieId as FlixId; -use flix_tmdb::model::id::{CollectionId, MovieId}; - -use seamantic::model::duration::Seconds; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a tmdb movie -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_tmdb_movies")] -pub struct Model { - /// The movie's TMDB ID - #[sea_orm(primary_key, auto_increment = false)] - pub tmdb_id: MovieId, - /// The movie's ID - pub flix_id: FlixId, - /// The date of the last update - pub last_update: NaiveDate, - /// The movie's runtime in seconds - pub runtime: Seconds, - /// The TMDB ID of the collection this movie belongs to - pub collection: Option, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The collection this movie belongs to - #[sea_orm( - belongs_to = "super::collections::Entity", - from = "Column::Collection", - to = "super::collections::Column::TmdbId", - on_update = "Cascade", - on_delete = "Cascade" - )] - Collection, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Collection.def() - } -} diff --git a/crates/db/src/entity/tmdb/seasons.rs b/crates/db/src/entity/tmdb/seasons.rs deleted file mode 100644 index 3ca7826..0000000 --- a/crates/db/src/entity/tmdb/seasons.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Season entity - -use flix_model::id::ShowId as FlixId; -use flix_model::numbers::SeasonNumber; -use flix_tmdb::model::id::ShowId; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a tmdb season -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_tmdb_seasons")] -pub struct Model { - /// The season's show's TMDB ID - #[sea_orm(primary_key, auto_increment = false)] - pub tmdb_show: ShowId, - /// The season's TMDB season number - #[sea_orm(primary_key, auto_increment = false)] - pub tmdb_season: SeasonNumber, - /// The season's show's ID - pub flix_show: FlixId, - /// The season's number - pub flix_season: SeasonNumber, - /// The date of the last update - pub last_update: NaiveDate, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The show this season belongs to - #[sea_orm( - belongs_to = "super::shows::Entity", - from = "Column::TmdbShow", - to = "super::shows::Column::TmdbId", - on_update = "Cascade", - on_delete = "Cascade" - )] - Show, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Show.def() - } -} diff --git a/crates/db/src/entity/tmdb/shows.rs b/crates/db/src/entity/tmdb/shows.rs deleted file mode 100644 index b8ac976..0000000 --- a/crates/db/src/entity/tmdb/shows.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Show entity - -use flix_model::id::ShowId as FlixId; -use flix_tmdb::model::id::ShowId; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, -}; - -/// The database representation of a tmdb show -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_tmdb_shows")] -pub struct Model { - /// The show's TMDB ID - #[sea_orm(primary_key, auto_increment = false)] - pub tmdb_id: ShowId, - /// The show's ID - pub flix_id: FlixId, - /// The movie's runtime in seconds - pub last_update: NaiveDate, - /// The number of seasons the show has - pub number_of_seasons: u32, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Debug, EnumIter, DeriveRelation)] -pub enum Relation { - /// The seasons that are part of this show - #[sea_orm(has_many = "super::seasons::Entity")] - Seasons, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Seasons.def() - } -} diff --git a/crates/db/src/entity/watched.rs b/crates/db/src/entity/watched.rs new file mode 100644 index 0000000..6047f26 --- /dev/null +++ b/crates/db/src/entity/watched.rs @@ -0,0 +1,555 @@ +//! This module contains entities for storing watched information + +/// Collection entity +pub mod collections { + use flix_model::id::{CollectionId, RawId}; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a watched movie + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_watched_collections")] + pub struct Model { + /// The collection's ID + #[sea_orm(primary_key, auto_increment = false)] + pub id: CollectionId, + /// The user's ID + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: RawId, + /// The date this collection was watched + pub watched_date: NaiveDate, + + /// The info for this collection + #[sea_orm(belongs_to, relation_enum = "Info", from = "id", to = "id")] + pub info: HasOne, + /// The content for this collection + #[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)] + pub content: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Movie entity +pub mod movies { + use flix_model::id::{MovieId, RawId}; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a watched movie + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_watched_movies")] + pub struct Model { + /// The movie's ID + #[sea_orm(primary_key, auto_increment = false)] + pub id: MovieId, + /// The user's ID + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: RawId, + /// The date this movie was watched + pub watched_date: NaiveDate, + + /// The info for this movie + #[sea_orm(belongs_to, from = "id", to = "id")] + pub info: HasOne, + /// The content for this movie + #[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)] + pub content: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Show entity +pub mod shows { + use flix_model::id::{RawId, ShowId}; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a watched movie + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_watched_shows")] + pub struct Model { + /// The show's ID + #[sea_orm(primary_key, auto_increment = false)] + pub id: ShowId, + /// The user's ID + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: RawId, + /// The date this show was watched + pub watched_date: NaiveDate, + + /// The info for this show + #[sea_orm(belongs_to, relation_enum = "Info", from = "id", to = "id")] + pub info: HasOne, + /// The content for this show + #[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)] + pub content: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Season entity +pub mod seasons { + use flix_model::id::{RawId, ShowId}; + use flix_model::numbers::SeasonNumber; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a watched movie + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_watched_seasons")] + pub struct Model { + /// The season's show's ID + #[sea_orm(primary_key, auto_increment = false)] + pub show_id: ShowId, + /// The season's number + #[sea_orm(primary_key, auto_increment = false)] + pub season_number: SeasonNumber, + /// The user's ID + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: RawId, + /// The date this season was watched + pub watched_date: NaiveDate, + + /// The info for this season + #[sea_orm( + belongs_to, + relation_enum = "Info", + from = "(show_id, season_number)", + to = "(show_id, season_number)" + )] + pub info: HasOne, + /// The content for this season + #[sea_orm( + belongs_to, + relation_enum = "Content", + from = "(show_id, season_number)", + to = "(show_id, season_number)", + skip_fk + )] + pub content: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Episode entity +pub mod episodes { + use flix_model::id::{RawId, ShowId}; + use flix_model::numbers::{EpisodeNumber, SeasonNumber}; + + use chrono::NaiveDate; + use sea_orm::entity::prelude::*; + + use crate::entity; + + /// The database representation of a watched movie + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "flix_watched_episodes")] + pub struct Model { + /// The episode's show's ID + #[sea_orm(primary_key, auto_increment = false)] + pub show_id: ShowId, + /// The episode's season's number + #[sea_orm(primary_key, auto_increment = false)] + pub season_number: SeasonNumber, + /// The episode's number + #[sea_orm(primary_key, auto_increment = false)] + pub episode_number: EpisodeNumber, + /// The user's ID + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: RawId, + /// The date this episode was watched + pub watched_date: NaiveDate, + + /// The info for this episode + #[sea_orm( + belongs_to, + relation_enum = "Info", + from = "(show_id, season_number, episode_number)", + to = "(show_id, season_number, episode_number)" + )] + pub info: HasOne, + /// The content for this episode + #[sea_orm( + belongs_to, + relation_enum = "Content", + from = "(show_id, season_number, episode_number)", + to = "(show_id, season_number, episode_number)", + skip_fk + )] + pub content: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Macros for creating watched entities +#[cfg(test)] +pub mod test { + macro_rules! make_watched_movie { + ($db:expr, $id:literal, $user:literal) => { + $crate::entity::watched::movies::ActiveModel { + id: Set(::flix_model::id::MovieId::from_raw($id)), + user_id: Set($user), + watched_date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_watched_movie; + + macro_rules! make_watched_episode { + ($db:expr, $show:literal, $season:literal, $episode:literal, $user:literal) => { + $crate::entity::watched::episodes::ActiveModel { + show_id: Set(::flix_model::id::ShowId::from_raw($show)), + season_number: Set($season), + episode_number: Set($episode), + user_id: Set($user), + watched_date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")), + } + .insert($db) + .await + .expect("insert"); + }; + } + pub(crate) use make_watched_episode; +} + +#[cfg(test)] +mod tests { + use flix_model::id::{MovieId, ShowId}; + + use chrono::NaiveDate; + use sea_orm::ActiveValue::{NotSet, Set}; + use sea_orm::Condition; + use sea_orm::entity::prelude::*; + use sea_orm::sqlx::error::ErrorKind; + + use crate::entity::content::test::{ + make_content_collection, make_content_episode, make_content_library, make_content_movie, + make_content_season, make_content_show, + }; + use crate::entity::info::test::{ + make_info_episode, make_info_movie, make_info_season, make_info_show, + }; + use crate::tests::new_initialized_memory_db; + + use super::super::tests::get_error_kind; + use super::super::tests::notsettable; + use super::test::{make_watched_episode, make_watched_movie}; + + #[tokio::test] + async fn use_test_macros() { + let db = new_initialized_memory_db().await; + + make_info_movie!(&db, 1); + make_info_show!(&db, 1); + make_info_season!(&db, 1, 1); + make_info_episode!(&db, 1, 1, 1); + + make_watched_movie!(&db, 1, 1); + make_watched_episode!(&db, 1, 1, 1, 1); + } + + #[tokio::test] + async fn test_round_trip_movies() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_movie { + ($db:expr, $id:literal, $uid:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_movie!(@insert, $db, $id, $uid $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.id, MovieId::from_raw($id)); + assert_eq!(model.user_id, $uid); + assert_eq!(model.watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt")); + }; + ($db:expr, $id:literal, $uid:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_movie!(@insert, $db, $id, $uid $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $id:literal, $uid:literal $(; $($skip:ident),+)?) => { + super::movies::ActiveModel { + id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?), + user_id: notsettable!(user_id, $uid $(, $($skip),+)?), + watched_date: notsettable!(watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt") $(, $($skip),+)?), + }.insert($db).await + }; + } + + assert_movie!(&db, 1, 1, ForeignKeyViolation); + make_info_movie!(&db, 1); + assert_movie!(&db, 1, 1, Success); + assert_movie!(&db, 1, 2, Success); + + assert_movie!(&db, 1, 1, UniqueViolation); + make_info_movie!(&db, 2); + assert_movie!(&db, 2, 1, Success); + assert_movie!(&db, 2, 2, Success); + + assert_movie!(&db, 3, 1, NotNullViolation; id); + assert_movie!(&db, 4, 1, NotNullViolation; user_id); + assert_movie!(&db, 5, 1, NotNullViolation; watched_date); + } + + #[tokio::test] + async fn test_round_trip_episodes() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_episode { + ($db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal, Success $(; $($skip:ident),+)?) => { + let model = assert_episode!(@insert, $db, $show, $season, $episode, $uid $(; $($skip),+)?) + .expect("insert"); + + assert_eq!(model.show_id, ShowId::from_raw($show)); + assert_eq!(model.season_number, $season); + assert_eq!(model.episode_number, $episode); + assert_eq!(model.user_id, $uid); + assert_eq!(model.watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt")); + }; + ($db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal, $error:ident $(; $($skip:ident),+)?) => { + let model = assert_episode!(@insert, $db, $show, $season, $episode, $uid $(; $($skip),+)?) + .expect_err("insert"); + + assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); + }; + (@insert, $db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal $(; $($skip:ident),+)?) => { + super::episodes::ActiveModel { + show_id: notsettable!(show_id, ShowId::from_raw($show) $(, $($skip),+)?), + season_number: notsettable!(season_number, $season $(, $($skip),+)?), + episode_number: notsettable!(episode_number, $episode $(, $($skip),+)?), + user_id: notsettable!(user_id, $uid $(, $($skip),+)?), + watched_date: notsettable!(watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt") $(, $($skip),+)?), + }.insert($db).await + }; + } + + make_info_show!(&db, 1); + make_info_season!(&db, 1, 1); + make_info_show!(&db, 2); + make_info_season!(&db, 2, 1); + + assert_episode!(&db, 1, 1, 1, 1, ForeignKeyViolation); + assert_episode!(&db, 2, 1, 1, 1, ForeignKeyViolation); + make_info_episode!(&db, 1, 1, 1); + make_info_episode!(&db, 2, 1, 1); + assert_episode!(&db, 1, 1, 1, 1, Success); + assert_episode!(&db, 1, 1, 1, 2, Success); + assert_episode!(&db, 2, 1, 1, 1, Success); + + assert_episode!(&db, 1, 1, 1, 1, UniqueViolation); + + assert_episode!(&db, 3, 1, 1, 1, NotNullViolation; show_id); + assert_episode!(&db, 4, 1, 1, 1, NotNullViolation; season_number); + assert_episode!(&db, 5, 1, 1, 1, NotNullViolation; episode_number); + assert_episode!(&db, 6, 1, 1, 1, NotNullViolation; user_id); + assert_episode!(&db, 7, 1, 1, 1, NotNullViolation; watched_date); + } + + #[tokio::test] + async fn test_query_seasons() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_season { + ($db:expr, $show:literal, $season:literal, $uid:literal, Watched) => { + assert_season!(@find, $db, $show, $season, $uid) + .ok_or(()) + .expect("is none"); + }; + ($db:expr, $show:literal, $season:literal, $uid:literal, Unwatched) => { + assert_season!(@find, $db, $show, $season, $uid) + .ok_or(()) + .expect_err("is some"); + }; + (@find, $db:expr, $show:literal, $season:literal, $uid:literal) => { + super::seasons::Entity::find() + .filter( + Condition::all() + .add(super::seasons::Column::ShowId.eq($show)) + .add(super::seasons::Column::SeasonNumber.eq($season)) + .add(super::seasons::Column::UserId.eq($uid)), + ) + .one(&db) + .await + .expect("find.filter.one") + }; + } + + make_content_library!(&db, 1); + make_content_show!(&db, 1, 1, None); + + make_content_season!(&db, 1, 1, 1); + assert_season!(&db, 1, 1, 1, Unwatched); + make_content_episode!(&db, 1, 1, 1, 1); + assert_season!(&db, 1, 1, 1, Unwatched); + make_watched_episode!(&db, 1, 1, 1, 1); + assert_season!(&db, 1, 1, 1, Watched); + assert_season!(&db, 1, 1, 2, Unwatched); + make_content_episode!(&db, 1, 1, 1, 2); + assert_season!(&db, 1, 1, 1, Unwatched); + + make_content_season!(&db, 1, 1, 2); + make_content_episode!(&db, 1, 1, 2, 1); + make_content_episode!(&db, 1, 1, 2, 2, >1); + make_info_episode!(&db, 1, 2, 3); + make_content_episode!(&db, 1, 1, 2, 4); + assert_season!(&db, 1, 2, 1, Unwatched); + assert_season!(&db, 1, 2, 2, Unwatched); + assert_season!(&db, 1, 2, 3, Unwatched); + make_watched_episode!(&db, 1, 2, 1, 1); + make_watched_episode!(&db, 1, 2, 1, 2); + make_watched_episode!(&db, 1, 2, 2, 3); + assert_season!(&db, 1, 2, 1, Unwatched); + assert_season!(&db, 1, 2, 2, Unwatched); + assert_season!(&db, 1, 2, 3, Unwatched); + make_watched_episode!(&db, 1, 2, 2, 1); + make_watched_episode!(&db, 1, 2, 2, 2); + make_watched_episode!(&db, 1, 2, 1, 3); + assert_season!(&db, 1, 2, 1, Unwatched); + assert_season!(&db, 1, 2, 2, Unwatched); + assert_season!(&db, 1, 2, 3, Unwatched); + make_watched_episode!(&db, 1, 2, 4, 1); + assert_season!(&db, 1, 2, 1, Watched); + assert_season!(&db, 1, 2, 2, Unwatched); + assert_season!(&db, 1, 2, 3, Unwatched); + } + + #[tokio::test] + async fn test_query_shows() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_show { + ($db:expr, $show:literal, $uid:literal, Watched) => { + assert_show!(@find, $db, $show, $uid) + .ok_or(()) + .expect("is none"); + }; + ($db:expr, $show:literal, $uid:literal, Unwatched) => { + assert_show!(@find, $db, $show, $uid) + .ok_or(()) + .expect_err("is some"); + }; + (@find, $db:expr, $show:literal, $uid:literal) => { + super::shows::Entity::find() + .filter( + Condition::all() + .add(super::shows::Column::Id.eq($show)) + .add(super::shows::Column::UserId.eq($uid)), + ) + .one(&db) + .await + .expect("find.filter.one") + }; + } + + make_content_library!(&db, 1); + make_content_show!(&db, 1, 1, None); + + assert_show!(&db, 1, 1, Unwatched); + make_content_season!(&db, 1, 1, 1); + assert_show!(&db, 1, 1, Unwatched); + make_content_episode!(&db, 1, 1, 1, 1); + assert_show!(&db, 1, 1, Unwatched); + make_watched_episode!(&db, 1, 1, 1, 1); + assert_show!(&db, 1, 1, Watched); + assert_show!(&db, 1, 2, Unwatched); + make_content_episode!(&db, 1, 1, 1, 2); + assert_show!(&db, 1, 1, Unwatched); + assert_show!(&db, 1, 2, Unwatched); + make_watched_episode!(&db, 1, 1, 2, 1); + + make_content_season!(&db, 1, 1, 2); + assert_show!(&db, 1, 1, Unwatched); + assert_show!(&db, 1, 2, Unwatched); + make_content_episode!(&db, 1, 1, 2, 1); + assert_show!(&db, 1, 1, Unwatched); + assert_show!(&db, 1, 2, Unwatched); + make_watched_episode!(&db, 1, 2, 1, 1); + assert_show!(&db, 1, 1, Watched); + assert_show!(&db, 1, 2, Unwatched); + } + + #[tokio::test] + async fn test_query_collections() { + let db = new_initialized_memory_db().await; + + macro_rules! assert_collection { + ($db:expr, $id:literal, $uid:literal, Watched) => { + assert_collection!(@find, $db, $id, $uid) + .ok_or(()) + .expect("is none"); + }; + ($db:expr, $id:literal, $uid:literal, Unwatched) => { + assert_collection!(@find, $db, $id, $uid) + .ok_or(()) + .expect_err("is some"); + }; + (@find, $db:expr, $id:literal, $uid:literal) => { + super::collections::Entity::find() + .filter( + Condition::all() + .add(super::collections::Column::Id.eq($id)) + .add(super::collections::Column::UserId.eq($uid)), + ) + .one(&db) + .await + .expect("find.filter.one") + }; + } + + make_content_library!(&db, 1); + make_content_collection!(&db, 1, 1, None); + assert_collection!(&db, 1, 1, Unwatched); + + make_content_movie!(&db, 1, 1, Some(1)); + assert_collection!(&db, 1, 1, Unwatched); + make_info_movie!(&db, 9999); + make_watched_movie!(&db, 9999, 1); + assert_collection!(&db, 1, 1, Unwatched); + make_watched_movie!(&db, 1, 1); + assert_collection!(&db, 1, 1, Watched); + + make_content_collection!(&db, 1, 2, Some(1)); + assert_collection!(&db, 1, 1, Watched); + assert_collection!(&db, 2, 1, Unwatched); + make_content_movie!(&db, 1, 2, Some(2)); + assert_collection!(&db, 1, 1, Unwatched); + assert_collection!(&db, 2, 1, Unwatched); + make_watched_movie!(&db, 2, 1); + assert_collection!(&db, 1, 1, Watched); + assert_collection!(&db, 2, 1, Watched); + + make_content_show!(&db, 1, 1, Some(2)); + assert_collection!(&db, 1, 1, Unwatched); + assert_collection!(&db, 2, 1, Unwatched); + make_content_season!(&db, 1, 1, 1); + make_content_episode!(&db, 1, 1, 1, 1); + make_watched_episode!(&db, 1, 1, 1, 1); + assert_collection!(&db, 1, 1, Watched); + assert_collection!(&db, 2, 1, Watched); + } +} diff --git a/crates/db/src/entity/watched/collections.rs b/crates/db/src/entity/watched/collections.rs deleted file mode 100644 index 2cd8442..0000000 --- a/crates/db/src/entity/watched/collections.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Collection entity - -use flix_model::id::{CollectionId, RawId}; - -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, -}; - -/// The database representation of a watched movie -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_watched_collections")] -pub struct Model { - /// The collection's ID - #[sea_orm(primary_key, auto_increment = false)] - pub id: CollectionId, - /// The user's ID - #[sea_orm(primary_key, auto_increment = false)] - pub user_id: RawId, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} diff --git a/crates/db/src/entity/watched/episodes.rs b/crates/db/src/entity/watched/episodes.rs deleted file mode 100644 index e4d55e1..0000000 --- a/crates/db/src/entity/watched/episodes.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Episode entity - -use flix_model::id::{RawId, ShowId}; -use flix_model::numbers::{EpisodeNumber, SeasonNumber}; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, -}; - -/// The database representation of a watched movie -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_watched_episodes")] -pub struct Model { - /// The episode's show's ID - #[sea_orm(primary_key, auto_increment = false)] - pub show: ShowId, - /// The episode's season's number - #[sea_orm(primary_key, auto_increment = false)] - pub season: SeasonNumber, - /// The episode's number - #[sea_orm(primary_key, auto_increment = false)] - pub episode: EpisodeNumber, - /// The user's ID - #[sea_orm(primary_key, auto_increment = false)] - pub user_id: RawId, - /// The date this episode was watched - pub watched_date: NaiveDate, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} diff --git a/crates/db/src/entity/watched/mod.rs b/crates/db/src/entity/watched/mod.rs deleted file mode 100644 index 5f73b58..0000000 --- a/crates/db/src/entity/watched/mod.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! This module contains entities for storing watched information - -pub mod collections; - -pub mod movies; - -pub mod episodes; -pub mod seasons; -pub mod shows; - -#[cfg(test)] -mod tests { - - use flix_model::id::{MovieId, ShowId}; - - use chrono::NaiveDate; - use sea_orm::ActiveValue::{NotSet, Set}; - use sea_orm::sqlx::error::ErrorKind; - use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, QueryFilter}; - use sea_orm_migration::MigratorTrait; - - use crate::entity::tests::{have_collection, have_library, have_movie, have_season, have_show}; - use crate::migration::Migrator; - use crate::tests::new_memory_db; - - use super::super::tests::get_error_kind; - use super::super::tests::notsettable; - use super::super::tests::{ - have_episode, make_flix_episode, make_flix_movie, make_flix_season, make_flix_show, - }; - - macro_rules! assert_movie { - ($db:expr, $id:literal, $uid:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_movie!(@insert, $db, $id, $uid $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.id, MovieId::from_raw($id)); - assert_eq!(model.user_id, $uid); - assert_eq!(model.watched_date, NaiveDate::from_yo_opt(2000, $uid).expect("NaiveDate::from_yo_opt")); - }; - ($db:expr, $id:literal, $uid:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_movie!(@insert, $db, $id, $uid $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $id:literal, $uid:literal $(; $($skip:ident),+)?) => { - super::movies::ActiveModel { - id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?), - user_id: notsettable!(user_id, $uid $(, $($skip),+)?), - watched_date: notsettable!(watched_date, NaiveDate::from_yo_opt(2000, $uid).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - }.insert($db).await - }; - } - - macro_rules! assert_episode { - ($db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal, Success $(; $($skip:ident),+)?) => { - let model = assert_episode!(@insert, $db, $show, $season, $episode, $uid $(; $($skip),+)?) - .expect("insert"); - - assert_eq!(model.show, ShowId::from_raw($show)); - assert_eq!(model.season, $season); - assert_eq!(model.episode, $episode); - assert_eq!(model.user_id, $uid); - assert_eq!(model.watched_date, NaiveDate::from_yo_opt(2000, $uid).expect("NaiveDate::from_yo_opt")); - }; - ($db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal, $error:ident $(; $($skip:ident),+)?) => { - let model = assert_episode!(@insert, $db, $show, $season, $episode, $uid $(; $($skip),+)?) - .expect_err("insert"); - - assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error); - }; - (@insert, $db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal $(; $($skip:ident),+)?) => { - super::episodes::ActiveModel { - show: notsettable!(show, ShowId::from_raw($show) $(, $($skip),+)?), - season: notsettable!(season, $season $(, $($skip),+)?), - episode: notsettable!(episode, $episode $(, $($skip),+)?), - user_id: notsettable!(user_id, $uid $(, $($skip),+)?), - watched_date: notsettable!(watched_date, NaiveDate::from_yo_opt(2000, $uid).expect("NaiveDate::from_yo_opt") $(, $($skip),+)?), - }.insert($db).await - }; - } - - #[tokio::test] - async fn test_inserts() { - let db = new_memory_db().await; - Migrator::up(&db, None).await.expect("up"); - - // Movies - assert_movie!(&db, 1, 1, ForeignKeyViolation); - make_flix_movie!(&db, 1); - make_flix_movie!(&db, 2); - - assert_movie!(&db, 1, 1, Success); - assert_movie!(&db, 1, 1, UniqueViolation); - assert_movie!(&db, 2, 1, Success); - assert_movie!(&db, 3, 1, NotNullViolation; id); - assert_movie!(&db, 4, 1, NotNullViolation; user_id); - assert_movie!(&db, 5, 1, NotNullViolation; watched_date); - - // Episodes - make_flix_show!(&db, 1); - make_flix_season!(&db, 1, 1); - make_flix_show!(&db, 2); - make_flix_season!(&db, 2, 1); - - assert_episode!(&db, 1, 1, 1, 1, ForeignKeyViolation); - make_flix_episode!(&db, 1, 1, 1); - make_flix_episode!(&db, 1, 1, 2); - make_flix_episode!(&db, 2, 1, 1); - - assert_episode!(&db, 1, 1, 1, 1, Success); - assert_episode!(&db, 1, 1, 1, 1, UniqueViolation); - assert_episode!(&db, 1, 1, 2, 1, Success); - assert_episode!(&db, 2, 1, 1, 1, Success); - assert_episode!(&db, 3, 1, 1, 1, NotNullViolation; show); - assert_episode!(&db, 4, 1, 1, 1, NotNullViolation; season); - assert_episode!(&db, 5, 1, 1, 1, NotNullViolation; episode); - assert_episode!(&db, 6, 1, 1, 1, NotNullViolation; user_id); - assert_episode!(&db, 7, 1, 1, 1, NotNullViolation; watched_date); - } - - #[tokio::test] - async fn test_queries() { - let db = new_memory_db().await; - Migrator::up(&db, None).await.expect("up"); - - have_library!(&db, 1); - - // Season + Show - macro_rules! assert_season { - ($db:expr, $show:literal, $season:literal, $uid:literal, Watched) => { - assert_season!(@find, $db, $show, $season, $uid) - .ok_or(()) - .expect("is none"); - }; - ($db:expr, $show:literal, $season:literal, $uid:literal, Unwatched) => { - assert_season!(@find, $db, $show, $season, $uid) - .ok_or(()) - .expect_err("is some"); - }; - (@find, $db:expr, $show:literal, $season:literal, $uid:literal) => { - super::seasons::Entity::find() - .filter( - Condition::all() - .add(super::seasons::Column::Show.eq($show)) - .add(super::seasons::Column::Season.eq($season)) - .add(super::seasons::Column::UserId.eq($uid)), - ) - .one(&db) - .await - .expect("find.filter.one") - }; - } - macro_rules! assert_show { - ($db:expr, $show:literal, $uid:literal, Watched) => { - assert_show!(@find, $db, $show, $uid) - .ok_or(()) - .expect("is none"); - }; - ($db:expr, $show:literal, $uid:literal, Unwatched) => { - assert_show!(@find, $db, $show, $uid) - .ok_or(()) - .expect_err("is some"); - }; - (@find, $db:expr, $show:literal, $uid:literal) => { - super::shows::Entity::find() - .filter( - Condition::all() - .add(super::shows::Column::Id.eq($show)) - .add(super::shows::Column::UserId.eq($uid)), - ) - .one(&db) - .await - .expect("find.filter.one") - }; - } - - have_show!(&db, 1, 1, None); - have_season!(&db, 1, 1, 1); - have_episode!(&db, 1, 1, 1, 1); - assert_episode!(&db, 1, 1, 1, 1, Success); - assert_episode!(&db, 1, 1, 1, 2, Success); - have_episode!(&db, 1, 1, 1, 2, >1); // Covers 2 and 3 - make_flix_episode!(&db, 1, 1, 3); - assert_episode!(&db, 1, 1, 2, 1, Success); - assert_episode!(&db, 1, 1, 2, 2, Success); - have_season!(&db, 1, 1, 2); - have_episode!(&db, 1, 1, 2, 1); - assert_episode!(&db, 1, 2, 1, 1, Success); - assert_episode!(&db, 1, 2, 1, 2, Success); - have_episode!(&db, 1, 1, 2, 2); - assert_episode!(&db, 1, 2, 2, 1, Success); - have_episode!(&db, 1, 1, 2, 3); - assert_episode!(&db, 1, 2, 3, 1, Success); - - // Add watched episodes that we do not have - make_flix_episode!(&db, 1, 1, 4); - assert_episode!(&db, 1, 1, 4, 1, Success); - make_flix_episode!(&db, 1, 2, 4); - assert_episode!(&db, 1, 2, 4, 2, Success); - make_flix_episode!(&db, 1, 2, 5); - assert_episode!(&db, 1, 2, 5, 2, Success); - - assert_season!(&db, 1, 1, 1, Watched); - assert_season!(&db, 1, 1, 2, Watched); - assert_season!(&db, 1, 1, 3, Unwatched); - assert_season!(&db, 1, 2, 1, Watched); - assert_season!(&db, 1, 2, 2, Unwatched); - assert_season!(&db, 1, 2, 3, Unwatched); - assert_season!(&db, 1, 3, 1, Unwatched); - assert_season!(&db, 1, 3, 2, Unwatched); - assert_season!(&db, 1, 3, 3, Unwatched); - - assert_show!(&db, 1, 1, Watched); - assert_show!(&db, 1, 2, Unwatched); - assert_show!(&db, 1, 3, Unwatched); - assert_show!(&db, 2, 1, Unwatched); - assert_show!(&db, 2, 2, Unwatched); - assert_show!(&db, 2, 3, Unwatched); - - // Collection - macro_rules! assert_collection { - ($db:expr, $id:literal, $uid:literal, Watched) => { - assert_collection!(@find, $db, $id, $uid) - .ok_or(()) - .expect("is none"); - }; - ($db:expr, $id:literal, $uid:literal, Unwatched) => { - assert_collection!(@find, $db, $id, $uid) - .ok_or(()) - .expect_err("is some"); - }; - (@find, $db:expr, $id:literal, $uid:literal) => { - super::collections::Entity::find() - .filter( - Condition::all() - .add(super::collections::Column::Id.eq($id)) - .add(super::collections::Column::UserId.eq($uid)), - ) - .one(&db) - .await - .expect("find.filter.one") - }; - } - - have_collection!(&db, 1, 1, None); - have_movie!(&db, 1, 1, Some(1)); - assert_movie!(&db, 1, 1, Success); - assert_movie!(&db, 1, 2, Success); - have_movie!(&db, 1, 2, Some(1)); - assert_movie!(&db, 2, 1, Success); - assert_movie!(&db, 2, 2, Success); - - have_collection!(&db, 1, 2, Some(1)); - have_movie!(&db, 1, 3, Some(2)); - have_show!(&db, 1, 2, Some(2)); - have_season!(&db, 1, 2, 1); - have_episode!(&db, 1, 2, 1, 1); - assert_episode!(&db, 2, 1, 1, 1, Success); - assert_movie!(&db, 3, 1, Success); - have_movie!(&db, 1, 4, Some(2)); - assert_movie!(&db, 4, 1, Success); - - have_collection!(&db, 1, 3, Some(2)); - have_movie!(&db, 1, 5, Some(3)); - assert_movie!(&db, 5, 1, Success); - have_movie!(&db, 1, 6, Some(3)); - assert_movie!(&db, 6, 1, Success); - assert_movie!(&db, 6, 2, Success); - - have_collection!(&db, 1, 4, Some(3)); - have_movie!(&db, 1, 7, Some(4)); - assert_movie!(&db, 7, 1, Success); - assert_movie!(&db, 7, 2, Success); - have_movie!(&db, 1, 8, Some(4)); - assert_movie!(&db, 8, 1, Success); - assert_movie!(&db, 8, 2, Success); - - assert_collection!(&db, 1, 1, Watched); - assert_collection!(&db, 1, 2, Unwatched); - assert_collection!(&db, 2, 1, Watched); - assert_collection!(&db, 2, 2, Unwatched); - assert_collection!(&db, 3, 1, Watched); - assert_collection!(&db, 3, 2, Unwatched); - assert_collection!(&db, 4, 1, Watched); - assert_collection!(&db, 4, 2, Watched); - } -} diff --git a/crates/db/src/entity/watched/movies.rs b/crates/db/src/entity/watched/movies.rs deleted file mode 100644 index caffe55..0000000 --- a/crates/db/src/entity/watched/movies.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Movie entity - -use flix_model::id::{MovieId, RawId}; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, -}; - -/// The database representation of a watched movie -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_watched_movies")] -pub struct Model { - /// The movie's ID - #[sea_orm(primary_key, auto_increment = false)] - pub id: MovieId, - /// The user's ID - #[sea_orm(primary_key, auto_increment = false)] - pub user_id: RawId, - /// The date this movie was watched - pub watched_date: NaiveDate, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} diff --git a/crates/db/src/entity/watched/seasons.rs b/crates/db/src/entity/watched/seasons.rs deleted file mode 100644 index bb62400..0000000 --- a/crates/db/src/entity/watched/seasons.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Episode entity - -use flix_model::id::{RawId, ShowId}; -use flix_model::numbers::SeasonNumber; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, -}; - -/// The database representation of a watched movie -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_watched_seasons")] -pub struct Model { - /// The season's show's ID - #[sea_orm(primary_key, auto_increment = false)] - pub show: ShowId, - /// The season's number - #[sea_orm(primary_key, auto_increment = false)] - pub season: SeasonNumber, - /// The user's ID - #[sea_orm(primary_key, auto_increment = false)] - pub user_id: RawId, - /// The date this season was watched - pub watched_date: NaiveDate, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} diff --git a/crates/db/src/entity/watched/shows.rs b/crates/db/src/entity/watched/shows.rs deleted file mode 100644 index 0f310c4..0000000 --- a/crates/db/src/entity/watched/shows.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Show entity - -use flix_model::id::{RawId, ShowId}; - -use chrono::NaiveDate; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, - EnumIter, PrimaryKeyTrait, -}; - -/// The database representation of a watched movie -#[derive(Debug, Clone, DeriveEntityModel)] -#[sea_orm(table_name = "flix_watched_shows")] -pub struct Model { - /// The show's ID - #[sea_orm(primary_key, auto_increment = false)] - pub id: ShowId, - /// The user's ID - #[sea_orm(primary_key, auto_increment = false)] - pub user_id: RawId, - /// The date this show was watched - pub watched_date: NaiveDate, -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Relation -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} diff --git a/crates/db/src/lib.rs b/crates/db/src/lib.rs index 8a58a81..369c769 100644 --- a/crates/db/src/lib.rs +++ b/crates/db/src/lib.rs @@ -10,8 +10,17 @@ pub mod migration; mod tests { use sea_orm::{ConnectOptions, Database, DatabaseConnection}; - pub async fn new_memory_db() -> DatabaseConnection { - let options = ConnectOptions::new("sqlite:/tmp/db?mode=memory"); - Database::connect(options).await.expect("Database::connect") + use crate::connection::Connection; + + pub async fn new_initialized_memory_db() -> DatabaseConnection { + let options = ConnectOptions::new("sqlite::memory:"); + + let db = Database::connect(options) + .await + .expect("Database::connect()"); + let connection = Connection::try_from(db) + .await + .expect("Connection::try_from"); + connection.take() } } diff --git a/crates/db/src/migration/m_000001.rs b/crates/db/src/migration/m_000001.rs index 38dadee..70cc3ea 100644 --- a/crates/db/src/migration/m_000001.rs +++ b/crates/db/src/migration/m_000001.rs @@ -1,52 +1,33 @@ -//! Adds entity/info tables: +//! Adds watched views: //! - Collections -//! - Movies //! - Shows //! - Seasons -//! - Episodes use sea_orm::{DbErr, DeriveMigrationName}; use sea_orm_migration::async_trait; use sea_orm_migration::{MigrationTrait, SchemaManager}; mod collections; -mod episodes; -mod movies; mod seasons; mod shows; -#[allow(unused_imports)] -pub use collections::FlixInfoCollections; -#[allow(unused_imports)] -pub use episodes::FlixInfoEpisodes; -#[allow(unused_imports)] -pub use movies::FlixInfoMovies; -#[allow(unused_imports)] -pub use seasons::FlixInfoSeasons; -#[allow(unused_imports)] -pub use shows::FlixInfoShows; - #[derive(DeriveMigrationName)] pub(super) struct Migration; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - collections::up(manager).await?; - movies::up(manager).await?; - shows::up(manager).await?; seasons::up(manager).await?; - episodes::up(manager).await?; + shows::up(manager).await?; + collections::up(manager).await?; Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - episodes::down(manager).await?; - seasons::down(manager).await?; - shows::down(manager).await?; - movies::down(manager).await?; collections::down(manager).await?; + shows::down(manager).await?; + seasons::down(manager).await?; Ok(()) } diff --git a/crates/db/src/migration/m_000001/collections.rs b/crates/db/src/migration/m_000001/collections.rs index bbfda26..54e25a4 100644 --- a/crates/db/src/migration/m_000001/collections.rs +++ b/crates/db/src/migration/m_000001/collections.rs @@ -1,39 +1,88 @@ -use seamantic::schema::sqlite_rowid_alias; - -use sea_orm::sea_query; -use sea_orm::sea_query::{Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::string; - -#[derive(Iden)] -pub enum FlixInfoCollections { - Table, - Id, - Title, - Overview, -} +use sea_orm::prelude::*; +use sea_orm::sea_query::Table; +use sea_orm::{ConnectionTrait, DbBackend, Statement}; +use sea_orm_migration::prelude::*; pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { manager - .create_table( - Table::create() - .table(FlixInfoCollections::Table) - .col(sqlite_rowid_alias(FlixInfoCollections::Id)) - .col(string(FlixInfoCollections::Title)) - .col(string(FlixInfoCollections::Overview)) - .to_owned(), - ) + .drop_table(Table::drop().table("flix_watched_collections").to_owned()) .await?; manager - .create_index( - Index::create() - .name("idx-flix_info_collections-title") - .table(FlixInfoCollections::Table) - .col(FlixInfoCollections::Title) - .to_owned(), - ) + .get_connection() + .execute_raw(Statement::from_string( + DbBackend::Sqlite, + r#" + CREATE VIEW flix_watched_collections AS + WITH RECURSIVE + watched_items AS ( + SELECT + w.id, + w.user_id, + w.watched_date, + 'movie' AS type + FROM flix_watched_movies w + + UNION ALL + + SELECT + w.id, + w.user_id, + w.watched_date, + 'show' AS type + FROM flix_watched_shows w + ), + collection_items AS ( + SELECT + m.parent_id, + m.id, + 'movie' AS type + FROM flix_movies m + WHERE m.parent_id IS NOT NULL + + UNION ALL + + SELECT + s.parent_id, + s.id, + 'show' AS type + FROM flix_shows s + WHERE s.parent_id IS NOT NULL + + UNION ALL + + SELECT + c.parent_id, + ci.id, + ci.type + FROM collection_items ci + JOIN flix_collections c + ON c.id = ci.parent_id + ) + SELECT + ci.parent_id AS id, + wi.user_id, + MAX(wi.watched_date) AS watched_date + FROM collection_items ci + JOIN watched_items wi + ON wi.id = ci.id + AND wi.type = ci.type + WHERE NOT EXISTS ( + SELECT 1 + FROM collection_items ci2 + WHERE ci2.parent_id = ci.parent_id + AND NOT EXISTS ( + SELECT 1 + FROM watched_items wi2 + WHERE wi2.id = ci2.id + AND wi2.type = ci2.type + AND wi2.user_id = wi.user_id + ) + ) + GROUP BY ci.parent_id, wi.user_id + ; + "#, + )) .await?; Ok(()) @@ -41,6 +90,15 @@ pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(FlixInfoCollections::Table).to_owned()) - .await + .get_connection() + .execute_raw(Statement::from_string( + DbBackend::Sqlite, + r#" + DROP VIEW flix_watched_collections + ; + "#, + )) + .await?; + + Ok(()) } diff --git a/crates/db/src/migration/m_000001/episodes.rs b/crates/db/src/migration/m_000001/episodes.rs deleted file mode 100644 index 189bbb8..0000000 --- a/crates/db/src/migration/m_000001/episodes.rs +++ /dev/null @@ -1,68 +0,0 @@ -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, integer, string}; - -use crate::migration::m_000001::FlixInfoShows; - -use super::FlixInfoSeasons; - -#[derive(Iden)] -pub enum FlixInfoEpisodes { - Table, - Show, - Season, - Episode, - Title, - Overview, - Date, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixInfoEpisodes::Table) - .col(integer(FlixInfoEpisodes::Show)) - .col(integer(FlixInfoEpisodes::Season)) - .col(integer(FlixInfoEpisodes::Episode)) - .col(string(FlixInfoEpisodes::Title)) - .col(string(FlixInfoEpisodes::Overview)) - .col(date(FlixInfoEpisodes::Date)) - .primary_key( - Index::create() - .col(FlixInfoEpisodes::Show) - .col(FlixInfoEpisodes::Season) - .col(FlixInfoEpisodes::Episode), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_info_episodes-show") - .from_tbl(FlixInfoEpisodes::Table) - .from_col(FlixInfoEpisodes::Show) - .to_tbl(FlixInfoShows::Table) - .to_col(FlixInfoShows::Id), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_info_episodes-show_season") - .from_tbl(FlixInfoEpisodes::Table) - .from_col(FlixInfoEpisodes::Show) - .from_col(FlixInfoEpisodes::Season) - .to_tbl(FlixInfoSeasons::Table) - .to_col(FlixInfoSeasons::Show) - .to_col(FlixInfoSeasons::Season), - ) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixInfoEpisodes::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000001/movies.rs b/crates/db/src/migration/m_000001/movies.rs deleted file mode 100644 index c9812f4..0000000 --- a/crates/db/src/migration/m_000001/movies.rs +++ /dev/null @@ -1,60 +0,0 @@ -use seamantic::schema::sqlite_rowid_alias; - -use sea_orm::sea_query; -use sea_orm::sea_query::{Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, string}; - -#[derive(Iden)] -pub enum FlixInfoMovies { - Table, - Id, - Title, - Tagline, - Overview, - Date, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixInfoMovies::Table) - .col(sqlite_rowid_alias(FlixInfoMovies::Id)) - .col(string(FlixInfoMovies::Title)) - .col(string(FlixInfoMovies::Tagline)) - .col(string(FlixInfoMovies::Overview)) - .col(date(FlixInfoMovies::Date)) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx-flix_info_movies-title") - .table(FlixInfoMovies::Table) - .col(FlixInfoMovies::Title) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx-flix_info_movies-date") - .table(FlixInfoMovies::Table) - .col(FlixInfoMovies::Date) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixInfoMovies::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000001/seasons.rs b/crates/db/src/migration/m_000001/seasons.rs index 1c24af0..046c814 100644 --- a/crates/db/src/migration/m_000001/seasons.rs +++ b/crates/db/src/migration/m_000001/seasons.rs @@ -1,46 +1,43 @@ -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, integer, string}; - -use super::FlixInfoShows; - -#[derive(Iden)] -pub enum FlixInfoSeasons { - Table, - Show, - Season, - Title, - Overview, - Date, -} +use sea_orm::prelude::*; +use sea_orm::sea_query::Table; +use sea_orm::{ConnectionTrait, DbBackend, Statement}; +use sea_orm_migration::prelude::*; pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { manager - .create_table( - Table::create() - .table(FlixInfoSeasons::Table) - .col(integer(FlixInfoSeasons::Show)) - .col(integer(FlixInfoSeasons::Season)) - .col(string(FlixInfoSeasons::Title)) - .col(string(FlixInfoSeasons::Overview)) - .col(date(FlixInfoSeasons::Date)) - .primary_key( - Index::create() - .col(FlixInfoSeasons::Show) - .col(FlixInfoSeasons::Season), + .drop_table(Table::drop().table("flix_watched_seasons").to_owned()) + .await?; + + manager + .get_connection() + .execute_raw(Statement::from_string( + DbBackend::Sqlite, + r#" + CREATE VIEW flix_watched_seasons AS + SELECT + w.show_id, + w.season_number, + w.user_id, + MAX(w.watched_date) AS watched_date + FROM flix_watched_episodes w + WHERE NOT EXISTS ( + SELECT 1 + FROM flix_episodes e + WHERE e.show_id = w.show_id + AND e.season_number = w.season_number + AND NOT EXISTS ( + SELECT 1 + FROM flix_watched_episodes wc + WHERE wc.show_id = e.show_id + AND wc.season_number = e.season_number + AND wc.episode_number = e.episode_number + AND wc.user_id = w.user_id + ) ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_info_seasons-show") - .from_tbl(FlixInfoSeasons::Table) - .from_col(FlixInfoSeasons::Show) - .to_tbl(FlixInfoShows::Table) - .to_col(FlixInfoShows::Id), - ) - .to_owned(), - ) + GROUP BY w.show_id, w.season_number, w.user_id + ; + "#, + )) .await?; Ok(()) @@ -48,6 +45,15 @@ pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(FlixInfoSeasons::Table).to_owned()) - .await + .get_connection() + .execute_raw(Statement::from_string( + DbBackend::Sqlite, + r#" + DROP VIEW flix_watched_seasons + ; + "#, + )) + .await?; + + Ok(()) } diff --git a/crates/db/src/migration/m_000001/shows.rs b/crates/db/src/migration/m_000001/shows.rs index 73df566..b1fcd7f 100644 --- a/crates/db/src/migration/m_000001/shows.rs +++ b/crates/db/src/migration/m_000001/shows.rs @@ -1,53 +1,40 @@ -use seamantic::schema::sqlite_rowid_alias; - -use sea_orm::sea_query; -use sea_orm::sea_query::{Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, string}; - -#[derive(Iden)] -pub enum FlixInfoShows { - Table, - Id, - Title, - Tagline, - Overview, - Date, -} +use sea_orm::prelude::*; +use sea_orm::sea_query::Table; +use sea_orm::{ConnectionTrait, DbBackend, Statement}; +use sea_orm_migration::prelude::*; pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { manager - .create_table( - Table::create() - .table(FlixInfoShows::Table) - .col(sqlite_rowid_alias(FlixInfoShows::Id)) - .col(string(FlixInfoShows::Title)) - .col(string(FlixInfoShows::Tagline)) - .col(string(FlixInfoShows::Overview)) - .col(date(FlixInfoShows::Date)) - .to_owned(), - ) + .drop_table(Table::drop().table("flix_watched_shows").to_owned()) .await?; manager - .create_index( - Index::create() - .name("idx-flix_info_shows-title") - .table(FlixInfoShows::Table) - .col(FlixInfoShows::Title) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx-flix_info_shows-date") - .table(FlixInfoShows::Table) - .col(FlixInfoShows::Date) - .to_owned(), - ) + .get_connection() + .execute_raw(Statement::from_string( + DbBackend::Sqlite, + r#" + CREATE VIEW flix_watched_shows AS + SELECT + w.show_id as id, + w.user_id, + MAX(w.watched_date) AS watched_date + FROM flix_watched_seasons w + WHERE NOT EXISTS ( + SELECT 1 + FROM flix_seasons s + WHERE s.show_id = w.show_id + AND NOT EXISTS ( + SELECT 1 + FROM flix_watched_seasons wc + WHERE wc.show_id = s.show_id + AND wc.season_number = s.season_number + AND wc.user_id = w.user_id + ) + ) + GROUP BY w.show_id, w.user_id + ; + "#, + )) .await?; Ok(()) @@ -55,6 +42,15 @@ pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(FlixInfoShows::Table).to_owned()) - .await + .get_connection() + .execute_raw(Statement::from_string( + DbBackend::Sqlite, + r#" + DROP VIEW flix_watched_shows + ; + "#, + )) + .await?; + + Ok(()) } diff --git a/crates/db/src/migration/m_000002.rs b/crates/db/src/migration/m_000002.rs deleted file mode 100644 index 9d6c532..0000000 --- a/crates/db/src/migration/m_000002.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Adds entity/tmdb tables: -//! - Collections -//! - Movies -//! - Shows -//! - Seasons -//! - Episodes - -use sea_orm::{DbErr, DeriveMigrationName}; -use sea_orm_migration::async_trait; -use sea_orm_migration::{MigrationTrait, SchemaManager}; - -mod collections; -mod episodes; -mod movies; -mod seasons; -mod shows; - -#[allow(unused_imports)] -pub use collections::FlixTmdbCollections; -#[allow(unused_imports)] -pub use episodes::FlixTmdbEpisodes; -#[allow(unused_imports)] -pub use movies::FlixTmdbMovies; -#[allow(unused_imports)] -pub use seasons::FlixTmdbSeasons; -#[allow(unused_imports)] -pub use shows::FlixTmdbShows; - -#[derive(DeriveMigrationName)] -pub(super) struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - collections::up(manager).await?; - movies::up(manager).await?; - shows::up(manager).await?; - seasons::up(manager).await?; - episodes::up(manager).await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - episodes::down(manager).await?; - seasons::down(manager).await?; - shows::down(manager).await?; - movies::down(manager).await?; - collections::down(manager).await?; - - Ok(()) - } -} diff --git a/crates/db/src/migration/m_000002/collections.rs b/crates/db/src/migration/m_000002/collections.rs deleted file mode 100644 index 9a18a25..0000000 --- a/crates/db/src/migration/m_000002/collections.rs +++ /dev/null @@ -1,58 +0,0 @@ -use seamantic::schema::sqlite_rowid_alias; - -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, integer}; - -use super::super::m_000001::FlixInfoCollections; - -#[derive(Iden)] -pub enum FlixTmdbCollections { - Table, - TmdbId, - FlixId, - LastUpdate, - MovieCount, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixTmdbCollections::Table) - .col(sqlite_rowid_alias(FlixTmdbCollections::TmdbId)) - .col(integer(FlixTmdbCollections::FlixId).unique_key()) - .col(date(FlixTmdbCollections::LastUpdate)) - .col(integer(FlixTmdbCollections::MovieCount)) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_tmdb_collections-flixid") - .from_tbl(FlixTmdbCollections::Table) - .from_col(FlixTmdbCollections::FlixId) - .to_tbl(FlixInfoCollections::Table) - .to_col(FlixInfoCollections::Id), - ) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx-flix_tmdb_collections-flixid") - .table(FlixTmdbCollections::Table) - .col(FlixTmdbCollections::FlixId) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixTmdbCollections::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000002/episodes.rs b/crates/db/src/migration/m_000002/episodes.rs deleted file mode 100644 index a182087..0000000 --- a/crates/db/src/migration/m_000002/episodes.rs +++ /dev/null @@ -1,94 +0,0 @@ -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, integer}; - -use super::super::m_000001::{FlixInfoEpisodes, FlixInfoSeasons}; - -use super::FlixTmdbSeasons; - -#[derive(Iden)] -pub enum FlixTmdbEpisodes { - Table, - TmdbShow, - TmdbSeason, - TmdbEpisode, - FlixShow, - FlixSeason, - FlixEpisode, - LastUpdate, - Runtime, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixTmdbEpisodes::Table) - .col(integer(FlixTmdbEpisodes::TmdbShow)) - .col(integer(FlixTmdbEpisodes::TmdbSeason)) - .col(integer(FlixTmdbEpisodes::TmdbEpisode)) - .col(integer(FlixTmdbEpisodes::FlixShow)) - .col(integer(FlixTmdbEpisodes::FlixSeason)) - .col(integer(FlixTmdbEpisodes::FlixEpisode)) - .col(date(FlixTmdbEpisodes::LastUpdate)) - .col(integer(FlixTmdbEpisodes::Runtime)) - .primary_key( - Index::create() - .col(FlixTmdbEpisodes::TmdbShow) - .col(FlixTmdbEpisodes::TmdbSeason) - .col(FlixTmdbEpisodes::TmdbEpisode), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_tmdb_episodes-tmdb_show_season") - .from_tbl(FlixTmdbEpisodes::Table) - .from_col(FlixTmdbEpisodes::TmdbShow) - .from_col(FlixTmdbEpisodes::TmdbSeason) - .to_tbl(FlixTmdbSeasons::Table) - .to_col(FlixTmdbSeasons::TmdbShow) - .to_col(FlixTmdbSeasons::TmdbSeason), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_tmdb_episodes-flix_show_season") - .from_tbl(FlixTmdbEpisodes::Table) - .from_col(FlixTmdbEpisodes::FlixShow) - .from_col(FlixTmdbEpisodes::FlixSeason) - .to_tbl(FlixInfoSeasons::Table) - .to_col(FlixInfoSeasons::Show) - .to_col(FlixInfoSeasons::Season), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_tmdb_episodes-flix_show_season_episode") - .from_tbl(FlixTmdbEpisodes::Table) - .from_col(FlixTmdbEpisodes::FlixShow) - .from_col(FlixTmdbEpisodes::FlixSeason) - .from_col(FlixTmdbEpisodes::FlixEpisode) - .to_tbl(FlixInfoEpisodes::Table) - .to_col(FlixInfoEpisodes::Show) - .to_col(FlixInfoEpisodes::Season) - .to_col(FlixInfoEpisodes::Episode), - ) - .index( - Index::create() - .unique() - .name("idx-flix_tmdb_episodes-flix_show_season_episode") - .col(FlixTmdbEpisodes::FlixShow) - .col(FlixTmdbEpisodes::FlixSeason) - .col(FlixTmdbEpisodes::FlixEpisode), - ) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixTmdbEpisodes::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000002/movies.rs b/crates/db/src/migration/m_000002/movies.rs deleted file mode 100644 index ccb2d70..0000000 --- a/crates/db/src/migration/m_000002/movies.rs +++ /dev/null @@ -1,70 +0,0 @@ -use seamantic::schema::sqlite_rowid_alias; - -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, integer, integer_null}; - -use crate::migration::m_000002::FlixTmdbCollections; - -use super::super::m_000001::FlixInfoMovies; - -#[derive(Iden)] -pub enum FlixTmdbMovies { - Table, - TmdbId, - FlixId, - LastUpdate, - Runtime, - Collection, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixTmdbMovies::Table) - .col(sqlite_rowid_alias(FlixTmdbMovies::TmdbId)) - .col(integer(FlixTmdbMovies::FlixId).unique_key()) - .col(date(FlixTmdbMovies::LastUpdate)) - .col(integer(FlixTmdbMovies::Runtime)) - .col(integer_null(FlixTmdbMovies::Collection)) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_tmdb_movies-flixid") - .from_tbl(FlixTmdbMovies::Table) - .from_col(FlixTmdbMovies::FlixId) - .to_tbl(FlixInfoMovies::Table) - .to_col(FlixInfoMovies::Id), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_tmdb_movies-collectionid") - .from_tbl(FlixTmdbMovies::Table) - .from_col(FlixTmdbMovies::Collection) - .to_tbl(FlixTmdbCollections::Table) - .to_col(FlixTmdbCollections::TmdbId), - ) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx-flix_tmdb_movies-flixid") - .table(FlixTmdbMovies::Table) - .col(FlixTmdbMovies::FlixId) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixTmdbMovies::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000002/seasons.rs b/crates/db/src/migration/m_000002/seasons.rs deleted file mode 100644 index b1b8c34..0000000 --- a/crates/db/src/migration/m_000002/seasons.rs +++ /dev/null @@ -1,82 +0,0 @@ -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, integer}; - -use crate::migration::m_000001::FlixInfoShows; - -use super::super::m_000001::FlixInfoSeasons; - -use super::FlixTmdbShows; - -#[derive(Iden)] -pub enum FlixTmdbSeasons { - Table, - TmdbShow, - TmdbSeason, - FlixShow, - FlixSeason, - LastUpdate, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixTmdbSeasons::Table) - .col(integer(FlixTmdbSeasons::TmdbShow)) - .col(integer(FlixTmdbSeasons::TmdbSeason)) - .col(integer(FlixTmdbSeasons::FlixShow)) - .col(integer(FlixTmdbSeasons::FlixSeason)) - .col(date(FlixTmdbSeasons::LastUpdate)) - .primary_key( - Index::create() - .col(FlixTmdbSeasons::TmdbShow) - .col(FlixTmdbSeasons::TmdbSeason), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_tmdb_seasons-tmdb_show") - .from_tbl(FlixTmdbSeasons::Table) - .from_col(FlixTmdbSeasons::FlixShow) - .to_tbl(FlixTmdbShows::Table) - .to_col(FlixTmdbShows::FlixId), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_tmdb_seasons-flix_show") - .from_tbl(FlixTmdbSeasons::Table) - .from_col(FlixTmdbSeasons::FlixShow) - .to_tbl(FlixInfoShows::Table) - .to_col(FlixInfoShows::Id), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_tmdb_seasons-flix_show_season") - .from_tbl(FlixTmdbSeasons::Table) - .from_col(FlixTmdbSeasons::FlixShow) - .from_col(FlixTmdbSeasons::FlixSeason) - .to_tbl(FlixInfoSeasons::Table) - .to_col(FlixInfoSeasons::Show) - .to_col(FlixInfoSeasons::Season), - ) - .index( - Index::create() - .unique() - .name("idx-flix_tmdb_seasons-flix_show_season") - .col(FlixTmdbSeasons::FlixShow) - .col(FlixTmdbSeasons::FlixSeason), - ) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixTmdbSeasons::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000002/shows.rs b/crates/db/src/migration/m_000002/shows.rs deleted file mode 100644 index 9df839c..0000000 --- a/crates/db/src/migration/m_000002/shows.rs +++ /dev/null @@ -1,58 +0,0 @@ -use seamantic::schema::sqlite_rowid_alias; - -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, integer}; - -use super::super::m_000001::FlixInfoShows; - -#[derive(Iden)] -pub enum FlixTmdbShows { - Table, - TmdbId, - FlixId, - LastUpdate, - NumberOfSeasons, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixTmdbShows::Table) - .col(sqlite_rowid_alias(FlixTmdbShows::TmdbId)) - .col(integer(FlixTmdbShows::FlixId).unique_key()) - .col(date(FlixTmdbShows::LastUpdate)) - .col(integer(FlixTmdbShows::NumberOfSeasons)) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_tmdb_shows-flixid") - .from_tbl(FlixTmdbShows::Table) - .from_col(FlixTmdbShows::FlixId) - .to_tbl(FlixInfoShows::Table) - .to_col(FlixInfoShows::Id), - ) - .to_owned(), - ) - .await?; - - manager - .create_index( - Index::create() - .name("idx-flix_tmdb_shows-flixid") - .table(FlixTmdbShows::Table) - .col(FlixTmdbShows::FlixId) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixTmdbShows::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000003.rs b/crates/db/src/migration/m_000003.rs deleted file mode 100644 index 4fd3e6b..0000000 --- a/crates/db/src/migration/m_000003.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Adds entity/content tables: -//! - Libraries -//! - Collections -//! - Movies -//! - Shows -//! - Seasons -//! - Episodes - -use sea_orm::{DbErr, DeriveMigrationName}; -use sea_orm_migration::async_trait; -use sea_orm_migration::{MigrationTrait, SchemaManager}; - -mod collections; -mod episodes; -mod libraries; -mod movies; -mod seasons; -mod shows; - -#[allow(unused_imports)] -pub use collections::FlixCollections; -#[allow(unused_imports)] -pub use episodes::FlixEpisodes; -#[allow(unused_imports)] -pub use libraries::FlixLibraries; -#[allow(unused_imports)] -pub use movies::FlixMovies; -#[allow(unused_imports)] -pub use seasons::FlixSeasons; -#[allow(unused_imports)] -pub use shows::FlixShows; - -#[derive(DeriveMigrationName)] -pub(super) struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - libraries::up(manager).await?; - collections::up(manager).await?; - movies::up(manager).await?; - shows::up(manager).await?; - seasons::up(manager).await?; - episodes::up(manager).await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - episodes::down(manager).await?; - seasons::down(manager).await?; - shows::down(manager).await?; - movies::down(manager).await?; - collections::down(manager).await?; - libraries::down(manager).await?; - - Ok(()) - } -} diff --git a/crates/db/src/migration/m_000003/collections.rs b/crates/db/src/migration/m_000003/collections.rs deleted file mode 100644 index 3d41eba..0000000 --- a/crates/db/src/migration/m_000003/collections.rs +++ /dev/null @@ -1,69 +0,0 @@ -use seamantic::schema::sqlite_rowid_alias; - -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{binary, integer, integer_null, string, string_null}; - -use crate::migration::m_000001::FlixInfoCollections; -use crate::migration::m_000003::FlixLibraries; - -#[derive(Iden)] -pub enum FlixCollections { - Table, - Id, - Parent, - Slug, - Library, - Directory, - RelativePosterPath, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixCollections::Table) - .col(sqlite_rowid_alias(FlixCollections::Id)) - .col(integer_null(FlixCollections::Parent)) - .col(string(FlixCollections::Slug)) - .col(integer(FlixCollections::Library)) - .col(binary(FlixCollections::Directory)) - .col(string_null(FlixCollections::RelativePosterPath)) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_collections-id") - .from_tbl(FlixCollections::Table) - .from_col(FlixCollections::Id) - .to_tbl(FlixInfoCollections::Table) - .to_col(FlixInfoCollections::Id), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_collections-parent") - .from_tbl(FlixCollections::Table) - .from_col(FlixCollections::Parent) - .to_tbl(FlixCollections::Table) - .to_col(FlixCollections::Id), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_collections-library") - .from_tbl(FlixCollections::Table) - .from_col(FlixCollections::Library) - .to_tbl(FlixLibraries::Table) - .to_col(FlixLibraries::Id), - ) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixCollections::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000003/episodes.rs b/crates/db/src/migration/m_000003/episodes.rs deleted file mode 100644 index 4385021..0000000 --- a/crates/db/src/migration/m_000003/episodes.rs +++ /dev/null @@ -1,75 +0,0 @@ -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{binary, integer, string, string_null}; - -use crate::migration::m_000001::FlixInfoEpisodes; -use crate::migration::m_000003::FlixLibraries; - -#[derive(Iden)] -pub enum FlixEpisodes { - Table, - Show, - Season, - Episode, - Count, - Slug, - Library, - Directory, - RelativeMediaPath, - RelativePosterPath, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixEpisodes::Table) - .col(integer(FlixEpisodes::Show)) - .col(integer(FlixEpisodes::Season)) - .col(integer(FlixEpisodes::Episode)) - .col(integer(FlixEpisodes::Count)) - .col(string(FlixEpisodes::Slug)) - .col(integer(FlixEpisodes::Library)) - .col(binary(FlixEpisodes::Directory)) - .col(string(FlixEpisodes::RelativeMediaPath)) - .col(string_null(FlixEpisodes::RelativePosterPath)) - .primary_key( - Index::create() - .col(FlixEpisodes::Show) - .col(FlixEpisodes::Season) - .col(FlixEpisodes::Episode), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_episodes-show_season_episode") - .from_tbl(FlixEpisodes::Table) - .from_col(FlixEpisodes::Show) - .from_col(FlixEpisodes::Season) - .from_col(FlixEpisodes::Episode) - .to_tbl(FlixInfoEpisodes::Table) - .to_col(FlixInfoEpisodes::Show) - .to_col(FlixInfoEpisodes::Season) - .to_col(FlixInfoEpisodes::Episode), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_episodes-library") - .from_tbl(FlixEpisodes::Table) - .from_col(FlixEpisodes::Library) - .to_tbl(FlixLibraries::Table) - .to_col(FlixLibraries::Id), - ) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixEpisodes::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000003/libraries.rs b/crates/db/src/migration/m_000003/libraries.rs deleted file mode 100644 index 6864e5e..0000000 --- a/crates/db/src/migration/m_000003/libraries.rs +++ /dev/null @@ -1,34 +0,0 @@ -use seamantic::schema::sqlite_rowid_alias; - -use sea_orm::sea_query; -use sea_orm::sea_query::Table; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::binary; - -#[derive(Iden)] -pub enum FlixLibraries { - Table, - Id, - Directory, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixLibraries::Table) - .col(sqlite_rowid_alias(FlixLibraries::Id)) - .col(binary(FlixLibraries::Directory)) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixLibraries::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000003/movies.rs b/crates/db/src/migration/m_000003/movies.rs deleted file mode 100644 index 5523c9d..0000000 --- a/crates/db/src/migration/m_000003/movies.rs +++ /dev/null @@ -1,71 +0,0 @@ -use seamantic::schema::sqlite_rowid_alias; - -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{binary, integer, integer_null, string, string_null}; - -use crate::migration::m_000001::FlixInfoMovies; -use crate::migration::m_000003::{FlixCollections, FlixLibraries}; - -#[derive(Iden)] -pub enum FlixMovies { - Table, - Id, - Parent, - Slug, - Library, - Directory, - RelativeMediaPath, - RelativePosterPath, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixMovies::Table) - .col(sqlite_rowid_alias(FlixMovies::Id)) - .col(integer_null(FlixMovies::Parent)) - .col(string(FlixMovies::Slug)) - .col(integer(FlixMovies::Library)) - .col(binary(FlixMovies::Directory)) - .col(string(FlixMovies::RelativeMediaPath)) - .col(string_null(FlixMovies::RelativePosterPath)) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_movies-id") - .from_tbl(FlixMovies::Table) - .from_col(FlixMovies::Id) - .to_tbl(FlixInfoMovies::Table) - .to_col(FlixInfoMovies::Id), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_movies-parent") - .from_tbl(FlixMovies::Table) - .from_col(FlixMovies::Parent) - .to_tbl(FlixCollections::Table) - .to_col(FlixCollections::Id), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_movies-library") - .from_tbl(FlixMovies::Table) - .from_col(FlixMovies::Library) - .to_tbl(FlixLibraries::Table) - .to_col(FlixLibraries::Id), - ) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixMovies::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000003/seasons.rs b/crates/db/src/migration/m_000003/seasons.rs deleted file mode 100644 index d29ca1b..0000000 --- a/crates/db/src/migration/m_000003/seasons.rs +++ /dev/null @@ -1,66 +0,0 @@ -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{binary, integer, string, string_null}; - -use crate::migration::m_000001::FlixInfoSeasons; -use crate::migration::m_000003::FlixLibraries; - -#[derive(Iden)] -pub enum FlixSeasons { - Table, - Show, - Season, - Slug, - Library, - Directory, - RelativePosterPath, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixSeasons::Table) - .col(integer(FlixSeasons::Show)) - .col(integer(FlixSeasons::Season)) - .col(string(FlixSeasons::Slug)) - .col(integer(FlixSeasons::Library)) - .col(binary(FlixSeasons::Directory)) - .col(string_null(FlixSeasons::RelativePosterPath)) - .primary_key( - Index::create() - .col(FlixSeasons::Show) - .col(FlixSeasons::Season), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_seasons-show_season") - .from_tbl(FlixSeasons::Table) - .from_col(FlixSeasons::Show) - .from_col(FlixSeasons::Season) - .to_tbl(FlixInfoSeasons::Table) - .to_col(FlixInfoSeasons::Show) - .to_col(FlixInfoSeasons::Season), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_seasons-library") - .from_tbl(FlixSeasons::Table) - .from_col(FlixSeasons::Library) - .to_tbl(FlixLibraries::Table) - .to_col(FlixLibraries::Id), - ) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixSeasons::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000003/shows.rs b/crates/db/src/migration/m_000003/shows.rs deleted file mode 100644 index 8095515..0000000 --- a/crates/db/src/migration/m_000003/shows.rs +++ /dev/null @@ -1,69 +0,0 @@ -use seamantic::schema::sqlite_rowid_alias; - -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{binary, integer, integer_null, string, string_null}; - -use crate::migration::m_000001::FlixInfoShows; -use crate::migration::m_000003::{FlixCollections, FlixLibraries}; - -#[derive(Iden)] -pub enum FlixShows { - Table, - Id, - Parent, - Slug, - Library, - Directory, - RelativePosterPath, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixShows::Table) - .col(sqlite_rowid_alias(FlixShows::Id)) - .col(integer_null(FlixShows::Parent)) - .col(string(FlixShows::Slug)) - .col(integer(FlixShows::Library)) - .col(binary(FlixShows::Directory)) - .col(string_null(FlixShows::RelativePosterPath)) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_shows-id") - .from_tbl(FlixShows::Table) - .from_col(FlixShows::Id) - .to_tbl(FlixInfoShows::Table) - .to_col(FlixInfoShows::Id), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_shows-parent") - .from_tbl(FlixShows::Table) - .from_col(FlixShows::Parent) - .to_tbl(FlixCollections::Table) - .to_col(FlixCollections::Id), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_shows-library") - .from_tbl(FlixShows::Table) - .from_col(FlixShows::Library) - .to_tbl(FlixLibraries::Table) - .to_col(FlixLibraries::Id), - ) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixShows::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000004.rs b/crates/db/src/migration/m_000004.rs deleted file mode 100644 index db96196..0000000 --- a/crates/db/src/migration/m_000004.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Adds entity/watched tables: -//! - Collections -//! - Movies -//! - Shows -//! - Seasons -//! - Episodes - -use sea_orm::{DbErr, DeriveMigrationName}; -use sea_orm_migration::async_trait; -use sea_orm_migration::{MigrationTrait, SchemaManager}; - -mod collections; -mod episodes; -mod movies; -mod seasons; -mod shows; - -#[allow(unused_imports)] -pub use episodes::FlixWatchedEpisodes; -#[allow(unused_imports)] -pub use movies::FlixWatchedMovies; - -#[derive(DeriveMigrationName)] -pub(super) struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - episodes::up(manager).await?; - seasons::up(manager).await?; - shows::up(manager).await?; - movies::up(manager).await?; - collections::up(manager).await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - collections::down(manager).await?; - movies::down(manager).await?; - shows::down(manager).await?; - seasons::down(manager).await?; - episodes::down(manager).await?; - - Ok(()) - } -} diff --git a/crates/db/src/migration/m_000004/collections.rs b/crates/db/src/migration/m_000004/collections.rs deleted file mode 100644 index 5b0ab16..0000000 --- a/crates/db/src/migration/m_000004/collections.rs +++ /dev/null @@ -1,98 +0,0 @@ -use sea_orm::DbErr; -use sea_orm::{ConnectionTrait, DbBackend, Statement}; -use sea_orm_migration::SchemaManager; - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .get_connection() - .execute_raw(Statement::from_string( - DbBackend::Sqlite, - r#" - CREATE VIEW flix_watched_collections AS - WITH RECURSIVE - watched_items AS ( - SELECT - w.id, - w.user_id, - w.watched_date, - 'movie' AS type - FROM flix_watched_movies w - - UNION ALL - - SELECT - w.id, - w.user_id, - w.watched_date, - 'show' AS type - FROM flix_watched_shows w - ), - collection_items AS ( - SELECT - m.parent, - m.id, - 'movie' AS type - FROM flix_movies m - WHERE m.parent IS NOT NULL - - UNION ALL - - SELECT - s.parent, - s.id, - 'show' AS type - FROM flix_shows s - WHERE s.parent IS NOT NULL - - UNION ALL - - SELECT - c.parent, - ci.id, - ci.type - FROM collection_items ci - JOIN flix_collections c - ON c.id = ci.parent - ) - SELECT - ci.parent AS id, - wi.user_id, - MAX(wi.watched_date) AS watched_date - FROM collection_items ci - JOIN watched_items wi - ON wi.id = ci.id - AND wi.type = ci.type - WHERE NOT EXISTS ( - SELECT 1 - FROM collection_items ci2 - WHERE ci2.parent = ci.parent - AND NOT EXISTS ( - SELECT 1 - FROM watched_items wi2 - WHERE wi2.id = ci2.id - AND wi2.type = ci2.type - AND wi2.user_id = wi.user_id - ) - ) - GROUP BY ci.parent, wi.user_id; - "#, - )) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .get_connection() - .execute_raw(Statement::from_string( - DbBackend::Sqlite, - r#" - DROP VIEW flix_watched_collections - ; - "#, - )) - .await?; - - Ok(()) -} diff --git a/crates/db/src/migration/m_000004/episodes.rs b/crates/db/src/migration/m_000004/episodes.rs deleted file mode 100644 index 2ce3dd9..0000000 --- a/crates/db/src/migration/m_000004/episodes.rs +++ /dev/null @@ -1,60 +0,0 @@ -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, integer}; - -use crate::migration::m_000001::FlixInfoEpisodes; - -#[derive(Iden)] -pub enum FlixWatchedEpisodes { - Table, - Show, - Season, - Episode, - UserId, - WatchedDate, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixWatchedEpisodes::Table) - .col(integer(FlixWatchedEpisodes::Show)) - .col(integer(FlixWatchedEpisodes::Season)) - .col(integer(FlixWatchedEpisodes::Episode)) - .col(integer(FlixWatchedEpisodes::UserId)) - .col(date(FlixWatchedEpisodes::WatchedDate)) - .primary_key( - Index::create() - .col(FlixWatchedEpisodes::Show) - .col(FlixWatchedEpisodes::Season) - .col(FlixWatchedEpisodes::Episode) - .col(FlixWatchedEpisodes::UserId), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_watched_episodes-show_season_episode") - .from_tbl(FlixWatchedEpisodes::Table) - .from_tbl(FlixWatchedEpisodes::Table) - .from_col(FlixWatchedEpisodes::Show) - .from_col(FlixWatchedEpisodes::Season) - .from_col(FlixWatchedEpisodes::Episode) - .to_tbl(FlixInfoEpisodes::Table) - .to_col(FlixInfoEpisodes::Show) - .to_col(FlixInfoEpisodes::Season) - .to_col(FlixInfoEpisodes::Episode), - ) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixWatchedEpisodes::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000004/movies.rs b/crates/db/src/migration/m_000004/movies.rs deleted file mode 100644 index 6f4175e..0000000 --- a/crates/db/src/migration/m_000004/movies.rs +++ /dev/null @@ -1,49 +0,0 @@ -use sea_orm::sea_query; -use sea_orm::sea_query::{ForeignKeyCreateStatement, Index, Table}; -use sea_orm::{DbErr, Iden}; -use sea_orm_migration::SchemaManager; -use sea_orm_migration::schema::{date, integer}; - -use crate::migration::m_000001::FlixInfoMovies; - -#[derive(Iden)] -pub enum FlixWatchedMovies { - Table, - Id, - UserId, - WatchedDate, -} - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(FlixWatchedMovies::Table) - .col(integer(FlixWatchedMovies::Id)) - .col(integer(FlixWatchedMovies::UserId)) - .col(date(FlixWatchedMovies::WatchedDate)) - .primary_key( - Index::create() - .col(FlixWatchedMovies::Id) - .col(FlixWatchedMovies::UserId), - ) - .foreign_key( - ForeignKeyCreateStatement::new() - .name("fk-flix_watched_movies-id") - .from_tbl(FlixWatchedMovies::Table) - .from_col(FlixWatchedMovies::Id) - .to_tbl(FlixInfoMovies::Table) - .to_col(FlixInfoMovies::Id), - ) - .to_owned(), - ) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(FlixWatchedMovies::Table).to_owned()) - .await -} diff --git a/crates/db/src/migration/m_000004/seasons.rs b/crates/db/src/migration/m_000004/seasons.rs deleted file mode 100644 index 08bb07f..0000000 --- a/crates/db/src/migration/m_000004/seasons.rs +++ /dev/null @@ -1,54 +0,0 @@ -use sea_orm::DbErr; -use sea_orm::{ConnectionTrait, DbBackend, Statement}; -use sea_orm_migration::SchemaManager; - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .get_connection() - .execute_raw(Statement::from_string( - DbBackend::Sqlite, - r#" - CREATE VIEW flix_watched_seasons AS - SELECT - w.show, - w.season, - w.user_id, - MAX(w.watched_date) AS watched_date - FROM flix_watched_episodes w - WHERE NOT EXISTS ( - SELECT 1 - FROM flix_episodes e - WHERE e.show = w.show - AND e.season = w.season - AND NOT EXISTS ( - SELECT 1 - FROM flix_watched_episodes wc - WHERE wc.show = e.show - AND wc.season = e.season - AND wc.episode = e.episode - AND wc.user_id = w.user_id - ) - ) - GROUP BY w.show, w.season, w.user_id - ; - "#, - )) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .get_connection() - .execute_raw(Statement::from_string( - DbBackend::Sqlite, - r#" - DROP VIEW flix_watched_seasons - ; - "#, - )) - .await?; - - Ok(()) -} diff --git a/crates/db/src/migration/m_000004/shows.rs b/crates/db/src/migration/m_000004/shows.rs deleted file mode 100644 index 08afa89..0000000 --- a/crates/db/src/migration/m_000004/shows.rs +++ /dev/null @@ -1,51 +0,0 @@ -use sea_orm::DbErr; -use sea_orm::{ConnectionTrait, DbBackend, Statement}; -use sea_orm_migration::SchemaManager; - -pub async fn up(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .get_connection() - .execute_raw(Statement::from_string( - DbBackend::Sqlite, - r#" - CREATE VIEW flix_watched_shows AS - SELECT - w.show as id, - w.user_id, - MAX(w.watched_date) AS watched_date - FROM flix_watched_seasons w - WHERE NOT EXISTS ( - SELECT 1 - FROM flix_seasons s - WHERE s.show = w.show - AND NOT EXISTS ( - SELECT 1 - FROM flix_watched_seasons wc - WHERE wc.show = s.show - AND wc.season = s.season - AND wc.user_id = w.user_id - ) - ) - GROUP BY w.show, w.user_id - ; - "#, - )) - .await?; - - Ok(()) -} - -pub async fn down(manager: &SchemaManager<'_>) -> Result<(), DbErr> { - manager - .get_connection() - .execute_raw(Statement::from_string( - DbBackend::Sqlite, - r#" - DROP VIEW flix_watched_shows - ; - "#, - )) - .await?; - - Ok(()) -} diff --git a/crates/db/src/migration/mod.rs b/crates/db/src/migration/mod.rs index f4e50d7..f0221d7 100644 --- a/crates/db/src/migration/mod.rs +++ b/crates/db/src/migration/mod.rs @@ -3,7 +3,4 @@ seamantic::migrations! { "seaql_migrations_flix"; m_000001, - m_000002, - m_000003, - m_000004, } diff --git a/crates/flix/Cargo.toml b/crates/flix/Cargo.toml index e005657..949e418 100644 --- a/crates/flix/Cargo.toml +++ b/crates/flix/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flix" -version = "0.0.12" +version = "0.0.13" categories = [] description = "Mechanisms for interacting with flix media" diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 692fc0a..34fb873 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flix-fs" -version = "0.0.12" +version = "0.0.13" categories = [] description = "Filesystem scanner for flix media" diff --git a/crates/model/Cargo.toml b/crates/model/Cargo.toml index 0e8ba50..0e2568e 100644 --- a/crates/model/Cargo.toml +++ b/crates/model/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flix-model" -version = "0.0.12" +version = "0.0.13" categories = [] description = "Core types for flix data" diff --git a/crates/tmdb/Cargo.toml b/crates/tmdb/Cargo.toml index 35ee781..90d8149 100644 --- a/crates/tmdb/Cargo.toml +++ b/crates/tmdb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flix-tmdb" -version = "0.0.12" +version = "0.0.13" categories = [] description = "Clients and models for fetching data from TMDB"