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