flix_db/entity/
content.rs

1//! This module contains entities for storing media file information
2
3/// Library entity
4pub mod libraries {
5	use flix_model::id::LibraryId;
6
7	use seamantic::model::path::PathBytes;
8
9	use sea_orm::entity::prelude::*;
10
11	/// The database representation of a library media folder
12	#[sea_orm::model]
13	#[derive(Debug, Clone, DeriveEntityModel)]
14	#[sea_orm(table_name = "flix_libraries")]
15	pub struct Model {
16		/// The library's ID
17		#[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)]
18		pub id: LibraryId,
19		/// The library's directory
20		pub directory: PathBytes,
21
22		/// Collections that are part of this library
23		#[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")]
24		pub collections: HasMany<super::collections::Entity>,
25		/// Movies that are part of this library
26		#[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")]
27		pub movies: HasMany<super::movies::Entity>,
28		/// Shows that are part of this library
29		#[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")]
30		pub shows: HasMany<super::shows::Entity>,
31		/// Seasons that are part of this library
32		#[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")]
33		pub seasons: HasMany<super::seasons::Entity>,
34		/// Episodes that are part of this library
35		#[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")]
36		pub episodes: HasMany<super::episodes::Entity>,
37	}
38
39	impl ActiveModelBehavior for ActiveModel {}
40}
41
42/// Collection entity
43pub mod collections {
44	use flix_model::id::{CollectionId, LibraryId};
45
46	use seamantic::model::path::PathBytes;
47
48	use sea_orm::entity::prelude::*;
49
50	use crate::entity;
51
52	/// The database representation of a collection media folder
53	#[sea_orm::model]
54	#[derive(Debug, Clone, DeriveEntityModel)]
55	#[sea_orm(table_name = "flix_collections")]
56	pub struct Model {
57		/// The collection's ID
58		#[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)]
59		pub id: CollectionId,
60		/// The collection's parent
61		#[sea_orm(indexed)]
62		pub parent_id: Option<CollectionId>,
63		/// The collection's slug
64		#[sea_orm(unique)]
65		pub slug: String,
66		/// The collection's library ID
67		pub library_id: LibraryId,
68		/// The collection's directory
69		pub directory: PathBytes,
70		/// The collection's poster path
71		pub relative_poster_path: Option<String>,
72
73		/// This collection's parent
74		#[sea_orm(self_ref, relation_enum = "Parent", from = "parent_id", to = "id")]
75		pub parent: HasOne<Entity>,
76		/// The library this collection belongs to
77		#[sea_orm(belongs_to, from = "library_id", to = "id")]
78		pub library: HasOne<super::libraries::Entity>,
79		/// The info for this collection
80		#[sea_orm(belongs_to, relation_enum = "Info", from = "id", to = "id")]
81		pub info: HasOne<entity::info::collections::Entity>,
82		/// The watched info for this collection
83		#[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
84		pub watched: HasMany<entity::watched::collections::Entity>,
85	}
86
87	impl ActiveModelBehavior for ActiveModel {}
88}
89
90/// Movie entity
91pub mod movies {
92	use flix_model::id::{CollectionId, LibraryId, MovieId};
93
94	use seamantic::model::path::PathBytes;
95
96	use sea_orm::entity::prelude::*;
97
98	use crate::entity;
99
100	/// The database representation of a movie media folder
101	#[sea_orm::model]
102	#[derive(Debug, Clone, DeriveEntityModel)]
103	#[sea_orm(table_name = "flix_movies")]
104	pub struct Model {
105		/// The movie's ID
106		#[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)]
107		pub id: MovieId,
108		/// The movie's parent
109		#[sea_orm(indexed)]
110		pub parent_id: Option<CollectionId>,
111		/// The movie's slug
112		#[sea_orm(unique)]
113		pub slug: String,
114		/// The movie's library
115		pub library_id: LibraryId,
116		/// The movie's directory
117		pub directory: PathBytes,
118		/// The movie's media path
119		pub relative_media_path: String,
120		/// The movie's poster path
121		pub relative_poster_path: Option<String>,
122
123		/// This movie's parent
124		#[sea_orm(belongs_to, from = "parent_id", to = "id")]
125		pub parent: HasOne<super::collections::Entity>,
126		/// The library this movie belongs to
127		#[sea_orm(belongs_to, from = "library_id", to = "id")]
128		pub library: HasOne<super::libraries::Entity>,
129		/// The info for this movie
130		#[sea_orm(belongs_to, relation_enum = "Info", from = "id", to = "id")]
131		pub info: HasOne<entity::info::movies::Entity>,
132		/// The watched info for this movie
133		#[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
134		pub watched: HasMany<entity::watched::movies::Entity>,
135	}
136
137	impl ActiveModelBehavior for ActiveModel {}
138}
139
140/// Show entity
141pub mod shows {
142	use flix_model::id::{CollectionId, LibraryId, ShowId};
143
144	use seamantic::model::path::PathBytes;
145
146	use sea_orm::entity::prelude::*;
147
148	use crate::entity;
149
150	/// The database representation of a show media folder
151	#[sea_orm::model]
152	#[derive(Debug, Clone, DeriveEntityModel)]
153	#[sea_orm(table_name = "flix_shows")]
154	pub struct Model {
155		/// The show's ID
156		#[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)]
157		pub id: ShowId,
158		/// The show's parent
159		#[sea_orm(indexed)]
160		pub parent_id: Option<CollectionId>,
161		/// The show's slug
162		#[sea_orm(unique)]
163		pub slug: String,
164		/// The show's library
165		pub library_id: LibraryId,
166		/// The show's directory
167		pub directory: PathBytes,
168		/// The show's poster path
169		pub relative_poster_path: Option<String>,
170
171		/// This show's parent
172		#[sea_orm(belongs_to, from = "parent_id", to = "id")]
173		pub parent: HasOne<super::collections::Entity>,
174		/// The library this show belongs to
175		#[sea_orm(belongs_to, from = "library_id", to = "id")]
176		pub library: HasOne<super::libraries::Entity>,
177		/// The info for this show
178		#[sea_orm(belongs_to, relation_enum = "Info", from = "id", to = "id")]
179		pub info: HasOne<entity::info::shows::Entity>,
180		/// The watched info for this show
181		#[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
182		pub watched: HasMany<entity::watched::shows::Entity>,
183	}
184
185	impl ActiveModelBehavior for ActiveModel {}
186}
187
188/// Season entity
189pub mod seasons {
190	use flix_model::id::{LibraryId, ShowId};
191	use flix_model::numbers::SeasonNumber;
192
193	use seamantic::model::path::PathBytes;
194
195	use sea_orm::entity::prelude::*;
196
197	use crate::entity;
198
199	/// The database representation of a season media folder
200	#[sea_orm::model]
201	#[derive(Debug, Clone, DeriveEntityModel)]
202	#[sea_orm(table_name = "flix_seasons")]
203	pub struct Model {
204		/// The season's show's ID
205		#[sea_orm(primary_key, auto_increment = false)]
206		pub show_id: ShowId,
207		/// The season's number
208		#[sea_orm(primary_key, auto_increment = false)]
209		pub season_number: SeasonNumber,
210		/// The season's slug
211		#[sea_orm(unique)]
212		pub slug: String,
213		/// The season's library
214		pub library_id: LibraryId,
215		/// The season's directory
216		pub directory: PathBytes,
217		/// The season's poster path
218		pub relative_poster_path: Option<String>,
219
220		/// The library this season belongs to
221		#[sea_orm(belongs_to, from = "library_id", to = "id")]
222		pub library: HasOne<super::libraries::Entity>,
223		/// The info for this season
224		#[sea_orm(
225			belongs_to,
226			relation_enum = "Info",
227			from = "(show_id, season_number)",
228			to = "(show_id, season_number)"
229		)]
230		pub info: HasOne<entity::info::seasons::Entity>,
231		/// The watched info for this season
232		#[sea_orm(
233			has_many,
234			relation_enum = "Watched",
235			from = "(show_id, season_number)",
236			to = "(show_id, season_number)"
237		)]
238		pub watched: HasMany<entity::watched::seasons::Entity>,
239	}
240
241	impl ActiveModelBehavior for ActiveModel {}
242}
243
244/// Episode entity
245pub mod episodes {
246	use flix_model::id::{LibraryId, ShowId};
247	use flix_model::numbers::{EpisodeNumber, SeasonNumber};
248
249	use seamantic::model::path::PathBytes;
250
251	use sea_orm::entity::prelude::*;
252
253	use crate::entity;
254
255	/// The database representation of a episode media folder
256	#[sea_orm::model]
257	#[derive(Debug, Clone, DeriveEntityModel)]
258	#[sea_orm(table_name = "flix_episodes")]
259	pub struct Model {
260		/// The episode's show's ID
261		#[sea_orm(primary_key, auto_increment = false)]
262		pub show_id: ShowId,
263		/// The episode's season's number
264		#[sea_orm(primary_key, auto_increment = false)]
265		pub season_number: SeasonNumber,
266		/// The episode's number
267		#[sea_orm(primary_key, auto_increment = false)]
268		pub episode_number: EpisodeNumber,
269		/// The number of additional contained episodes
270		pub count: u8,
271		/// The episode's slug
272		#[sea_orm(unique)]
273		pub slug: String,
274		/// The episode's library
275		pub library_id: LibraryId,
276		/// The episode's directory
277		pub directory: PathBytes,
278		/// The episode's media path
279		pub relative_media_path: String,
280		/// The episode's poster path
281		pub relative_poster_path: Option<String>,
282
283		/// The library this episode belongs to
284		#[sea_orm(belongs_to, from = "library_id", to = "id")]
285		pub library: HasOne<super::libraries::Entity>,
286		/// The info for this episode
287		#[sea_orm(
288			belongs_to,
289			relation_enum = "Info",
290			from = "(show_id, season_number, episode_number)",
291			to = "(show_id, season_number, episode_number)"
292		)]
293		pub info: HasOne<entity::info::episodes::Entity>,
294		/// The watched info for this episode
295		#[sea_orm(
296			has_many,
297			relation_enum = "Watched",
298			from = "(show_id, season_number, episode_number)",
299			to = "(show_id, season_number, episode_number)"
300		)]
301		pub watched: HasMany<entity::watched::episodes::Entity>,
302	}
303
304	impl ActiveModelBehavior for ActiveModel {}
305}
306
307/// Macros for creating content entities
308#[cfg(test)]
309pub mod test {
310	macro_rules! make_content_library {
311		($db:expr, $id:literal) => {
312			$crate::entity::content::libraries::ActiveModel {
313				id: Set(::flix_model::id::LibraryId::from_raw($id)),
314				directory: Set(::std::path::PathBuf::new().into()),
315			}
316			.insert($db)
317			.await
318			.expect("insert");
319		};
320	}
321	pub(crate) use make_content_library;
322
323	macro_rules! make_content_collection {
324		($db:expr, $lid:literal, $id:literal, $pid:expr) => {
325			$crate::entity::info::test::make_info_collection!($db, $id);
326			$crate::entity::content::collections::ActiveModel {
327				id: Set(::flix_model::id::CollectionId::from_raw($id)),
328				parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
329				slug: Set(concat!("C ", $id).to_string()),
330				library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
331				directory: Set(::std::path::PathBuf::new().into()),
332				relative_poster_path: Set(::core::option::Option::None),
333			}
334			.insert($db)
335			.await
336			.expect("insert");
337		};
338	}
339	pub(crate) use make_content_collection;
340
341	macro_rules! make_content_movie {
342		($db:expr, $lid:literal, $id:literal, $pid:expr) => {
343			$crate::entity::info::test::make_info_movie!($db, $id);
344			$crate::entity::content::movies::ActiveModel {
345				id: Set(::flix_model::id::MovieId::from_raw($id)),
346				parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
347				slug: Set(concat!("< ", $id).to_string()),
348				library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
349				directory: Set(::std::path::PathBuf::new().into()),
350				relative_media_path: Set(::std::string::String::new()),
351				relative_poster_path: Set(::core::option::Option::None),
352			}
353			.insert($db)
354			.await
355			.expect("insert");
356		};
357	}
358	pub(crate) use make_content_movie;
359
360	macro_rules! make_content_show {
361		($db:expr, $lid:literal, $id:literal, $pid:expr) => {
362			$crate::entity::info::test::make_info_show!($db, $id);
363			$crate::entity::content::shows::ActiveModel {
364				id: Set(::flix_model::id::ShowId::from_raw($id)),
365				parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
366				slug: Set(concat!("S ", $id).to_string()),
367				library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
368				directory: Set(::std::path::PathBuf::new().into()),
369				relative_poster_path: Set(::core::option::Option::None),
370			}
371			.insert($db)
372			.await
373			.expect("insert");
374		};
375	}
376	pub(crate) use make_content_show;
377
378	macro_rules! make_content_season {
379		($db:expr, $lid:literal, $show:literal, $season:literal) => {
380			$crate::entity::info::test::make_info_season!($db, $show, $season);
381			$crate::entity::content::seasons::ActiveModel {
382				show_id: Set(::flix_model::id::ShowId::from_raw($show)),
383				season_number: Set($season),
384				slug: Set(concat!("SS ", $show, $season).to_string()),
385				library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
386				directory: Set(::std::path::PathBuf::new().into()),
387				relative_poster_path: Set(::core::option::Option::None),
388			}
389			.insert($db)
390			.await
391			.expect("insert");
392		};
393	}
394	pub(crate) use make_content_season;
395
396	macro_rules! make_content_episode {
397		($db:expr, $lid:literal, $show:literal, $season:literal, $episode:literal) => {
398			make_content_episode!(@make, $db, $lid, $show, $season, $episode, 0);
399		};
400		($db:expr, $lid:literal, $show:literal, $season:literal, $episode:literal, >1) => {
401			make_content_episode!(@make, $db, $lid, $show, $season, $episode, 1);
402		};
403		(@make, $db:expr, $lid:literal, $show:literal, $season:literal, $episode:literal, $count:literal) => {
404			$crate::entity::info::test::make_info_episode!($db, $show, $season, $episode);
405			$crate::entity::content::episodes::ActiveModel {
406				show_id: Set(::flix_model::id::ShowId::from_raw($show)),
407				season_number: Set($season),
408				episode_number: Set($episode),
409				count: Set($count),
410				slug: Set(concat!("SSE ", $show, $season, $episode).to_string()),
411				library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
412				directory: Set(::std::path::PathBuf::new().into()),
413				relative_media_path: Set(::std::string::String::new()),
414				relative_poster_path: Set(::core::option::Option::None),
415			}
416			.insert($db)
417			.await
418			.expect("insert");
419		};
420	}
421	pub(crate) use make_content_episode;
422}
423
424#[cfg(test)]
425mod tests {
426	use std::path::Path;
427
428	use flix_model::id::{CollectionId, LibraryId, MovieId, ShowId};
429
430	use sea_orm::ActiveValue::{NotSet, Set};
431	use sea_orm::entity::prelude::*;
432	use sea_orm::sqlx::error::ErrorKind;
433
434	use crate::entity::content::test::{
435		make_content_collection, make_content_episode, make_content_library, make_content_movie,
436		make_content_season, make_content_show,
437	};
438	use crate::entity::info::test::{
439		make_info_collection, make_info_episode, make_info_movie, make_info_season, make_info_show,
440	};
441	use crate::tests::new_initialized_memory_db;
442
443	use super::super::tests::get_error_kind;
444	use super::super::tests::{noneable, notsettable};
445
446	#[tokio::test]
447	async fn use_test_macros() {
448		let db = new_initialized_memory_db().await;
449
450		make_content_library!(&db, 1);
451		make_content_collection!(&db, 1, 1, None);
452		make_content_movie!(&db, 1, 1, None);
453		make_content_show!(&db, 1, 1, None);
454		make_content_season!(&db, 1, 1, 1);
455		make_content_episode!(&db, 1, 1, 1, 1);
456	}
457
458	#[tokio::test]
459	async fn test_round_trip_libraries() {
460		let db = new_initialized_memory_db().await;
461
462		macro_rules! assert_library {
463			($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => {
464				let model = assert_library!(@insert, $db, $id $(; $($skip),+)?)
465					.expect("insert");
466
467				assert_eq!(model.id, LibraryId::from_raw($id));
468				assert_eq!(model.directory, Path::new(concat!("L Directory ", $id)).to_owned().into());
469			};
470			($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => {
471				let model = assert_library!(@insert, $db, $id $(; $($skip),+)?)
472					.expect_err("insert");
473
474				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
475			};
476			(@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => {
477				super::libraries::ActiveModel {
478					id: notsettable!(id, LibraryId::from_raw($id) $(, $($skip),+)?),
479					directory: notsettable!(directory, Path::new(concat!("L Directory ", $id)).to_owned().into() $(, $($skip),+)?),
480				}.insert($db).await
481			};
482		}
483
484		assert_library!(&db, 1, Success);
485		assert_library!(&db, 1, UniqueViolation);
486		assert_library!(&db, 2, Success);
487		assert_library!(&db, 3, Success; id);
488		assert_library!(&db, 4, NotNullViolation; directory);
489	}
490
491	#[tokio::test]
492	async fn test_round_trip_collections() {
493		let db = new_initialized_memory_db().await;
494
495		macro_rules! assert_collection {
496			($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => {
497				let model = assert_collection!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
498					.expect("insert");
499
500				assert_eq!(model.id, CollectionId::from_raw($id));
501				assert_eq!(model.parent_id, $pid.map(CollectionId::from_raw));
502				assert_eq!(model.slug, concat!("C Slug ", $id).to_string());
503				assert_eq!(model.library_id, LibraryId::from_raw($lid));
504				assert_eq!(model.directory, Path::new(concat!("C Directory ", $id)).to_owned().into());
505				assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("C Poster ", $id).to_owned() $(, $($skip),+)?));
506			};
507			($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => {
508				let model = assert_collection!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
509					.expect_err("insert");
510
511				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
512			};
513			(@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => {
514				super::collections::ActiveModel {
515					id: notsettable!(id, CollectionId::from_raw($id) $(, $($skip),+)?),
516					parent_id: notsettable!(parent_id, $pid.map(CollectionId::from_raw) $(, $($skip),+)?),
517					slug: notsettable!(slug, concat!("C Slug ", $id).to_string() $(, $($skip),+)?),
518					library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?),
519					directory: notsettable!(directory, Path::new(concat!("C Directory ", $id)).to_owned().into() $(, $($skip),+)?),
520					relative_poster_path: notsettable!(relative_poster_path, Some(concat!("C Poster ", $id).to_owned()) $(, $($skip),+)?),
521				}.insert($db).await
522			};
523		}
524
525		make_content_library!(&db, 1);
526		assert_collection!(&db, 1, None, 1, ForeignKeyViolation);
527		make_info_collection!(&db, 1);
528		assert_collection!(&db, 1, None, 1, Success);
529		make_info_collection!(&db, 2);
530		assert_collection!(&db, 2, None, 2, ForeignKeyViolation);
531		make_content_library!(&db, 2);
532		assert_collection!(&db, 2, None, 2, Success);
533
534		assert_collection!(&db, 1, None, 1, UniqueViolation);
535		make_info_collection!(&db, 3);
536		make_info_collection!(&db, 4);
537		make_info_collection!(&db, 5);
538		make_info_collection!(&db, 6);
539		make_info_collection!(&db, 7);
540		make_info_collection!(&db, 8);
541		assert_collection!(&db, 3, None, 1, Success; id);
542		assert_collection!(&db, 4, None, 1, Success; parent_id);
543		assert_collection!(&db, 5, None, 1, NotNullViolation; slug);
544		assert_collection!(&db, 6, None, 1, NotNullViolation; library_id);
545		assert_collection!(&db, 7, None, 1, NotNullViolation; directory);
546		assert_collection!(&db, 8, None, 1, Success; relative_poster_path);
547	}
548
549	#[tokio::test]
550	async fn test_round_trip_movies() {
551		let db = new_initialized_memory_db().await;
552
553		macro_rules! assert_movie {
554			($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => {
555				let model = assert_movie!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
556					.expect("insert");
557
558				assert_eq!(model.id, MovieId::from_raw($id));
559				assert_eq!(model.parent_id, $pid.map(CollectionId::from_raw));
560				assert_eq!(model.slug, concat!("M Slug ", $id).to_string());
561				assert_eq!(model.library_id, LibraryId::from_raw($lid));
562				assert_eq!(model.directory, Path::new(concat!("M Directory ", $id)).to_owned().into());
563				assert_eq!(model.relative_media_path, concat!("M Media ", $id));
564				assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("M Poster ", $id).to_owned() $(, $($skip),+)?));
565			};
566			($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => {
567				let model = assert_movie!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
568					.expect_err("insert");
569
570				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
571			};
572			(@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => {
573				super::movies::ActiveModel {
574					id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?),
575					parent_id: notsettable!(parent_id, $pid.map(CollectionId::from_raw) $(, $($skip),+)?),
576					slug: notsettable!(slug, concat!("M Slug ", $id).to_string() $(, $($skip),+)?),
577					library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?),
578					directory: notsettable!(directory, Path::new(concat!("M Directory ", $id)).to_owned().into() $(, $($skip),+)?),
579					relative_media_path: notsettable!(relative_media_path, concat!("M Media ", $id).to_owned() $(, $($skip),+)?),
580					relative_poster_path: notsettable!(relative_poster_path, Some(concat!("M Poster ", $id).to_owned()) $(, $($skip),+)?),
581				}.insert($db).await
582			};
583		}
584
585		make_content_library!(&db, 1);
586		assert_movie!(&db, 1, None, 1, ForeignKeyViolation);
587		make_info_movie!(&db, 1);
588		assert_movie!(&db, 1, Some(1), 1, ForeignKeyViolation);
589		make_content_collection!(&db, 1, 1, None);
590		assert_movie!(&db, 1, Some(1), 1, Success);
591		assert_movie!(&db, 2, None, 2, ForeignKeyViolation);
592		make_info_movie!(&db, 2);
593		assert_movie!(&db, 2, None, 2, ForeignKeyViolation);
594		make_content_library!(&db, 2);
595		assert_movie!(&db, 2, None, 2, Success);
596
597		assert_movie!(&db, 1, None, 1, UniqueViolation);
598		make_info_movie!(&db, 3);
599		make_info_movie!(&db, 4);
600		make_info_movie!(&db, 5);
601		make_info_movie!(&db, 6);
602		make_info_movie!(&db, 7);
603		make_info_movie!(&db, 8);
604		make_info_movie!(&db, 9);
605		assert_movie!(&db, 3, None, 1, Success; id);
606		assert_movie!(&db, 4, None, 1, Success; parent_id);
607		assert_movie!(&db, 5, None, 1, NotNullViolation; slug);
608		assert_movie!(&db, 6, None, 1, NotNullViolation; library_id);
609		assert_movie!(&db, 7, None, 1, NotNullViolation; directory);
610		assert_movie!(&db, 8, None, 1, NotNullViolation; relative_media_path);
611		assert_movie!(&db, 9, None, 1, Success; relative_poster_path);
612	}
613
614	#[tokio::test]
615	async fn test_round_trip_shows() {
616		let db = new_initialized_memory_db().await;
617
618		macro_rules! assert_show {
619			($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => {
620				let model = assert_show!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
621					.expect("insert");
622
623				assert_eq!(model.id, ShowId::from_raw($id));
624				assert_eq!(model.parent_id, $pid.map(CollectionId::from_raw));
625				assert_eq!(model.slug, concat!("S Slug ", $id).to_string());
626				assert_eq!(model.library_id, LibraryId::from_raw($lid));
627				assert_eq!(model.directory, Path::new(concat!("S Directory ", $id)).to_owned().into());
628				assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("S Poster ", $id).to_owned() $(, $($skip),+)?));
629			};
630			($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => {
631				let model = assert_show!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
632					.expect_err("insert");
633
634				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
635			};
636			(@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => {
637				super::shows::ActiveModel {
638					id: notsettable!(id, ShowId::from_raw($id) $(, $($skip),+)?),
639					parent_id: notsettable!(parent_id, $pid.map(CollectionId::from_raw) $(, $($skip),+)?),
640					slug: notsettable!(slug, concat!("S Slug ", $id).to_string() $(, $($skip),+)?),
641					library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?),
642					directory: notsettable!(directory, Path::new(concat!("S Directory ", $id)).to_owned().into() $(, $($skip),+)?),
643					relative_poster_path: notsettable!(relative_poster_path, Some(concat!("S Poster ", $id).to_owned()) $(, $($skip),+)?),
644				}.insert($db).await
645			};
646		}
647
648		make_content_library!(&db, 1);
649		assert_show!(&db, 1, None, 1, ForeignKeyViolation);
650		make_info_show!(&db, 1);
651		assert_show!(&db, 1, Some(1), 1, ForeignKeyViolation);
652		make_content_collection!(&db, 1, 1, None);
653		assert_show!(&db, 1, Some(1), 1, Success);
654		assert_show!(&db, 2, None, 2, ForeignKeyViolation);
655		make_info_show!(&db, 2);
656		assert_show!(&db, 2, None, 2, ForeignKeyViolation);
657		make_content_library!(&db, 2);
658		assert_show!(&db, 2, None, 2, Success);
659
660		assert_show!(&db, 1, None, 1, UniqueViolation);
661		make_info_show!(&db, 3);
662		make_info_show!(&db, 4);
663		make_info_show!(&db, 5);
664		make_info_show!(&db, 6);
665		make_info_show!(&db, 7);
666		make_info_show!(&db, 8);
667		assert_show!(&db, 3, None, 1, Success; id);
668		assert_show!(&db, 4, None, 1, Success; parent_id);
669		assert_show!(&db, 5, None, 1, NotNullViolation; slug);
670		assert_show!(&db, 6, None, 1, NotNullViolation; library_id);
671		assert_show!(&db, 7, None, 1, NotNullViolation; directory);
672		assert_show!(&db, 8, None, 1, Success; relative_poster_path);
673	}
674
675	#[tokio::test]
676	async fn test_round_trip_seasons() {
677		let db = new_initialized_memory_db().await;
678
679		macro_rules! assert_season {
680			($db:expr, $id:literal, $season:literal, $lid:literal, Success $(; $($skip:ident),+)?) => {
681				let model = assert_season!(@insert, $db, $id, $season, $lid $(; $($skip),+)?)
682					.expect("insert");
683
684				assert_eq!(model.show_id, ShowId::from_raw($id));
685				assert_eq!(model.season_number, $season);
686				assert_eq!(model.slug, concat!("SS Slug ", $id, ",", $season).to_string());
687				assert_eq!(model.library_id, LibraryId::from_raw($lid));
688				assert_eq!(model.directory, Path::new(concat!("SS Directory ", $id, ",", $season)).to_owned().into());
689				assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("SS Poster ", $id, ",", $season).to_owned() $(, $($skip),+)?));
690			};
691			($db:expr, $id:literal, $season:literal, $lid:literal, $error:ident $(; $($skip:ident),+)?) => {
692				let model = assert_season!(@insert, $db, $id, $season, $lid $(; $($skip),+)?)
693					.expect_err("insert");
694
695				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
696			};
697			(@insert, $db:expr, $id:literal, $season:literal, $lid:literal $(; $($skip:ident),+)?) => {
698				super::seasons::ActiveModel {
699					show_id: notsettable!(show_id, ShowId::from_raw($id) $(, $($skip),+)?),
700					season_number: notsettable!(season_number, $season $(, $($skip),+)?),
701					slug: notsettable!(slug, concat!("SS Slug ", $id, ",", $season).to_string() $(, $($skip),+)?),
702					library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?),
703					directory: notsettable!(directory, Path::new(concat!("SS Directory ", $id, ",", $season)).to_owned().into() $(, $($skip),+)?),
704					relative_poster_path: notsettable!(relative_poster_path, Some(concat!("SS Poster ", $id, ",", $season).to_owned()) $(, $($skip),+)?),
705				}.insert($db).await
706			};
707		}
708
709		make_content_library!(&db, 1);
710		make_info_show!(&db, 1);
711		assert_season!(&db, 1, 1, 1, ForeignKeyViolation);
712		make_info_season!(&db, 1, 1);
713		assert_season!(&db, 1, 1, 1, Success);
714
715		assert_season!(&db, 1, 1, 1, UniqueViolation);
716		make_info_season!(&db, 1, 3);
717		make_info_season!(&db, 1, 4);
718		make_info_season!(&db, 1, 5);
719		make_info_season!(&db, 1, 6);
720		make_info_season!(&db, 1, 7);
721		make_info_season!(&db, 1, 8);
722		assert_season!(&db, 1, 3, 1, NotNullViolation; show_id);
723		assert_season!(&db, 1, 4, 1, NotNullViolation; season_number);
724		assert_season!(&db, 1, 5, 1, NotNullViolation; slug);
725		assert_season!(&db, 1, 6, 1, NotNullViolation; library_id);
726		assert_season!(&db, 1, 7, 1, NotNullViolation; directory);
727		assert_season!(&db, 1, 8, 1, Success; relative_poster_path);
728	}
729
730	#[tokio::test]
731	async fn test_round_trip_episodes() {
732		let db = new_initialized_memory_db().await;
733
734		macro_rules! assert_episode {
735			($db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal, Success $(; $($skip:ident),+)?) => {
736				let model = assert_episode!(@insert, $db, $id, $season, $episode, $lid $(; $($skip),+)?)
737					.expect("insert");
738
739				assert_eq!(model.show_id, ShowId::from_raw($id));
740				assert_eq!(model.season_number, $season);
741				assert_eq!(model.episode_number, $episode);
742				assert_eq!(model.slug, concat!("SS Slug ", $id, ",", $season, $episode).to_string());
743				assert_eq!(model.library_id, LibraryId::from_raw($lid));
744				assert_eq!(model.directory, Path::new(concat!("SS Directory ", $id, ",", $season, $episode)).to_owned().into());
745				assert_eq!(model.relative_media_path, concat!("SS Media ", $id, ",", $season, $episode));
746				assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("SS Poster ", $id, ",", $season, $episode).to_owned() $(, $($skip),+)?));
747			};
748			($db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal, $error:ident $(; $($skip:ident),+)?) => {
749				let model = assert_episode!(@insert, $db, $id, $season, $episode, $lid $(; $($skip),+)?)
750					.expect_err("insert");
751
752				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
753			};
754			(@insert, $db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal $(; $($skip:ident),+)?) => {
755				super::episodes::ActiveModel {
756					show_id: notsettable!(show_id, ShowId::from_raw($id) $(, $($skip),+)?),
757					season_number: notsettable!(season_number, $season $(, $($skip),+)?),
758					episode_number: notsettable!(episode_number, $episode $(, $($skip),+)?),
759					count: notsettable!(count, 0 $(, $($skip),+)?),
760					slug: notsettable!(slug, concat!("SS Slug ", $id, ",", $season, $episode).to_string() $(, $($skip),+)?),
761					library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?),
762					directory: notsettable!(directory, Path::new(concat!("SS Directory ", $id, ",", $season, $episode)).to_owned().into() $(, $($skip),+)?),
763					relative_media_path: notsettable!(relative_media_path, concat!("SS Media ", $id, ",", $season, $episode).to_owned() $(, $($skip),+)?),
764					relative_poster_path: notsettable!(relative_poster_path, Some(concat!("SS Poster ", $id, ",", $season, $episode).to_owned()) $(, $($skip),+)?),
765				}.insert($db).await
766			};
767		}
768
769		make_content_library!(&db, 1);
770		make_info_show!(&db, 1);
771		make_info_season!(&db, 1, 1);
772		assert_episode!(&db, 1, 1, 1, 1, ForeignKeyViolation);
773		make_info_episode!(&db, 1, 1, 1);
774		assert_episode!(&db, 1, 1, 1, 1, Success);
775
776		assert_episode!(&db, 1, 1, 1, 1, UniqueViolation);
777		make_info_episode!(&db, 1, 1, 3);
778		make_info_episode!(&db, 1, 1, 4);
779		make_info_episode!(&db, 1, 1, 5);
780		make_info_episode!(&db, 1, 1, 6);
781		make_info_episode!(&db, 1, 1, 7);
782		make_info_episode!(&db, 1, 1, 8);
783		make_info_episode!(&db, 1, 1, 9);
784		make_info_episode!(&db, 1, 1, 10);
785		assert_episode!(&db, 1, 1, 3, 1, NotNullViolation; show_id);
786		assert_episode!(&db, 1, 1, 4, 1, NotNullViolation; season_number);
787		assert_episode!(&db, 1, 1, 5, 1, NotNullViolation; episode_number);
788		assert_episode!(&db, 1, 1, 6, 1, NotNullViolation; slug);
789		assert_episode!(&db, 1, 1, 7, 1, NotNullViolation; library_id);
790		assert_episode!(&db, 1, 1, 8, 1, NotNullViolation; directory);
791		assert_episode!(&db, 1, 1, 9, 1, NotNullViolation; relative_media_path);
792		assert_episode!(&db, 1, 1, 10, 1, Success; relative_poster_path);
793	}
794}