1pub mod libraries {
5 use flix_model::id::LibraryId;
6
7 use seamantic::model::duration::Seconds;
8 use seamantic::model::path::PathBytes;
9
10 use chrono::{DateTime, Utc};
11 use sea_orm::entity::prelude::*;
12
13 #[sea_orm::model]
15 #[derive(Debug, Clone, DeriveEntityModel)]
16 #[sea_orm(table_name = "flix_libraries")]
17 pub struct Model {
18 #[sea_orm(primary_key, auto_increment = false)]
20 pub id: LibraryId,
21 pub directory: PathBytes,
23 pub last_scan_date: Option<DateTime<Utc>>,
25 pub last_scan_duration: Option<Seconds>,
27
28 #[sea_orm(has_many)]
30 pub collections: HasMany<super::collections::Entity>,
31 #[sea_orm(has_many)]
33 pub movies: HasMany<super::movies::Entity>,
34 #[sea_orm(has_many)]
36 pub shows: HasMany<super::shows::Entity>,
37 #[sea_orm(has_many)]
39 pub seasons: HasMany<super::seasons::Entity>,
40 #[sea_orm(has_many)]
42 pub episodes: HasMany<super::episodes::Entity>,
43 }
44
45 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
46 impl ActiveModelBehavior for ActiveModel {}
47}
48
49pub mod collections {
51 use flix_model::id::{CollectionId, LibraryId};
52
53 use seamantic::model::path::PathBytes;
54
55 use sea_orm::entity::prelude::*;
56
57 use crate::entity;
58
59 #[sea_orm::model]
61 #[derive(Debug, Clone, DeriveEntityModel)]
62 #[sea_orm(table_name = "flix_collections")]
63 pub struct Model {
64 #[sea_orm(primary_key, auto_increment = false)]
66 pub id: CollectionId,
67 #[sea_orm(indexed)]
69 pub parent_id: Option<CollectionId>,
70 pub library_id: LibraryId,
72 pub directory: PathBytes,
74 pub relative_poster_path: Option<String>,
76
77 #[sea_orm(
79 self_ref,
80 relation_enum = "Parent",
81 from = "parent_id",
82 to = "id",
83 on_update = "Cascade",
84 on_delete = "Cascade"
85 )]
86 pub parent: HasOne<Entity>,
87 #[sea_orm(
89 belongs_to,
90 from = "library_id",
91 to = "id",
92 on_update = "Cascade",
93 on_delete = "Cascade"
94 )]
95 pub library: HasOne<super::libraries::Entity>,
96 #[sea_orm(
98 belongs_to,
99 relation_enum = "Info",
100 from = "id",
101 to = "id",
102 on_update = "Cascade",
103 on_delete = "Cascade"
104 )]
105 pub info: HasOne<entity::info::collections::Entity>,
106
107 #[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
109 pub watched: HasMany<entity::watched::collections::Entity>,
110 }
111
112 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
113 impl ActiveModelBehavior for ActiveModel {}
114}
115
116pub mod movies {
118 use flix_model::id::{CollectionId, LibraryId, MovieId};
119
120 use seamantic::model::path::PathBytes;
121
122 use sea_orm::entity::prelude::*;
123
124 use crate::entity;
125
126 #[sea_orm::model]
128 #[derive(Debug, Clone, DeriveEntityModel)]
129 #[sea_orm(table_name = "flix_movies")]
130 pub struct Model {
131 #[sea_orm(primary_key, auto_increment = false)]
133 pub id: MovieId,
134 #[sea_orm(indexed)]
136 pub parent_id: Option<CollectionId>,
137 pub library_id: LibraryId,
139 pub directory: PathBytes,
141 pub relative_media_path: String,
143 pub relative_poster_path: Option<String>,
145
146 #[sea_orm(
148 belongs_to,
149 from = "parent_id",
150 to = "id",
151 on_update = "Cascade",
152 on_delete = "Cascade"
153 )]
154 pub parent: HasOne<super::collections::Entity>,
155 #[sea_orm(
157 belongs_to,
158 from = "library_id",
159 to = "id",
160 on_update = "Cascade",
161 on_delete = "Cascade"
162 )]
163 pub library: HasOne<super::libraries::Entity>,
164 #[sea_orm(
166 belongs_to,
167 relation_enum = "Info",
168 from = "id",
169 to = "id",
170 on_update = "Cascade",
171 on_delete = "Cascade"
172 )]
173 pub info: HasOne<entity::info::movies::Entity>,
174
175 #[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
177 pub watched: HasMany<entity::watched::movies::Entity>,
178 }
179
180 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
181 impl ActiveModelBehavior for ActiveModel {}
182}
183
184pub mod shows {
186 use flix_model::id::{CollectionId, LibraryId, ShowId};
187
188 use seamantic::model::path::PathBytes;
189
190 use sea_orm::entity::prelude::*;
191
192 use crate::entity;
193
194 #[sea_orm::model]
196 #[derive(Debug, Clone, DeriveEntityModel)]
197 #[sea_orm(table_name = "flix_shows")]
198 pub struct Model {
199 #[sea_orm(primary_key, auto_increment = false)]
201 pub id: ShowId,
202 #[sea_orm(indexed)]
204 pub parent_id: Option<CollectionId>,
205 pub library_id: LibraryId,
207 pub directory: PathBytes,
209 pub relative_poster_path: Option<String>,
211
212 #[sea_orm(
214 belongs_to,
215 from = "parent_id",
216 to = "id",
217 on_update = "Cascade",
218 on_delete = "Cascade"
219 )]
220 pub parent: HasOne<super::collections::Entity>,
221 #[sea_orm(
223 belongs_to,
224 from = "library_id",
225 to = "id",
226 on_update = "Cascade",
227 on_delete = "Cascade"
228 )]
229 pub library: HasOne<super::libraries::Entity>,
230 #[sea_orm(
232 belongs_to,
233 relation_enum = "Info",
234 from = "id",
235 to = "id",
236 on_update = "Cascade",
237 on_delete = "Cascade"
238 )]
239 pub info: HasOne<entity::info::shows::Entity>,
240
241 #[sea_orm(has_many)]
243 pub seasons: HasMany<super::seasons::Entity>,
244 #[sea_orm(has_many)]
246 pub episodes: HasMany<super::episodes::Entity>,
247 #[sea_orm(has_many, relation_enum = "Watched", from = "id", to = "id")]
249 pub watched: HasMany<entity::watched::shows::Entity>,
250 }
251
252 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
253 impl ActiveModelBehavior for ActiveModel {}
254}
255
256pub mod seasons {
258 use flix_model::id::{LibraryId, ShowId};
259 use flix_model::numbers::SeasonNumber;
260
261 use seamantic::model::path::PathBytes;
262
263 use sea_orm::entity::prelude::*;
264
265 use crate::entity;
266
267 #[sea_orm::model]
269 #[derive(Debug, Clone, DeriveEntityModel)]
270 #[sea_orm(table_name = "flix_seasons")]
271 pub struct Model {
272 #[sea_orm(primary_key, auto_increment = false)]
274 pub show_id: ShowId,
275 #[sea_orm(primary_key, auto_increment = false)]
277 pub season_number: SeasonNumber,
278 pub library_id: LibraryId,
280 pub directory: PathBytes,
282 pub relative_poster_path: Option<String>,
284
285 #[sea_orm(
287 belongs_to,
288 from = "show_id",
289 to = "id",
290 on_update = "Cascade",
291 on_delete = "Cascade"
292 )]
293 pub show: HasOne<super::shows::Entity>,
294 #[sea_orm(
296 belongs_to,
297 from = "library_id",
298 to = "id",
299 on_update = "Cascade",
300 on_delete = "Cascade"
301 )]
302 pub library: HasOne<super::libraries::Entity>,
303 #[sea_orm(
305 belongs_to,
306 relation_enum = "Info",
307 from = "(show_id, season_number)",
308 to = "(show_id, season_number)",
309 on_update = "Cascade",
310 on_delete = "Cascade"
311 )]
312 pub info: HasOne<entity::info::seasons::Entity>,
313
314 #[sea_orm(has_many)]
316 pub episodes: HasMany<super::episodes::Entity>,
317 #[sea_orm(
319 has_many,
320 relation_enum = "Watched",
321 from = "(show_id, season_number)",
322 to = "(show_id, season_number)"
323 )]
324 pub watched: HasMany<entity::watched::seasons::Entity>,
325 }
326
327 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
328 impl ActiveModelBehavior for ActiveModel {}
329}
330
331pub mod episodes {
333 use flix_model::id::{LibraryId, ShowId};
334 use flix_model::numbers::{EpisodeNumber, SeasonNumber};
335
336 use seamantic::model::path::PathBytes;
337
338 use sea_orm::entity::prelude::*;
339
340 use crate::entity;
341
342 #[sea_orm::model]
344 #[derive(Debug, Clone, DeriveEntityModel)]
345 #[sea_orm(table_name = "flix_episodes")]
346 pub struct Model {
347 #[sea_orm(primary_key, auto_increment = false)]
349 pub show_id: ShowId,
350 #[sea_orm(primary_key, auto_increment = false)]
352 pub season_number: SeasonNumber,
353 #[sea_orm(primary_key, auto_increment = false)]
355 pub episode_number: EpisodeNumber,
356 pub count: u8,
358 pub library_id: LibraryId,
360 pub directory: PathBytes,
362 pub relative_media_path: String,
364 pub relative_poster_path: Option<String>,
366
367 #[sea_orm(
369 belongs_to,
370 from = "show_id",
371 to = "id",
372 on_update = "Cascade",
373 on_delete = "Cascade"
374 )]
375 pub show: HasOne<super::shows::Entity>,
376 #[sea_orm(
378 belongs_to,
379 from = "(show_id, season_number)",
380 to = "(show_id, season_number)",
381 on_update = "Cascade",
382 on_delete = "Cascade"
383 )]
384 pub season: HasOne<super::seasons::Entity>,
385 #[sea_orm(
387 belongs_to,
388 from = "library_id",
389 to = "id",
390 on_update = "Cascade",
391 on_delete = "Cascade"
392 )]
393 pub library: HasOne<super::libraries::Entity>,
394 #[sea_orm(
396 belongs_to,
397 relation_enum = "Info",
398 from = "(show_id, season_number, episode_number)",
399 to = "(show_id, season_number, episode_number)",
400 on_update = "Cascade",
401 on_delete = "Cascade"
402 )]
403 pub info: HasOne<entity::info::episodes::Entity>,
404
405 #[sea_orm(
407 has_many,
408 relation_enum = "Watched",
409 from = "(show_id, season_number, episode_number)",
410 to = "(show_id, season_number, episode_number)"
411 )]
412 pub watched: HasMany<entity::watched::episodes::Entity>,
413 }
414
415 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
416 impl ActiveModelBehavior for ActiveModel {}
417}
418
419#[cfg(test)]
421pub mod test {
422 macro_rules! make_content_library {
423 ($db:expr, $id:expr) => {
424 drop(
425 $crate::entity::content::libraries::ActiveModel {
426 id: Set(::flix_model::id::LibraryId::from_raw($id)),
427 directory: Set(::std::path::PathBuf::new().into()),
428 last_scan_date: Set(None),
429 last_scan_duration: Set(None),
430 }
431 .insert($db)
432 .await
433 .expect("insert"),
434 );
435 };
436 }
437 pub(crate) use make_content_library;
438
439 macro_rules! make_content_collection {
440 ($db:expr, $lid:expr, $id:expr, $pid:expr) => {
441 $crate::entity::info::test::make_info_collection!($db, $id);
442 drop(
443 $crate::entity::content::collections::ActiveModel {
444 id: Set(::flix_model::id::CollectionId::from_raw($id)),
445 parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
446 library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
447 directory: Set(::std::path::PathBuf::new().into()),
448 relative_poster_path: Set(::core::option::Option::None),
449 }
450 .insert($db)
451 .await
452 .expect("insert"),
453 );
454 };
455 }
456 pub(crate) use make_content_collection;
457
458 macro_rules! make_content_movie {
459 ($db:expr, $lid:expr, $id:expr, $pid:expr) => {
460 $crate::entity::info::test::make_info_movie!($db, $id);
461 drop(
462 $crate::entity::content::movies::ActiveModel {
463 id: Set(::flix_model::id::MovieId::from_raw($id)),
464 parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
465 library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
466 directory: Set(::std::path::PathBuf::new().into()),
467 relative_media_path: Set(::alloc::string::String::new()),
468 relative_poster_path: Set(::core::option::Option::None),
469 }
470 .insert($db)
471 .await
472 .expect("insert"),
473 );
474 };
475 }
476 pub(crate) use make_content_movie;
477
478 macro_rules! make_content_show {
479 ($db:expr, $lid:expr, $id:expr, $pid:expr) => {
480 $crate::entity::info::test::make_info_show!($db, $id);
481 drop(
482 $crate::entity::content::shows::ActiveModel {
483 id: Set(::flix_model::id::ShowId::from_raw($id)),
484 parent_id: Set($pid.map(::flix_model::id::CollectionId::from_raw)),
485 library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
486 directory: Set(::std::path::PathBuf::new().into()),
487 relative_poster_path: Set(::core::option::Option::None),
488 }
489 .insert($db)
490 .await
491 .expect("insert"),
492 );
493 };
494 }
495 pub(crate) use make_content_show;
496
497 macro_rules! make_content_season {
498 ($db:expr, $lid:expr, $show:expr, $season:expr) => {
499 $crate::entity::info::test::make_info_season!($db, $show, $season);
500 drop(
501 $crate::entity::content::seasons::ActiveModel {
502 show_id: Set(::flix_model::id::ShowId::from_raw($show)),
503 season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
504 library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
505 directory: Set(::std::path::PathBuf::new().into()),
506 relative_poster_path: Set(::core::option::Option::None),
507 }
508 .insert($db)
509 .await
510 .expect("insert"),
511 );
512 };
513 }
514 pub(crate) use make_content_season;
515
516 macro_rules! make_content_episode {
517 ($db:expr, $lid:expr, $show:expr, $season:expr, $episode:expr) => {
518 make_content_episode!(@make, $db, $lid, $show, $season, $episode, 0);
519 };
520 ($db:expr, $lid:literal, $show:literal, $season:literal, $episode:literal, >1) => {
521 make_content_episode!(@make, $db, $lid, $show, $season, $episode, 1);
522 };
523 (@make, $db:expr, $lid:expr, $show:expr, $season:expr, $episode:expr, $count:literal) => {
524 $crate::entity::info::test::make_info_episode!($db, $show, $season, $episode);
525 drop(
526 $crate::entity::content::episodes::ActiveModel {
527 show_id: Set(::flix_model::id::ShowId::from_raw($show)),
528 season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
529 episode_number: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
530 count: Set($count),
531 library_id: Set(::flix_model::id::LibraryId::from_raw($lid)),
532 directory: Set(::std::path::PathBuf::new().into()),
533 relative_media_path: Set(::alloc::string::String::new()),
534 relative_poster_path: Set(::core::option::Option::None),
535 }
536 .insert($db)
537 .await
538 .expect("insert"),
539 );
540 };
541 }
542 pub(crate) use make_content_episode;
543}
544
545#[cfg(test)]
546mod tests {
547 use core::time::Duration;
548 use std::path::Path;
549
550 use flix_model::id::{CollectionId, LibraryId, MovieId, ShowId};
551
552 use seamantic::model::duration::Seconds;
553
554 use chrono::NaiveDate;
555 use sea_orm::ActiveValue::{NotSet, Set};
556 use sea_orm::entity::prelude::*;
557 use sea_orm::sqlx::error::ErrorKind;
558
559 use crate::entity::content::test::{
560 make_content_collection, make_content_episode, make_content_library, make_content_movie,
561 make_content_season, make_content_show,
562 };
563 use crate::entity::info::test::{
564 make_info_collection, make_info_episode, make_info_movie, make_info_season, make_info_show,
565 };
566 use crate::tests::new_initialized_memory_db;
567
568 use super::super::tests::get_error_kind;
569 use super::super::tests::{noneable, notsettable};
570
571 #[cfg(not(miri))]
572 #[tokio::test]
573 #[expect(clippy::missing_panics_doc, reason = "unit test")]
574 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
575 async fn use_test_macros() {
576 let db = new_initialized_memory_db().await;
577
578 make_content_library!(&db, 1);
579 make_content_collection!(&db, 1, 1, None);
580 make_content_movie!(&db, 1, 1, None);
581 make_content_show!(&db, 1, 1, None);
582 make_content_season!(&db, 1, 1, 1);
583 make_content_episode!(&db, 1, 1, 1, 1);
584 }
585
586 #[cfg(not(miri))]
587 #[tokio::test]
588 #[expect(clippy::missing_panics_doc, reason = "unit test")]
589 async fn round_trip_libraries() {
590 let db = new_initialized_memory_db().await;
591
592 macro_rules! assert_library {
593 ($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => {
594 let model = assert_library!(@insert, $db, $id $(; $($skip),+)?)
595 .expect("insert");
596
597 assert_eq!(model.id, LibraryId::from_raw($id));
598 assert_eq!(model.directory, Path::new(concat!("L Directory ", $id)).to_owned().into());
599 assert_eq!(model.last_scan_date, noneable!(last_scan_date, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?));
600 assert_eq!(model.last_scan_duration, noneable!(last_scan_duration, Seconds(Duration::from_secs($id)) $(, $($skip),+)?));
601 };
602 ($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => {
603 let model = assert_library!(@insert, $db, $id $(; $($skip),+)?)
604 .expect_err("insert");
605
606 assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
607 };
608 (@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => {
609 super::libraries::ActiveModel {
610 id: notsettable!(id, LibraryId::from_raw($id) $(, $($skip),+)?),
611 directory: notsettable!(directory, Path::new(concat!("L Directory ", $id)).to_owned().into() $(, $($skip),+)?),
612 last_scan_date: notsettable!(last_scan_date, Some(NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc()) $(, $($skip),+)?),
613 last_scan_duration: notsettable!(last_scan_duration, Some(Seconds(Duration::from_secs($id))) $(, $($skip),+)?),
614 }.insert($db).await
615 };
616 }
617
618 assert_library!(&db, 1, Success);
619 assert_library!(&db, 1, UniqueViolation);
620 assert_library!(&db, 2, Success);
621 assert_library!(&db, 3, Success; id);
622 assert_library!(&db, 4, NotNullViolation; directory);
623 assert_library!(&db, 5, Success; last_scan_date);
624 assert_library!(&db, 6, Success; last_scan_duration);
625 }
626
627 #[cfg(not(miri))]
628 #[tokio::test]
629 #[expect(clippy::missing_panics_doc, reason = "unit test")]
630 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
631 async fn round_trip_collections() {
632 let db = new_initialized_memory_db().await;
633
634 macro_rules! assert_collection {
635 ($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => {
636 let model = assert_collection!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
637 .expect("insert");
638
639 assert_eq!(model.id, CollectionId::from_raw($id));
640 assert_eq!(model.parent_id, $pid.map(CollectionId::from_raw));
641 assert_eq!(model.library_id, LibraryId::from_raw($lid));
642 assert_eq!(model.directory, Path::new(concat!("C Directory ", $id)).to_owned().into());
643 assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("C Poster ", $id).to_owned() $(, $($skip),+)?));
644 };
645 ($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => {
646 let model = assert_collection!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
647 .expect_err("insert");
648
649 assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
650 };
651 (@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => {
652 super::collections::ActiveModel {
653 id: notsettable!(id, CollectionId::from_raw($id) $(, $($skip),+)?),
654 parent_id: notsettable!(parent_id, $pid.map(CollectionId::from_raw) $(, $($skip),+)?),
655 library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?),
656 directory: notsettable!(directory, Path::new(concat!("C Directory ", $id)).to_owned().into() $(, $($skip),+)?),
657 relative_poster_path: notsettable!(relative_poster_path, Some(concat!("C Poster ", $id).to_owned()) $(, $($skip),+)?),
658 }.insert($db).await
659 };
660 }
661
662 make_content_library!(&db, 1);
663 assert_collection!(&db, 1, None, 1, ForeignKeyViolation);
664 make_info_collection!(&db, 1);
665 assert_collection!(&db, 1, None, 1, Success);
666 make_info_collection!(&db, 2);
667 assert_collection!(&db, 2, None, 2, ForeignKeyViolation);
668 make_content_library!(&db, 2);
669 assert_collection!(&db, 2, None, 2, Success);
670
671 assert_collection!(&db, 1, None, 1, UniqueViolation);
672 make_info_collection!(&db, 3);
673 make_info_collection!(&db, 4);
674 make_info_collection!(&db, 5);
675 make_info_collection!(&db, 6);
676 make_info_collection!(&db, 7);
677 make_info_collection!(&db, 8);
678 assert_collection!(&db, 3, None, 1, Success; id);
679 assert_collection!(&db, 4, None, 1, Success; parent_id);
680 assert_collection!(&db, 5, None, 1, NotNullViolation; library_id);
681 assert_collection!(&db, 6, None, 1, NotNullViolation; directory);
682 assert_collection!(&db, 7, None, 1, Success; relative_poster_path);
683 }
684
685 #[cfg(not(miri))]
686 #[tokio::test]
687 #[expect(clippy::missing_panics_doc, reason = "unit test")]
688 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
689 async fn round_trip_movies() {
690 let db = new_initialized_memory_db().await;
691
692 macro_rules! assert_movie {
693 ($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => {
694 let model = assert_movie!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
695 .expect("insert");
696
697 assert_eq!(model.id, MovieId::from_raw($id));
698 assert_eq!(model.parent_id, $pid.map(CollectionId::from_raw));
699 assert_eq!(model.library_id, LibraryId::from_raw($lid));
700 assert_eq!(model.directory, Path::new(concat!("M Directory ", $id)).to_owned().into());
701 assert_eq!(model.relative_media_path, concat!("M Media ", $id));
702 assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("M Poster ", $id).to_owned() $(, $($skip),+)?));
703 };
704 ($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => {
705 let model = assert_movie!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
706 .expect_err("insert");
707
708 assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
709 };
710 (@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => {
711 super::movies::ActiveModel {
712 id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?),
713 parent_id: notsettable!(parent_id, $pid.map(CollectionId::from_raw) $(, $($skip),+)?),
714 library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?),
715 directory: notsettable!(directory, Path::new(concat!("M Directory ", $id)).to_owned().into() $(, $($skip),+)?),
716 relative_media_path: notsettable!(relative_media_path, concat!("M Media ", $id).to_owned() $(, $($skip),+)?),
717 relative_poster_path: notsettable!(relative_poster_path, Some(concat!("M Poster ", $id).to_owned()) $(, $($skip),+)?),
718 }.insert($db).await
719 };
720 }
721
722 make_content_library!(&db, 1);
723 assert_movie!(&db, 1, None, 1, ForeignKeyViolation);
724 make_info_movie!(&db, 1);
725 assert_movie!(&db, 1, Some(1), 1, ForeignKeyViolation);
726 make_content_collection!(&db, 1, 1, None);
727 assert_movie!(&db, 1, Some(1), 1, Success);
728 assert_movie!(&db, 2, None, 2, ForeignKeyViolation);
729 make_info_movie!(&db, 2);
730 assert_movie!(&db, 2, None, 2, ForeignKeyViolation);
731 make_content_library!(&db, 2);
732 assert_movie!(&db, 2, None, 2, Success);
733
734 assert_movie!(&db, 1, None, 1, UniqueViolation);
735 make_info_movie!(&db, 3);
736 make_info_movie!(&db, 4);
737 make_info_movie!(&db, 5);
738 make_info_movie!(&db, 6);
739 make_info_movie!(&db, 7);
740 make_info_movie!(&db, 8);
741 make_info_movie!(&db, 9);
742 assert_movie!(&db, 3, None, 1, Success; id);
743 assert_movie!(&db, 4, None, 1, Success; parent_id);
744 assert_movie!(&db, 5, None, 1, NotNullViolation; library_id);
745 assert_movie!(&db, 6, None, 1, NotNullViolation; directory);
746 assert_movie!(&db, 7, None, 1, NotNullViolation; relative_media_path);
747 assert_movie!(&db, 8, None, 1, Success; relative_poster_path);
748 }
749
750 #[cfg(not(miri))]
751 #[tokio::test]
752 #[expect(clippy::missing_panics_doc, reason = "unit test")]
753 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
754 async fn round_trip_shows() {
755 let db = new_initialized_memory_db().await;
756
757 macro_rules! assert_show {
758 ($db:expr, $id:literal, $pid:expr, $lid:literal, Success $(; $($skip:ident),+)?) => {
759 let model = assert_show!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
760 .expect("insert");
761
762 assert_eq!(model.id, ShowId::from_raw($id));
763 assert_eq!(model.parent_id, $pid.map(CollectionId::from_raw));
764 assert_eq!(model.library_id, LibraryId::from_raw($lid));
765 assert_eq!(model.directory, Path::new(concat!("S Directory ", $id)).to_owned().into());
766 assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("S Poster ", $id).to_owned() $(, $($skip),+)?));
767 };
768 ($db:expr, $id:literal, $pid:expr, $lid:literal, $error:ident $(; $($skip:ident),+)?) => {
769 let model = assert_show!(@insert, $db, $id, $pid, $lid $(; $($skip),+)?)
770 .expect_err("insert");
771
772 assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
773 };
774 (@insert, $db:expr, $id:literal, $pid:expr, $lid:literal $(; $($skip:ident),+)?) => {
775 super::shows::ActiveModel {
776 id: notsettable!(id, ShowId::from_raw($id) $(, $($skip),+)?),
777 parent_id: notsettable!(parent_id, $pid.map(CollectionId::from_raw) $(, $($skip),+)?),
778 library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?),
779 directory: notsettable!(directory, Path::new(concat!("S Directory ", $id)).to_owned().into() $(, $($skip),+)?),
780 relative_poster_path: notsettable!(relative_poster_path, Some(concat!("S Poster ", $id).to_owned()) $(, $($skip),+)?),
781 }.insert($db).await
782 };
783 }
784
785 make_content_library!(&db, 1);
786 assert_show!(&db, 1, None, 1, ForeignKeyViolation);
787 make_info_show!(&db, 1);
788 assert_show!(&db, 1, Some(1), 1, ForeignKeyViolation);
789 make_content_collection!(&db, 1, 1, None);
790 assert_show!(&db, 1, Some(1), 1, Success);
791 assert_show!(&db, 2, None, 2, ForeignKeyViolation);
792 make_info_show!(&db, 2);
793 assert_show!(&db, 2, None, 2, ForeignKeyViolation);
794 make_content_library!(&db, 2);
795 assert_show!(&db, 2, None, 2, Success);
796
797 assert_show!(&db, 1, None, 1, UniqueViolation);
798 make_info_show!(&db, 3);
799 make_info_show!(&db, 4);
800 make_info_show!(&db, 5);
801 make_info_show!(&db, 6);
802 make_info_show!(&db, 7);
803 make_info_show!(&db, 8);
804 assert_show!(&db, 3, None, 1, Success; id);
805 assert_show!(&db, 4, None, 1, Success; parent_id);
806 assert_show!(&db, 5, None, 1, NotNullViolation; library_id);
807 assert_show!(&db, 6, None, 1, NotNullViolation; directory);
808 assert_show!(&db, 7, None, 1, Success; relative_poster_path);
809 }
810
811 #[cfg(not(miri))]
812 #[tokio::test]
813 #[expect(clippy::missing_panics_doc, reason = "unit test")]
814 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
815 async fn round_trip_seasons() {
816 let db = new_initialized_memory_db().await;
817
818 macro_rules! assert_season {
819 ($db:expr, $id:literal, $season:literal, $lid:literal, Success $(; $($skip:ident),+)?) => {
820 let model = assert_season!(@insert, $db, $id, $season, $lid $(; $($skip),+)?)
821 .expect("insert");
822
823 assert_eq!(model.show_id, ShowId::from_raw($id));
824 assert_eq!(model.season_number, ::flix_model::numbers::SeasonNumber::new($season));
825 assert_eq!(model.library_id, LibraryId::from_raw($lid));
826 assert_eq!(model.directory, Path::new(concat!("SS Directory ", $id, ",", $season)).to_owned().into());
827 assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("SS Poster ", $id, ",", $season).to_owned() $(, $($skip),+)?));
828 };
829 ($db:expr, $id:literal, $season:literal, $lid:literal, $error:ident $(; $($skip:ident),+)?) => {
830 let model = assert_season!(@insert, $db, $id, $season, $lid $(; $($skip),+)?)
831 .expect_err("insert");
832
833 assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
834 };
835 (@insert, $db:expr, $id:literal, $season:literal, $lid:literal $(; $($skip:ident),+)?) => {
836 super::seasons::ActiveModel {
837 show_id: notsettable!(show_id, ShowId::from_raw($id) $(, $($skip),+)?),
838 season_number: notsettable!(season_number, ::flix_model::numbers::SeasonNumber::new($season) $(, $($skip),+)?),
839 library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?),
840 directory: notsettable!(directory, Path::new(concat!("SS Directory ", $id, ",", $season)).to_owned().into() $(, $($skip),+)?),
841 relative_poster_path: notsettable!(relative_poster_path, Some(concat!("SS Poster ", $id, ",", $season).to_owned()) $(, $($skip),+)?),
842 }.insert($db).await
843 };
844 }
845
846 make_content_library!(&db, 1);
847 make_content_show!(&db, 1, 1, None);
848 assert_season!(&db, 1, 1, 1, ForeignKeyViolation);
849 make_info_season!(&db, 1, 1);
850 assert_season!(&db, 1, 1, 1, Success);
851
852 assert_season!(&db, 1, 1, 1, UniqueViolation);
853 make_info_season!(&db, 1, 3);
854 make_info_season!(&db, 1, 4);
855 make_info_season!(&db, 1, 5);
856 make_info_season!(&db, 1, 6);
857 make_info_season!(&db, 1, 7);
858 make_info_season!(&db, 1, 8);
859 assert_season!(&db, 1, 3, 1, NotNullViolation; show_id);
860 assert_season!(&db, 1, 4, 1, NotNullViolation; season_number);
861 assert_season!(&db, 1, 5, 1, NotNullViolation; library_id);
862 assert_season!(&db, 1, 6, 1, NotNullViolation; directory);
863 assert_season!(&db, 1, 7, 1, Success; relative_poster_path);
864 }
865
866 #[cfg(not(miri))]
867 #[tokio::test]
868 #[expect(clippy::missing_panics_doc, reason = "unit test")]
869 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
870 async fn round_trip_episodes() {
871 let db = new_initialized_memory_db().await;
872
873 macro_rules! assert_episode {
874 ($db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal, Success $(; $($skip:ident),+)?) => {
875 let model = assert_episode!(@insert, $db, $id, $season, $episode, $lid $(; $($skip),+)?)
876 .expect("insert");
877
878 assert_eq!(model.show_id, ShowId::from_raw($id));
879 assert_eq!(model.season_number, ::flix_model::numbers::SeasonNumber::new($season));
880 assert_eq!(model.episode_number, ::flix_model::numbers::EpisodeNumber::new($episode));
881 assert_eq!(model.library_id, LibraryId::from_raw($lid));
882 assert_eq!(model.directory, Path::new(concat!("SS Directory ", $id, ",", $season, $episode)).to_owned().into());
883 assert_eq!(model.relative_media_path, concat!("SS Media ", $id, ",", $season, $episode));
884 assert_eq!(model.relative_poster_path, noneable!(relative_poster_path, concat!("SS Poster ", $id, ",", $season, $episode).to_owned() $(, $($skip),+)?));
885 };
886 ($db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal, $error:ident $(; $($skip:ident),+)?) => {
887 let model = assert_episode!(@insert, $db, $id, $season, $episode, $lid $(; $($skip),+)?)
888 .expect_err("insert");
889
890 assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
891 };
892 (@insert, $db:expr, $id:literal, $season:literal, $episode:literal, $lid:literal $(; $($skip:ident),+)?) => {
893 super::episodes::ActiveModel {
894 show_id: notsettable!(show_id, ShowId::from_raw($id) $(, $($skip),+)?),
895 season_number: notsettable!(season_number, ::flix_model::numbers::SeasonNumber::new($season) $(, $($skip),+)?),
896 episode_number: notsettable!(episode_number, ::flix_model::numbers::EpisodeNumber::new($episode) $(, $($skip),+)?),
897 count: notsettable!(count, 0 $(, $($skip),+)?),
898 library_id: notsettable!(library_id, LibraryId::from_raw($lid) $(, $($skip),+)?),
899 directory: notsettable!(directory, Path::new(concat!("SS Directory ", $id, ",", $season, $episode)).to_owned().into() $(, $($skip),+)?),
900 relative_media_path: notsettable!(relative_media_path, concat!("SS Media ", $id, ",", $season, $episode).to_owned() $(, $($skip),+)?),
901 relative_poster_path: notsettable!(relative_poster_path, Some(concat!("SS Poster ", $id, ",", $season, $episode).to_owned()) $(, $($skip),+)?),
902 }.insert($db).await
903 };
904 }
905
906 make_content_library!(&db, 1);
907 make_content_show!(&db, 1, 1, None);
908 make_content_season!(&db, 1, 1, 1);
909 assert_episode!(&db, 1, 1, 1, 1, ForeignKeyViolation);
910 make_info_episode!(&db, 1, 1, 1);
911 assert_episode!(&db, 1, 1, 1, 1, Success);
912
913 assert_episode!(&db, 1, 1, 1, 1, UniqueViolation);
914 make_info_episode!(&db, 1, 1, 3);
915 make_info_episode!(&db, 1, 1, 4);
916 make_info_episode!(&db, 1, 1, 5);
917 make_info_episode!(&db, 1, 1, 6);
918 make_info_episode!(&db, 1, 1, 7);
919 make_info_episode!(&db, 1, 1, 8);
920 make_info_episode!(&db, 1, 1, 9);
921 make_info_episode!(&db, 1, 1, 10);
922 assert_episode!(&db, 1, 1, 3, 1, NotNullViolation; show_id);
923 assert_episode!(&db, 1, 1, 4, 1, NotNullViolation; season_number);
924 assert_episode!(&db, 1, 1, 5, 1, NotNullViolation; episode_number);
925 assert_episode!(&db, 1, 1, 6, 1, NotNullViolation; library_id);
926 assert_episode!(&db, 1, 1, 7, 1, NotNullViolation; directory);
927 assert_episode!(&db, 1, 1, 8, 1, NotNullViolation; relative_media_path);
928 assert_episode!(&db, 1, 1, 9, 1, Success; relative_poster_path);
929 }
930}