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