flix_db/entity/
tmdb.rs

1//! This module contains entities for storing dynamic data from TMDB
2
3/// Collection entity
4pub mod collections {
5	use flix_model::id::CollectionId as FlixId;
6	use flix_tmdb::model::id::CollectionId;
7
8	use chrono::{DateTime, Utc};
9	use sea_orm::entity::prelude::*;
10
11	use crate::entity;
12
13	/// The database representation of a tmdb collection
14	#[sea_orm::model]
15	#[derive(Debug, Clone, DeriveEntityModel)]
16	#[sea_orm(table_name = "flix_tmdb_collections")]
17	pub struct Model {
18		/// The collection's TMDB ID
19		#[sea_orm(primary_key, auto_increment = false)]
20		pub tmdb_id: CollectionId,
21		/// The collection's ID
22		#[sea_orm(unique)]
23		pub flix_id: FlixId,
24		/// The date of the last update
25		pub last_update: DateTime<Utc>,
26		/// The number of movies in the collection
27		pub movie_count: u16,
28
29		/// The info for this collection
30		#[sea_orm(
31			belongs_to,
32			from = "flix_id",
33			to = "id",
34			on_update = "Cascade",
35			on_delete = "Cascade"
36		)]
37		pub info: HasOne<entity::info::collections::Entity>,
38
39		/// Movies that are in this collection
40		#[sea_orm(has_many)]
41		pub movies: HasMany<super::movies::Entity>,
42	}
43
44	impl ActiveModelBehavior for ActiveModel {}
45}
46
47/// Movie entity
48pub mod movies {
49	use flix_model::id::MovieId as FlixId;
50	use flix_tmdb::model::id::{CollectionId, MovieId};
51
52	use seamantic::model::duration::Seconds;
53
54	use chrono::{DateTime, Utc};
55	use sea_orm::entity::prelude::*;
56
57	use crate::entity;
58
59	/// The database representation of a tmdb movie
60	#[sea_orm::model]
61	#[derive(Debug, Clone, DeriveEntityModel)]
62	#[sea_orm(table_name = "flix_tmdb_movies")]
63	pub struct Model {
64		/// The movie's TMDB ID
65		#[sea_orm(primary_key, auto_increment = false)]
66		pub tmdb_id: MovieId,
67		/// The movie's ID
68		#[sea_orm(unique)]
69		pub flix_id: FlixId,
70		/// The date of the last update
71		pub last_update: DateTime<Utc>,
72		/// The movie's runtime in seconds
73		pub runtime: Seconds,
74		/// The TMDB ID of the collection this movie belongs to
75		#[sea_orm(indexed)]
76		pub collection_id: Option<CollectionId>,
77
78		/// The collection this movie belongs to
79		#[sea_orm(
80			belongs_to,
81			from = "collection_id",
82			to = "tmdb_id",
83			on_update = "Cascade",
84			on_delete = "Cascade"
85		)]
86		pub collection: HasOne<super::collections::Entity>,
87		/// The info for this movie
88		#[sea_orm(
89			belongs_to,
90			from = "flix_id",
91			to = "id",
92			on_update = "Cascade",
93			on_delete = "Cascade"
94		)]
95		pub info: HasOne<entity::info::movies::Entity>,
96	}
97
98	impl ActiveModelBehavior for ActiveModel {}
99}
100
101/// Show entity
102pub mod shows {
103	use flix_model::id::ShowId as FlixId;
104	use flix_tmdb::model::id::ShowId;
105
106	use chrono::{DateTime, Utc};
107	use sea_orm::entity::prelude::*;
108
109	use crate::entity;
110
111	/// The database representation of a tmdb show
112	#[sea_orm::model]
113	#[derive(Debug, Clone, DeriveEntityModel)]
114	#[sea_orm(table_name = "flix_tmdb_shows")]
115	pub struct Model {
116		/// The show's TMDB ID
117		#[sea_orm(primary_key, auto_increment = false)]
118		pub tmdb_id: ShowId,
119		/// The show's ID
120		#[sea_orm(unique)]
121		pub flix_id: FlixId,
122		/// The movie's runtime in seconds
123		pub last_update: DateTime<Utc>,
124		/// The number of seasons the show has
125		pub number_of_seasons: u32,
126
127		/// The info for this show
128		#[sea_orm(
129			belongs_to,
130			from = "flix_id",
131			to = "id",
132			on_update = "Cascade",
133			on_delete = "Cascade"
134		)]
135		pub info: HasOne<entity::info::shows::Entity>,
136
137		/// Seasons that are part of this show
138		#[sea_orm(has_many)]
139		pub seasons: HasMany<super::seasons::Entity>,
140		/// Episodes that are part of this show
141		#[sea_orm(has_many)]
142		pub episodes: HasMany<super::episodes::Entity>,
143	}
144
145	impl ActiveModelBehavior for ActiveModel {}
146}
147
148/// Season entity
149pub mod seasons {
150	use flix_model::id::ShowId as FlixId;
151	use flix_model::numbers::SeasonNumber;
152	use flix_tmdb::model::id::ShowId;
153
154	use chrono::{DateTime, Utc};
155	use sea_orm::entity::prelude::*;
156
157	use crate::entity;
158
159	/// The database representation of a tmdb season
160	#[sea_orm::model]
161	#[derive(Debug, Clone, DeriveEntityModel)]
162	#[sea_orm(table_name = "flix_tmdb_seasons")]
163	pub struct Model {
164		/// The season's show's TMDB ID
165		#[sea_orm(primary_key, auto_increment = false)]
166		pub tmdb_show: ShowId,
167		/// The season's TMDB season number
168		#[sea_orm(primary_key, auto_increment = false)]
169		pub tmdb_season: SeasonNumber,
170		/// The season's show's ID
171		#[sea_orm(unique_key = "flix")]
172		pub flix_show: FlixId,
173		/// The season's number
174		#[sea_orm(unique_key = "flix")]
175		pub flix_season: SeasonNumber,
176		/// The date of the last update
177		pub last_update: DateTime<Utc>,
178
179		/// The show this season belongs to
180		#[sea_orm(
181			belongs_to,
182			from = "tmdb_show",
183			to = "tmdb_id",
184			on_update = "Cascade",
185			on_delete = "Cascade"
186		)]
187		pub show: HasOne<super::shows::Entity>,
188		/// The info for this season
189		#[sea_orm(
190			belongs_to,
191			from = "(flix_show, flix_season)",
192			to = "(show_id, season_number)",
193			on_update = "Cascade",
194			on_delete = "Cascade"
195		)]
196		pub info: HasOne<entity::info::seasons::Entity>,
197
198		/// Episodes that are part of this season
199		#[sea_orm(has_many)]
200		pub episodes: HasMany<super::episodes::Entity>,
201	}
202
203	impl ActiveModelBehavior for ActiveModel {}
204}
205
206/// Season entity
207pub mod episodes {
208	use flix_model::id::ShowId as FlixId;
209	use flix_model::numbers::{EpisodeNumber, SeasonNumber};
210	use flix_tmdb::model::id::ShowId;
211	use seamantic::model::duration::Seconds;
212
213	use chrono::{DateTime, Utc};
214	use sea_orm::entity::prelude::*;
215
216	use crate::entity;
217
218	/// The database representation of a tmdb episode
219	#[sea_orm::model]
220	#[derive(Debug, Clone, DeriveEntityModel)]
221	#[sea_orm(table_name = "flix_tmdb_episodes")]
222	pub struct Model {
223		/// The episode's show's TMDB ID
224		#[sea_orm(primary_key, auto_increment = false)]
225		pub tmdb_show: ShowId,
226		/// The episode's season's TMDB season number
227		#[sea_orm(primary_key, auto_increment = false)]
228		pub tmdb_season: SeasonNumber,
229		/// The episode's TMDB episode number
230		#[sea_orm(primary_key, auto_increment = false)]
231		pub tmdb_episode: EpisodeNumber,
232		/// The episode's show's ID
233		#[sea_orm(unique_key = "flix")]
234		pub flix_show: FlixId,
235		/// The episode's season's number
236		#[sea_orm(unique_key = "flix")]
237		pub flix_season: SeasonNumber,
238		/// The episode's number
239		#[sea_orm(unique_key = "flix")]
240		pub flix_episode: EpisodeNumber,
241		/// The date of the last update
242		pub last_update: DateTime<Utc>,
243		/// The episode's runtime in seconds
244		pub runtime: Seconds,
245
246		/// The show this episode belongs to
247		#[sea_orm(
248			belongs_to,
249			from = "tmdb_show",
250			to = "tmdb_id",
251			on_update = "Cascade",
252			on_delete = "Cascade"
253		)]
254		pub show: HasOne<super::shows::Entity>,
255		/// The season this episode belongs to
256		#[sea_orm(
257			belongs_to,
258			from = "(tmdb_show, tmdb_season)",
259			to = "(tmdb_show, tmdb_season)",
260			on_update = "Cascade",
261			on_delete = "Cascade"
262		)]
263		pub season: HasOne<super::seasons::Entity>,
264		/// The info for this episode
265		#[sea_orm(
266			belongs_to,
267			from = "(flix_show, flix_season, flix_episode)",
268			to = "(show_id, season_number, episode_number)",
269			on_update = "Cascade",
270			on_delete = "Cascade"
271		)]
272		pub info: HasOne<entity::info::episodes::Entity>,
273	}
274
275	impl ActiveModelBehavior for ActiveModel {}
276}
277
278/// Macros for creating tmdb entities
279#[cfg(test)]
280pub mod test {
281	macro_rules! make_tmdb_collection {
282		($db:expr, $id:expr, $flix_id:expr) => {
283			$crate::entity::tmdb::collections::ActiveModel {
284				tmdb_id: Set(::flix_tmdb::model::id::CollectionId::from_raw($id)),
285				flix_id: Set(::flix_model::id::CollectionId::from_raw($flix_id)),
286				last_update: Set(::chrono::Utc::now()),
287				movie_count: Set(::core::default::Default::default()),
288			}
289			.insert($db)
290			.await
291			.expect("insert");
292		};
293	}
294	pub(crate) use make_tmdb_collection;
295
296	macro_rules! make_tmdb_movie {
297		($db:expr, $id:expr, $flix_id:expr) => {
298			$crate::entity::tmdb::movies::ActiveModel {
299				tmdb_id: Set(::flix_tmdb::model::id::MovieId::from_raw($id)),
300				flix_id: Set(::flix_model::id::MovieId::from_raw($flix_id)),
301				last_update: Set(::chrono::Utc::now()),
302				runtime: Set(::core::default::Default::default()),
303				collection_id: Set(None),
304			}
305			.insert($db)
306			.await
307			.expect("insert");
308		};
309	}
310	pub(crate) use make_tmdb_movie;
311
312	macro_rules! make_tmdb_show {
313		($db:expr, $id:expr, $flix_id:expr) => {
314			$crate::entity::tmdb::shows::ActiveModel {
315				tmdb_id: Set(::flix_tmdb::model::id::ShowId::from_raw($id)),
316				flix_id: Set(::flix_model::id::ShowId::from_raw($flix_id)),
317				last_update: Set(::chrono::Utc::now()),
318				number_of_seasons: Set(::core::default::Default::default()),
319			}
320			.insert($db)
321			.await
322			.expect("insert");
323		};
324	}
325	pub(crate) use make_tmdb_show;
326
327	macro_rules! make_tmdb_season {
328		($db:expr, $show:expr, $season:expr, $flix_show:expr, $flix_season:expr) => {
329			$crate::entity::tmdb::seasons::ActiveModel {
330				tmdb_show: Set(::flix_tmdb::model::id::ShowId::from_raw($show)),
331				tmdb_season: Set(::flix_model::numbers::SeasonNumber::new($season)),
332				flix_show: Set(::flix_model::id::ShowId::from_raw($flix_show)),
333				flix_season: Set(::flix_model::numbers::SeasonNumber::new($flix_season)),
334				last_update: Set(::chrono::Utc::now()),
335			}
336			.insert($db)
337			.await
338			.expect("insert");
339		};
340	}
341	pub(crate) use make_tmdb_season;
342
343	macro_rules! make_tmdb_episode {
344		($db:expr, $show:expr, $season:expr, $episode:expr, $flix_show:expr, $flix_season:expr, $flix_episode:expr) => {
345			$crate::entity::tmdb::episodes::ActiveModel {
346				tmdb_show: Set(::flix_tmdb::model::id::ShowId::from_raw($show)),
347				tmdb_season: Set(::flix_model::numbers::SeasonNumber::new($season)),
348				tmdb_episode: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
349				flix_show: Set(::flix_model::id::ShowId::from_raw($flix_show)),
350				flix_season: Set(::flix_model::numbers::SeasonNumber::new($flix_season)),
351				flix_episode: Set(::flix_model::numbers::EpisodeNumber::new($flix_episode)),
352				last_update: Set(::chrono::Utc::now()),
353				runtime: Set(::core::default::Default::default()),
354			}
355			.insert($db)
356			.await
357			.expect("insert");
358		};
359	}
360	pub(crate) use make_tmdb_episode;
361}
362
363#[cfg(test)]
364mod tests {
365	use core::time::Duration;
366
367	use flix_model::id::{CollectionId, MovieId, ShowId};
368	use flix_tmdb::model::id::{
369		CollectionId as TmdbCollectionId, MovieId as TmdbMovieId, ShowId as TmdbShowId,
370	};
371
372	use chrono::NaiveDate;
373	use sea_orm::ActiveValue::{NotSet, Set};
374	use sea_orm::entity::prelude::*;
375	use sea_orm::sqlx::error::ErrorKind;
376
377	use crate::entity::info::test::{
378		make_info_collection, make_info_episode, make_info_movie, make_info_season, make_info_show,
379	};
380	use crate::tests::new_initialized_memory_db;
381
382	use super::super::tests::get_error_kind;
383	use super::super::tests::notsettable;
384	use super::test::{
385		make_tmdb_collection, make_tmdb_episode, make_tmdb_movie, make_tmdb_season, make_tmdb_show,
386	};
387
388	#[tokio::test]
389	async fn use_test_macros() {
390		let db = new_initialized_memory_db().await;
391
392		make_info_collection!(&db, 1);
393		make_info_movie!(&db, 1);
394		make_info_show!(&db, 1);
395		make_info_season!(&db, 1, 1);
396		make_info_episode!(&db, 1, 1, 1);
397
398		make_tmdb_collection!(&db, 1, 1);
399		make_tmdb_movie!(&db, 1, 1);
400		make_tmdb_show!(&db, 1, 1);
401		make_tmdb_season!(&db, 1, 1, 1, 1);
402		make_tmdb_episode!(&db, 1, 1, 1, 1, 1, 1);
403	}
404
405	#[tokio::test]
406	async fn test_round_trip_collections() {
407		let db = new_initialized_memory_db().await;
408
409		macro_rules! assert_collection {
410			($db:expr, $id:literal, $tid:literal, Success $(; $($skip:ident),+)?) => {
411				let model = assert_collection!(@insert, $db, $id, $tid $(; $($skip),+)?)
412					.expect("insert");
413
414				assert_eq!(model.tmdb_id, TmdbCollectionId::from_raw($tid));
415				assert_eq!(model.flix_id, CollectionId::from_raw($id));
416				assert_eq!(model.last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
417				assert_eq!(model.movie_count, $id);
418			};
419			($db:expr, $id:literal, $tid:literal, $error:ident $(; $($skip:ident),+)?) => {
420				let model = assert_collection!(@insert, $db, $id, $tid $(; $($skip),+)?)
421					.expect_err("insert");
422
423				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
424			};
425			(@insert, $db:expr, $id:literal, $tid:literal $(; $($skip:ident),+)?) => {
426				super::collections::ActiveModel {
427					tmdb_id: notsettable!(tmdb_id, TmdbCollectionId::from_raw($tid) $(, $($skip),+)?),
428					flix_id: notsettable!(flix_id, CollectionId::from_raw($id) $(, $($skip),+)?),
429					last_update: notsettable!(last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
430					movie_count: notsettable!(movie_count, $id $(, $($skip),+)?),
431				}.insert($db).await
432			};
433		}
434
435		assert_collection!(&db, 1, 1, ForeignKeyViolation);
436		make_info_collection!(&db, 1);
437		assert_collection!(&db, 1, 1, Success);
438		assert_collection!(&db, 1, 1, UniqueViolation);
439
440		assert_collection!(&db, 1, 2, UniqueViolation);
441		assert_collection!(&db, 2, 1, UniqueViolation);
442		make_info_collection!(&db, 2);
443		assert_collection!(&db, 2, 2, Success);
444
445		make_info_collection!(&db, 3);
446		assert_collection!(&db, 3, 3, Success; tmdb_id);
447		assert_collection!(&db, 4, 4, NotNullViolation; flix_id);
448		assert_collection!(&db, 5, 5, NotNullViolation; last_update);
449		assert_collection!(&db, 6, 6, NotNullViolation; movie_count);
450	}
451
452	#[tokio::test]
453	async fn test_round_trip_movies() {
454		let db = new_initialized_memory_db().await;
455
456		macro_rules! assert_movie {
457			($db:expr, $id:literal, $tid:literal, $cid:expr, Success $(; $($skip:ident),+)?) => {
458				let model = assert_movie!(@insert, $db, $id, $tid, $cid $(; $($skip),+)?)
459					.expect("insert");
460
461				assert_eq!(model.tmdb_id, TmdbMovieId::from_raw($tid));
462				assert_eq!(model.flix_id, MovieId::from_raw($id));
463				assert_eq!(model.last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
464				assert_eq!(model.runtime, Duration::from_secs($tid).into());
465				assert_eq!(model.collection_id, $cid.map(TmdbCollectionId::from_raw));
466			};
467			($db:expr, $id:literal, $tid:literal, $cid:expr, $error:ident $(; $($skip:ident),+)?) => {
468				let model = assert_movie!(@insert, $db, $id, $tid, $cid $(; $($skip),+)?)
469					.expect_err("insert");
470
471				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
472			};
473			(@insert, $db:expr, $id:literal, $tid:literal, $cid:expr $(; $($skip:ident),+)?) => {
474				super::movies::ActiveModel {
475					tmdb_id: notsettable!(tmdb_id, TmdbMovieId::from_raw($tid) $(, $($skip),+)?),
476					flix_id: notsettable!(flix_id, MovieId::from_raw($id) $(, $($skip),+)?),
477					last_update: notsettable!(last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
478					runtime: notsettable!(runtime, Duration::from_secs($tid).into() $(, $($skip),+)?),
479					collection_id: notsettable!(collection_id, $cid.map(TmdbCollectionId::from_raw) $(, $($skip),+)?),
480				}.insert($db).await
481			};
482		}
483
484		assert_movie!(&db, 1, 1, None, ForeignKeyViolation);
485		make_info_movie!(&db, 1);
486		assert_movie!(&db, 1, 1, None, Success);
487		assert_movie!(&db, 1, 1, None, UniqueViolation);
488
489		make_info_movie!(&db, 2);
490		assert_movie!(&db, 2, 2, Some(2), ForeignKeyViolation);
491		make_info_collection!(&db, 2);
492		make_tmdb_collection!(&db, 2, 2);
493		assert_movie!(&db, 2, 2, Some(2), Success);
494		assert_movie!(&db, 1, 2, None, UniqueViolation);
495		assert_movie!(&db, 2, 1, None, UniqueViolation);
496
497		make_info_movie!(&db, 3);
498		assert_movie!(&db, 3, 3, None, Success; tmdb_id);
499		assert_movie!(&db, 4, 4, None, NotNullViolation; flix_id);
500		assert_movie!(&db, 5, 5, None, NotNullViolation; last_update);
501		assert_movie!(&db, 6, 6, None, NotNullViolation; runtime);
502		assert_movie!(&db, 7, 7, None, ForeignKeyViolation; collection_id);
503	}
504
505	#[tokio::test]
506	async fn test_round_trip_shows() {
507		let db = new_initialized_memory_db().await;
508
509		macro_rules! assert_show {
510			($db:expr, $id:literal, $tid:literal, Success $(; $($skip:ident),+)?) => {
511				let model = assert_show!(@insert, $db, $id, $tid $(; $($skip),+)?)
512					.expect("insert");
513
514				assert_eq!(model.tmdb_id, TmdbShowId::from_raw($tid));
515				assert_eq!(model.flix_id, ShowId::from_raw($id));
516				assert_eq!(model.last_update, NaiveDate::from_yo_opt($tid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
517				assert_eq!(model.number_of_seasons, $id);
518			};
519			($db:expr, $id:literal, $tid:literal, $error:ident $(; $($skip:ident),+)?) => {
520				let model = assert_show!(@insert, $db, $id, $tid $(; $($skip),+)?)
521					.expect_err("insert");
522
523				assert_eq!(
524					get_error_kind(model).expect("get_error_kind"),
525					ErrorKind::$error
526				);
527			};
528			(@insert, $db:expr, $id:literal, $tid:literal $(; $($skip:ident),+)?) => {
529				super::shows::ActiveModel {
530					tmdb_id: notsettable!(tmdb_id, TmdbShowId::from_raw($tid) $(, $($skip),+)?),
531					flix_id: notsettable!(flix_id, ShowId::from_raw($id) $(, $($skip),+)?),
532					last_update: notsettable!(last_update, NaiveDate::from_yo_opt($tid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
533					number_of_seasons: notsettable!(number_of_seasons, $id $(, $($skip),+)?),
534				}.insert($db).await
535			};
536		}
537
538		assert_show!(&db, 1, 1, ForeignKeyViolation);
539		make_info_show!(&db, 1);
540		assert_show!(&db, 1, 1, Success);
541		assert_show!(&db, 1, 1, UniqueViolation);
542
543		assert_show!(&db, 1, 2, UniqueViolation);
544		assert_show!(&db, 2, 1, UniqueViolation);
545		make_info_show!(&db, 2);
546		assert_show!(&db, 2, 2, Success);
547
548		make_info_show!(&db, 3);
549		assert_show!(&db, 3, 3, Success; tmdb_id);
550		assert_show!(&db, 4, 4, NotNullViolation; flix_id);
551		assert_show!(&db, 5, 5, NotNullViolation; last_update);
552		assert_show!(&db, 6, 6, NotNullViolation; number_of_seasons);
553	}
554
555	#[tokio::test]
556	async fn test_round_trip_seasons() {
557		let db = new_initialized_memory_db().await;
558
559		macro_rules! assert_season {
560			($db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal, Success $(; $($skip:ident),+)?) => {
561				let model = assert_season!(@insert, $db, $show, $season, $tshow, $tseason $(; $($skip),+)?)
562					.expect("insert");
563
564				assert_eq!(model.tmdb_show, TmdbShowId::from_raw($tshow));
565				assert_eq!(model.tmdb_season, ::flix_model::numbers::SeasonNumber::new($tseason));
566				assert_eq!(model.flix_show, ShowId::from_raw($show));
567				assert_eq!(model.flix_season, ::flix_model::numbers::SeasonNumber::new($season));
568				assert_eq!(model.last_update, NaiveDate::from_yo_opt($tshow, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
569			};
570			($db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal, $error:ident $(; $($skip:ident),+)?) => {
571				let model = assert_season!(@insert, $db, $show, $season, $tshow, $tseason $(; $($skip),+)?)
572					.expect_err("insert");
573
574				assert_eq!(
575					get_error_kind(model).expect("get_error_kind"),
576					ErrorKind::$error
577				);
578			};
579			(@insert, $db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal $(; $($skip:ident),+)?) => {
580				super::seasons::ActiveModel {
581					tmdb_show: notsettable!(tmdb_show, TmdbShowId::from_raw($tshow) $(, $($skip),+)?),
582					tmdb_season: notsettable!(tmdb_season, ::flix_model::numbers::SeasonNumber::new($tseason) $(, $($skip),+)?),
583					flix_show: notsettable!(flix_show, ShowId::from_raw($show) $(, $($skip),+)?),
584					flix_season: notsettable!(flix_season, ::flix_model::numbers::SeasonNumber::new($season) $(, $($skip),+)?),
585					last_update: notsettable!(last_update, NaiveDate::from_yo_opt($tshow, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
586				}.insert($db).await
587			};
588		}
589
590		make_info_show!(&db, 1);
591		make_tmdb_show!(&db, 1, 1);
592
593		assert_season!(&db, 1, 1, 1, 1, ForeignKeyViolation);
594		make_info_season!(&db, 1, 1);
595		assert_season!(&db, 1, 1, 1, 1, Success);
596
597		assert_season!(&db, 1, 1, 1, 1, UniqueViolation);
598		assert_season!(&db, 1, 1, 2, 1, UniqueViolation);
599		assert_season!(&db, 2, 1, 1, 1, UniqueViolation);
600		make_info_season!(&db, 1, 2);
601		assert_season!(&db, 1, 2, 1, 2, Success);
602
603		assert_season!(&db, 1, 3, 1, 3, NotNullViolation; tmdb_show);
604		assert_season!(&db, 1, 4, 1, 4, NotNullViolation; tmdb_season);
605		assert_season!(&db, 1, 5, 1, 5, NotNullViolation; flix_show);
606		assert_season!(&db, 1, 6, 1, 6, NotNullViolation; flix_season);
607		assert_season!(&db, 1, 7, 1, 7, NotNullViolation; last_update);
608	}
609
610	#[tokio::test]
611	async fn test_round_trip_episodes() {
612		let db = new_initialized_memory_db().await;
613
614		macro_rules! assert_episode {
615			($db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal, Success $(; $($skip:ident),+)?) => {
616				let model = assert_episode!(@insert, $db, $show, $season, $episode, $tshow, $tseason, $tepisode $(; $($skip),+)?)
617					.expect("insert");
618
619				assert_eq!(model.tmdb_show, TmdbShowId::from_raw($tshow));
620				assert_eq!(model.tmdb_season, ::flix_model::numbers::SeasonNumber::new($tseason));
621				assert_eq!(model.tmdb_episode, ::flix_model::numbers::EpisodeNumber::new($tepisode));
622				assert_eq!(model.flix_show, ShowId::from_raw($show));
623				assert_eq!(model.flix_season, ::flix_model::numbers::SeasonNumber::new($season));
624				assert_eq!(model.flix_episode, ::flix_model::numbers::EpisodeNumber::new($episode));
625				assert_eq!(model.last_update, NaiveDate::from_yo_opt($tshow, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
626				assert_eq!(model.runtime, Duration::from_secs($tshow).into());
627			};
628			($db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal, $error:ident $(; $($skip:ident),+)?) => {
629				let model = assert_episode!(@insert, $db, $show, $season, $episode, $tshow, $tseason, $tepisode $(; $($skip),+)?)
630					.expect_err("insert");
631
632				assert_eq!(
633					get_error_kind(model).expect("get_error_kind"),
634					ErrorKind::$error
635				);
636			};
637			(@insert, $db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal $(; $($skip:ident),+)?) => {
638				super::episodes::ActiveModel {
639					tmdb_show: notsettable!(tmdb_show, TmdbShowId::from_raw($tshow) $(, $($skip),+)?),
640					tmdb_season: notsettable!(tmdb_season, ::flix_model::numbers::SeasonNumber::new($tseason) $(, $($skip),+)?),
641					tmdb_episode: notsettable!(tmdb_episode, ::flix_model::numbers::EpisodeNumber::new($tepisode) $(, $($skip),+)?),
642					flix_show: notsettable!(flix_show, ShowId::from_raw($show) $(, $($skip),+)?),
643					flix_season: notsettable!(flix_season, ::flix_model::numbers::SeasonNumber::new($season) $(, $($skip),+)?),
644					flix_episode: notsettable!(flix_episode, ::flix_model::numbers::EpisodeNumber::new($episode) $(, $($skip),+)?),
645					last_update: notsettable!(last_update, NaiveDate::from_yo_opt($tshow, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
646					runtime: notsettable!(runtime, Duration::from_secs($tshow).into() $(, $($skip),+)?),
647				}.insert($db).await
648			};
649		}
650
651		make_info_show!(&db, 1);
652		make_info_season!(&db, 1, 1);
653		make_tmdb_show!(&db, 1, 1);
654		make_tmdb_season!(&db, 1, 1, 1, 1);
655
656		assert_episode!(&db, 1, 1, 1, 1, 1, 1, ForeignKeyViolation);
657		make_info_episode!(&db, 1, 1, 1);
658		assert_episode!(&db, 1, 1, 1, 1, 1, 1, Success);
659
660		assert_episode!(&db, 1, 1, 1, 1, 1, 1, UniqueViolation);
661		assert_episode!(&db, 1, 1, 1, 1, 2, 1, UniqueViolation);
662		assert_episode!(&db, 1, 1, 1, 2, 1, 1, UniqueViolation);
663		assert_episode!(&db, 1, 2, 1, 1, 1, 1, UniqueViolation);
664		assert_episode!(&db, 2, 1, 1, 1, 1, 1, UniqueViolation);
665		make_info_episode!(&db, 1, 1, 2);
666		assert_episode!(&db, 1, 1, 2, 1, 1, 2, Success);
667
668		assert_episode!(&db, 1, 1, 3, 1, 1, 3, NotNullViolation; tmdb_show);
669		assert_episode!(&db, 1, 1, 3, 1, 1, 4, NotNullViolation; tmdb_season);
670		assert_episode!(&db, 1, 1, 3, 1, 1, 5, NotNullViolation; tmdb_episode);
671		assert_episode!(&db, 1, 1, 3, 1, 1, 6, NotNullViolation; flix_show);
672		assert_episode!(&db, 1, 1, 3, 1, 1, 7, NotNullViolation; flix_season);
673		assert_episode!(&db, 1, 1, 3, 1, 1, 8, NotNullViolation; flix_episode);
674		assert_episode!(&db, 1, 1, 3, 1, 1, 9, NotNullViolation; last_update);
675		assert_episode!(&db, 1, 1, 3, 1, 1, 10, NotNullViolation; runtime);
676	}
677}