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