1pub mod collections {
5 use flix_model::id::CollectionId as FlixId;
6 use flix_tmdb::model::id::CollectionId;
7
8 use chrono::{DateTime, Utc};
9 use sea_orm::entity::prelude::*;
10
11 use crate::entity;
12
13 #[sea_orm::model]
15 #[derive(Debug, Clone, DeriveEntityModel)]
16 #[sea_orm(table_name = "flix_tmdb_collections")]
17 pub struct Model {
18 #[sea_orm(primary_key, auto_increment = false)]
20 pub tmdb_id: CollectionId,
21 #[sea_orm(unique)]
23 pub flix_id: FlixId,
24 pub last_update: DateTime<Utc>,
26 pub movie_count: u16,
28
29 #[sea_orm(
31 belongs_to,
32 from = "flix_id",
33 to = "id",
34 on_update = "Cascade",
35 on_delete = "Cascade"
36 )]
37 pub info: HasOne<entity::info::collections::Entity>,
38
39 #[sea_orm(has_many)]
41 pub movies: HasMany<super::movies::Entity>,
42 }
43
44 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
45 impl ActiveModelBehavior for ActiveModel {}
46}
47
48pub mod movies {
50 use flix_model::id::MovieId as FlixId;
51 use flix_tmdb::model::id::{CollectionId, MovieId};
52
53 use seamantic::model::duration::Seconds;
54
55 use chrono::{DateTime, Utc};
56 use sea_orm::entity::prelude::*;
57
58 use crate::entity;
59
60 #[sea_orm::model]
62 #[derive(Debug, Clone, DeriveEntityModel)]
63 #[sea_orm(table_name = "flix_tmdb_movies")]
64 pub struct Model {
65 #[sea_orm(primary_key, auto_increment = false)]
67 pub tmdb_id: MovieId,
68 #[sea_orm(unique)]
70 pub flix_id: FlixId,
71 pub last_update: DateTime<Utc>,
73 pub runtime: Seconds,
75 #[sea_orm(indexed)]
77 pub collection_id: Option<CollectionId>,
78
79 #[sea_orm(
81 belongs_to,
82 from = "collection_id",
83 to = "tmdb_id",
84 on_update = "Cascade",
85 on_delete = "Cascade"
86 )]
87 pub collection: HasOne<super::collections::Entity>,
88 #[sea_orm(
90 belongs_to,
91 from = "flix_id",
92 to = "id",
93 on_update = "Cascade",
94 on_delete = "Cascade"
95 )]
96 pub info: HasOne<entity::info::movies::Entity>,
97 }
98
99 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
100 impl ActiveModelBehavior for ActiveModel {}
101}
102
103pub mod shows {
105 use flix_model::id::ShowId as FlixId;
106 use flix_tmdb::model::id::ShowId;
107
108 use chrono::{DateTime, Utc};
109 use sea_orm::entity::prelude::*;
110
111 use crate::entity;
112
113 #[sea_orm::model]
115 #[derive(Debug, Clone, DeriveEntityModel)]
116 #[sea_orm(table_name = "flix_tmdb_shows")]
117 pub struct Model {
118 #[sea_orm(primary_key, auto_increment = false)]
120 pub tmdb_id: ShowId,
121 #[sea_orm(unique)]
123 pub flix_id: FlixId,
124 pub last_update: DateTime<Utc>,
126 pub number_of_seasons: u32,
128
129 #[sea_orm(
131 belongs_to,
132 from = "flix_id",
133 to = "id",
134 on_update = "Cascade",
135 on_delete = "Cascade"
136 )]
137 pub info: HasOne<entity::info::shows::Entity>,
138
139 #[sea_orm(has_many)]
141 pub seasons: HasMany<super::seasons::Entity>,
142 #[sea_orm(has_many)]
144 pub episodes: HasMany<super::episodes::Entity>,
145 }
146
147 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
148 impl ActiveModelBehavior for ActiveModel {}
149}
150
151pub mod seasons {
153 use flix_model::id::ShowId as FlixId;
154 use flix_model::numbers::SeasonNumber;
155 use flix_tmdb::model::id::ShowId;
156
157 use chrono::{DateTime, Utc};
158 use sea_orm::entity::prelude::*;
159
160 use crate::entity;
161
162 #[sea_orm::model]
164 #[derive(Debug, Clone, DeriveEntityModel)]
165 #[sea_orm(table_name = "flix_tmdb_seasons")]
166 pub struct Model {
167 #[sea_orm(primary_key, auto_increment = false)]
169 pub tmdb_show: ShowId,
170 #[sea_orm(primary_key, auto_increment = false)]
172 pub tmdb_season: SeasonNumber,
173 #[sea_orm(unique_key = "flix")]
175 pub flix_show: FlixId,
176 #[sea_orm(unique_key = "flix")]
178 pub flix_season: SeasonNumber,
179 pub last_update: DateTime<Utc>,
181
182 #[sea_orm(
184 belongs_to,
185 from = "tmdb_show",
186 to = "tmdb_id",
187 on_update = "Cascade",
188 on_delete = "Cascade"
189 )]
190 pub show: HasOne<super::shows::Entity>,
191 #[sea_orm(
193 belongs_to,
194 from = "(flix_show, flix_season)",
195 to = "(show_id, season_number)",
196 on_update = "Cascade",
197 on_delete = "Cascade"
198 )]
199 pub info: HasOne<entity::info::seasons::Entity>,
200
201 #[sea_orm(has_many)]
203 pub episodes: HasMany<super::episodes::Entity>,
204 }
205
206 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
207 impl ActiveModelBehavior for ActiveModel {}
208}
209
210pub mod episodes {
212 use flix_model::id::ShowId as FlixId;
213 use flix_model::numbers::{EpisodeNumber, SeasonNumber};
214 use flix_tmdb::model::id::ShowId;
215 use seamantic::model::duration::Seconds;
216
217 use chrono::{DateTime, Utc};
218 use sea_orm::entity::prelude::*;
219
220 use crate::entity;
221
222 #[sea_orm::model]
224 #[derive(Debug, Clone, DeriveEntityModel)]
225 #[sea_orm(table_name = "flix_tmdb_episodes")]
226 pub struct Model {
227 #[sea_orm(primary_key, auto_increment = false)]
229 pub tmdb_show: ShowId,
230 #[sea_orm(primary_key, auto_increment = false)]
232 pub tmdb_season: SeasonNumber,
233 #[sea_orm(primary_key, auto_increment = false)]
235 pub tmdb_episode: EpisodeNumber,
236 #[sea_orm(unique_key = "flix")]
238 pub flix_show: FlixId,
239 #[sea_orm(unique_key = "flix")]
241 pub flix_season: SeasonNumber,
242 #[sea_orm(unique_key = "flix")]
244 pub flix_episode: EpisodeNumber,
245 pub last_update: DateTime<Utc>,
247 pub runtime: Seconds,
249
250 #[sea_orm(
252 belongs_to,
253 from = "tmdb_show",
254 to = "tmdb_id",
255 on_update = "Cascade",
256 on_delete = "Cascade"
257 )]
258 pub show: HasOne<super::shows::Entity>,
259 #[sea_orm(
261 belongs_to,
262 from = "(tmdb_show, tmdb_season)",
263 to = "(tmdb_show, tmdb_season)",
264 on_update = "Cascade",
265 on_delete = "Cascade"
266 )]
267 pub season: HasOne<super::seasons::Entity>,
268 #[sea_orm(
270 belongs_to,
271 from = "(flix_show, flix_season, flix_episode)",
272 to = "(show_id, season_number, episode_number)",
273 on_update = "Cascade",
274 on_delete = "Cascade"
275 )]
276 pub info: HasOne<entity::info::episodes::Entity>,
277 }
278
279 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
280 impl ActiveModelBehavior for ActiveModel {}
281}
282
283#[cfg(test)]
285pub mod test {
286 macro_rules! make_tmdb_collection {
287 ($db:expr, $id:expr, $flix_id:expr) => {
288 _ = $crate::entity::tmdb::collections::ActiveModel {
289 tmdb_id: Set(::flix_tmdb::model::id::CollectionId::from_raw($id)),
290 flix_id: Set(::flix_model::id::CollectionId::from_raw($flix_id)),
291 last_update: Set(::chrono::Utc::now()),
292 movie_count: Set(::core::default::Default::default()),
293 }
294 .insert($db)
295 .await
296 .expect("insert");
297 };
298 }
299 pub(crate) use make_tmdb_collection;
300
301 macro_rules! make_tmdb_movie {
302 ($db:expr, $id:expr, $flix_id:expr) => {
303 _ = $crate::entity::tmdb::movies::ActiveModel {
304 tmdb_id: Set(::flix_tmdb::model::id::MovieId::from_raw($id)),
305 flix_id: Set(::flix_model::id::MovieId::from_raw($flix_id)),
306 last_update: Set(::chrono::Utc::now()),
307 runtime: Set(::core::default::Default::default()),
308 collection_id: Set(None),
309 }
310 .insert($db)
311 .await
312 .expect("insert");
313 };
314 }
315 pub(crate) use make_tmdb_movie;
316
317 macro_rules! make_tmdb_show {
318 ($db:expr, $id:expr, $flix_id:expr) => {
319 _ = $crate::entity::tmdb::shows::ActiveModel {
320 tmdb_id: Set(::flix_tmdb::model::id::ShowId::from_raw($id)),
321 flix_id: Set(::flix_model::id::ShowId::from_raw($flix_id)),
322 last_update: Set(::chrono::Utc::now()),
323 number_of_seasons: Set(::core::default::Default::default()),
324 }
325 .insert($db)
326 .await
327 .expect("insert");
328 };
329 }
330 pub(crate) use make_tmdb_show;
331
332 macro_rules! make_tmdb_season {
333 ($db:expr, $show:expr, $season:expr, $flix_show:expr, $flix_season:expr) => {
334 _ = $crate::entity::tmdb::seasons::ActiveModel {
335 tmdb_show: Set(::flix_tmdb::model::id::ShowId::from_raw($show)),
336 tmdb_season: Set(::flix_model::numbers::SeasonNumber::new($season)),
337 flix_show: Set(::flix_model::id::ShowId::from_raw($flix_show)),
338 flix_season: Set(::flix_model::numbers::SeasonNumber::new($flix_season)),
339 last_update: Set(::chrono::Utc::now()),
340 }
341 .insert($db)
342 .await
343 .expect("insert");
344 };
345 }
346 pub(crate) use make_tmdb_season;
347
348 macro_rules! make_tmdb_episode {
349 ($db:expr, $show:expr, $season:expr, $episode:expr, $flix_show:expr, $flix_season:expr, $flix_episode:expr) => {
350 _ = $crate::entity::tmdb::episodes::ActiveModel {
351 tmdb_show: Set(::flix_tmdb::model::id::ShowId::from_raw($show)),
352 tmdb_season: Set(::flix_model::numbers::SeasonNumber::new($season)),
353 tmdb_episode: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
354 flix_show: Set(::flix_model::id::ShowId::from_raw($flix_show)),
355 flix_season: Set(::flix_model::numbers::SeasonNumber::new($flix_season)),
356 flix_episode: Set(::flix_model::numbers::EpisodeNumber::new($flix_episode)),
357 last_update: Set(::chrono::Utc::now()),
358 runtime: Set(::core::default::Default::default()),
359 }
360 .insert($db)
361 .await
362 .expect("insert");
363 };
364 }
365 pub(crate) use make_tmdb_episode;
366}
367
368#[cfg(test)]
369mod tests {
370 use core::time::Duration;
371
372 use flix_model::id::{CollectionId, MovieId, ShowId};
373 use flix_tmdb::model::id::{
374 CollectionId as TmdbCollectionId, MovieId as TmdbMovieId, ShowId as TmdbShowId,
375 };
376
377 use chrono::NaiveDate;
378 use sea_orm::ActiveValue::{NotSet, Set};
379 use sea_orm::entity::prelude::*;
380 use sea_orm::sqlx::error::ErrorKind;
381
382 use crate::entity::info::test::{
383 make_info_collection, make_info_episode, make_info_movie, make_info_season, make_info_show,
384 };
385 use crate::tests::new_initialized_memory_db;
386
387 use super::super::tests::get_error_kind;
388 use super::super::tests::notsettable;
389 use super::test::{
390 make_tmdb_collection, make_tmdb_episode, make_tmdb_movie, make_tmdb_season, make_tmdb_show,
391 };
392
393 #[cfg(not(miri))]
394 #[tokio::test]
395 #[expect(clippy::missing_panics_doc, reason = "unit test")]
396 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
397 async fn use_test_macros() {
398 let db = new_initialized_memory_db().await;
399
400 make_info_collection!(&db, 1);
401 make_info_movie!(&db, 1);
402 make_info_show!(&db, 1);
403 make_info_season!(&db, 1, 1);
404 make_info_episode!(&db, 1, 1, 1);
405
406 make_tmdb_collection!(&db, 1, 1);
407 make_tmdb_movie!(&db, 1, 1);
408 make_tmdb_show!(&db, 1, 1);
409 make_tmdb_season!(&db, 1, 1, 1, 1);
410 make_tmdb_episode!(&db, 1, 1, 1, 1, 1, 1);
411 }
412
413 #[cfg(not(miri))]
414 #[tokio::test]
415 #[expect(clippy::missing_panics_doc, reason = "unit test")]
416 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
417 async fn round_trip_collections() {
418 let db = new_initialized_memory_db().await;
419
420 macro_rules! assert_collection {
421 ($db:expr, $id:literal, $tid:literal, Success $(; $($skip:ident),+)?) => {
422 let model = assert_collection!(@insert, $db, $id, $tid $(; $($skip),+)?)
423 .expect("insert");
424
425 assert_eq!(model.tmdb_id, TmdbCollectionId::from_raw($tid));
426 assert_eq!(model.flix_id, CollectionId::from_raw($id));
427 assert_eq!(model.last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
428 assert_eq!(model.movie_count, $id);
429 };
430 ($db:expr, $id:literal, $tid:literal, $error:ident $(; $($skip:ident),+)?) => {
431 let model = assert_collection!(@insert, $db, $id, $tid $(; $($skip),+)?)
432 .expect_err("insert");
433
434 assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
435 };
436 (@insert, $db:expr, $id:literal, $tid:literal $(; $($skip:ident),+)?) => {
437 super::collections::ActiveModel {
438 tmdb_id: notsettable!(tmdb_id, TmdbCollectionId::from_raw($tid) $(, $($skip),+)?),
439 flix_id: notsettable!(flix_id, CollectionId::from_raw($id) $(, $($skip),+)?),
440 last_update: notsettable!(last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
441 movie_count: notsettable!(movie_count, $id $(, $($skip),+)?),
442 }.insert($db).await
443 };
444 }
445
446 assert_collection!(&db, 1, 1, ForeignKeyViolation);
447 make_info_collection!(&db, 1);
448 assert_collection!(&db, 1, 1, Success);
449 assert_collection!(&db, 1, 1, UniqueViolation);
450
451 assert_collection!(&db, 1, 2, UniqueViolation);
452 assert_collection!(&db, 2, 1, UniqueViolation);
453 make_info_collection!(&db, 2);
454 assert_collection!(&db, 2, 2, Success);
455
456 make_info_collection!(&db, 3);
457 assert_collection!(&db, 3, 3, Success; tmdb_id);
458 assert_collection!(&db, 4, 4, NotNullViolation; flix_id);
459 assert_collection!(&db, 5, 5, NotNullViolation; last_update);
460 assert_collection!(&db, 6, 6, NotNullViolation; movie_count);
461 }
462
463 #[cfg(not(miri))]
464 #[tokio::test]
465 #[expect(clippy::missing_panics_doc, reason = "unit test")]
466 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
467 async fn round_trip_movies() {
468 let db = new_initialized_memory_db().await;
469
470 macro_rules! assert_movie {
471 ($db:expr, $id:literal, $tid:literal, $cid:expr, Success $(; $($skip:ident),+)?) => {
472 let model = assert_movie!(@insert, $db, $id, $tid, $cid $(; $($skip),+)?)
473 .expect("insert");
474
475 assert_eq!(model.tmdb_id, TmdbMovieId::from_raw($tid));
476 assert_eq!(model.flix_id, MovieId::from_raw($id));
477 assert_eq!(model.last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
478 assert_eq!(model.runtime, Duration::from_secs($tid).into());
479 assert_eq!(model.collection_id, $cid.map(TmdbCollectionId::from_raw));
480 };
481 ($db:expr, $id:literal, $tid:literal, $cid:expr, $error:ident $(; $($skip:ident),+)?) => {
482 let model = assert_movie!(@insert, $db, $id, $tid, $cid $(; $($skip),+)?)
483 .expect_err("insert");
484
485 assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
486 };
487 (@insert, $db:expr, $id:literal, $tid:literal, $cid:expr $(; $($skip:ident),+)?) => {
488 super::movies::ActiveModel {
489 tmdb_id: notsettable!(tmdb_id, TmdbMovieId::from_raw($tid) $(, $($skip),+)?),
490 flix_id: notsettable!(flix_id, MovieId::from_raw($id) $(, $($skip),+)?),
491 last_update: notsettable!(last_update, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
492 runtime: notsettable!(runtime, Duration::from_secs($tid).into() $(, $($skip),+)?),
493 collection_id: notsettable!(collection_id, $cid.map(TmdbCollectionId::from_raw) $(, $($skip),+)?),
494 }.insert($db).await
495 };
496 }
497
498 assert_movie!(&db, 1, 1, None, ForeignKeyViolation);
499 make_info_movie!(&db, 1);
500 assert_movie!(&db, 1, 1, None, Success);
501 assert_movie!(&db, 1, 1, None, UniqueViolation);
502
503 make_info_movie!(&db, 2);
504 assert_movie!(&db, 2, 2, Some(2), ForeignKeyViolation);
505 make_info_collection!(&db, 2);
506 make_tmdb_collection!(&db, 2, 2);
507 assert_movie!(&db, 2, 2, Some(2), Success);
508 assert_movie!(&db, 1, 2, None, UniqueViolation);
509 assert_movie!(&db, 2, 1, None, UniqueViolation);
510
511 make_info_movie!(&db, 3);
512 assert_movie!(&db, 3, 3, None, Success; tmdb_id);
513 assert_movie!(&db, 4, 4, None, NotNullViolation; flix_id);
514 assert_movie!(&db, 5, 5, None, NotNullViolation; last_update);
515 assert_movie!(&db, 6, 6, None, NotNullViolation; runtime);
516 assert_movie!(&db, 7, 7, None, ForeignKeyViolation; collection_id);
517 }
518
519 #[cfg(not(miri))]
520 #[tokio::test]
521 #[expect(clippy::missing_panics_doc, reason = "unit test")]
522 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
523 async fn round_trip_shows() {
524 let db = new_initialized_memory_db().await;
525
526 macro_rules! assert_show {
527 ($db:expr, $id:literal, $tid:literal, Success $(; $($skip:ident),+)?) => {
528 let model = assert_show!(@insert, $db, $id, $tid $(; $($skip),+)?)
529 .expect("insert");
530
531 assert_eq!(model.tmdb_id, TmdbShowId::from_raw($tid));
532 assert_eq!(model.flix_id, ShowId::from_raw($id));
533 assert_eq!(model.last_update, NaiveDate::from_yo_opt($tid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
534 assert_eq!(model.number_of_seasons, $id);
535 };
536 ($db:expr, $id:literal, $tid:literal, $error:ident $(; $($skip:ident),+)?) => {
537 let model = assert_show!(@insert, $db, $id, $tid $(; $($skip),+)?)
538 .expect_err("insert");
539
540 assert_eq!(
541 get_error_kind(model).expect("get_error_kind"),
542 ErrorKind::$error
543 );
544 };
545 (@insert, $db:expr, $id:literal, $tid:literal $(; $($skip:ident),+)?) => {
546 super::shows::ActiveModel {
547 tmdb_id: notsettable!(tmdb_id, TmdbShowId::from_raw($tid) $(, $($skip),+)?),
548 flix_id: notsettable!(flix_id, ShowId::from_raw($id) $(, $($skip),+)?),
549 last_update: notsettable!(last_update, NaiveDate::from_yo_opt($tid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
550 number_of_seasons: notsettable!(number_of_seasons, $id $(, $($skip),+)?),
551 }.insert($db).await
552 };
553 }
554
555 assert_show!(&db, 1, 1, ForeignKeyViolation);
556 make_info_show!(&db, 1);
557 assert_show!(&db, 1, 1, Success);
558 assert_show!(&db, 1, 1, UniqueViolation);
559
560 assert_show!(&db, 1, 2, UniqueViolation);
561 assert_show!(&db, 2, 1, UniqueViolation);
562 make_info_show!(&db, 2);
563 assert_show!(&db, 2, 2, Success);
564
565 make_info_show!(&db, 3);
566 assert_show!(&db, 3, 3, Success; tmdb_id);
567 assert_show!(&db, 4, 4, NotNullViolation; flix_id);
568 assert_show!(&db, 5, 5, NotNullViolation; last_update);
569 assert_show!(&db, 6, 6, NotNullViolation; number_of_seasons);
570 }
571
572 #[cfg(not(miri))]
573 #[tokio::test]
574 #[expect(clippy::missing_panics_doc, reason = "unit test")]
575 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
576 async fn round_trip_seasons() {
577 let db = new_initialized_memory_db().await;
578
579 macro_rules! assert_season {
580 ($db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal, Success $(; $($skip:ident),+)?) => {
581 let model = assert_season!(@insert, $db, $show, $season, $tshow, $tseason $(; $($skip),+)?)
582 .expect("insert");
583
584 assert_eq!(model.tmdb_show, TmdbShowId::from_raw($tshow));
585 assert_eq!(model.tmdb_season, ::flix_model::numbers::SeasonNumber::new($tseason));
586 assert_eq!(model.flix_show, ShowId::from_raw($show));
587 assert_eq!(model.flix_season, ::flix_model::numbers::SeasonNumber::new($season));
588 assert_eq!(model.last_update, NaiveDate::from_yo_opt($tshow, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
589 };
590 ($db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal, $error:ident $(; $($skip:ident),+)?) => {
591 let model = assert_season!(@insert, $db, $show, $season, $tshow, $tseason $(; $($skip),+)?)
592 .expect_err("insert");
593
594 assert_eq!(
595 get_error_kind(model).expect("get_error_kind"),
596 ErrorKind::$error
597 );
598 };
599 (@insert, $db:expr, $show:literal, $season:literal, $tshow:literal, $tseason:literal $(; $($skip:ident),+)?) => {
600 super::seasons::ActiveModel {
601 tmdb_show: notsettable!(tmdb_show, TmdbShowId::from_raw($tshow) $(, $($skip),+)?),
602 tmdb_season: notsettable!(tmdb_season, ::flix_model::numbers::SeasonNumber::new($tseason) $(, $($skip),+)?),
603 flix_show: notsettable!(flix_show, ShowId::from_raw($show) $(, $($skip),+)?),
604 flix_season: notsettable!(flix_season, ::flix_model::numbers::SeasonNumber::new($season) $(, $($skip),+)?),
605 last_update: notsettable!(last_update, NaiveDate::from_yo_opt($tshow, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
606 }.insert($db).await
607 };
608 }
609
610 make_info_show!(&db, 1);
611 make_tmdb_show!(&db, 1, 1);
612
613 assert_season!(&db, 1, 1, 1, 1, ForeignKeyViolation);
614 make_info_season!(&db, 1, 1);
615 assert_season!(&db, 1, 1, 1, 1, Success);
616
617 assert_season!(&db, 1, 1, 1, 1, UniqueViolation);
618 assert_season!(&db, 1, 1, 2, 1, UniqueViolation);
619 assert_season!(&db, 2, 1, 1, 1, UniqueViolation);
620 make_info_season!(&db, 1, 2);
621 assert_season!(&db, 1, 2, 1, 2, Success);
622
623 assert_season!(&db, 1, 3, 1, 3, NotNullViolation; tmdb_show);
624 assert_season!(&db, 1, 4, 1, 4, NotNullViolation; tmdb_season);
625 assert_season!(&db, 1, 5, 1, 5, NotNullViolation; flix_show);
626 assert_season!(&db, 1, 6, 1, 6, NotNullViolation; flix_season);
627 assert_season!(&db, 1, 7, 1, 7, NotNullViolation; last_update);
628 }
629
630 #[cfg(not(miri))]
631 #[tokio::test]
632 #[expect(clippy::missing_panics_doc, reason = "unit test")]
633 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
634 async fn round_trip_episodes() {
635 let db = new_initialized_memory_db().await;
636
637 macro_rules! assert_episode {
638 ($db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal, Success $(; $($skip:ident),+)?) => {
639 let model = assert_episode!(@insert, $db, $show, $season, $episode, $tshow, $tseason, $tepisode $(; $($skip),+)?)
640 .expect("insert");
641
642 assert_eq!(model.tmdb_show, TmdbShowId::from_raw($tshow));
643 assert_eq!(model.tmdb_season, ::flix_model::numbers::SeasonNumber::new($tseason));
644 assert_eq!(model.tmdb_episode, ::flix_model::numbers::EpisodeNumber::new($tepisode));
645 assert_eq!(model.flix_show, ShowId::from_raw($show));
646 assert_eq!(model.flix_season, ::flix_model::numbers::SeasonNumber::new($season));
647 assert_eq!(model.flix_episode, ::flix_model::numbers::EpisodeNumber::new($episode));
648 assert_eq!(model.last_update, NaiveDate::from_yo_opt($tshow, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
649 assert_eq!(model.runtime, Duration::from_secs($tshow).into());
650 };
651 ($db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal, $error:ident $(; $($skip:ident),+)?) => {
652 let model = assert_episode!(@insert, $db, $show, $season, $episode, $tshow, $tseason, $tepisode $(; $($skip),+)?)
653 .expect_err("insert");
654
655 assert_eq!(
656 get_error_kind(model).expect("get_error_kind"),
657 ErrorKind::$error
658 );
659 };
660 (@insert, $db:expr, $show:literal, $season:literal, $episode:literal, $tshow:literal, $tseason:literal, $tepisode:literal $(; $($skip:ident),+)?) => {
661 super::episodes::ActiveModel {
662 tmdb_show: notsettable!(tmdb_show, TmdbShowId::from_raw($tshow) $(, $($skip),+)?),
663 tmdb_season: notsettable!(tmdb_season, ::flix_model::numbers::SeasonNumber::new($tseason) $(, $($skip),+)?),
664 tmdb_episode: notsettable!(tmdb_episode, ::flix_model::numbers::EpisodeNumber::new($tepisode) $(, $($skip),+)?),
665 flix_show: notsettable!(flix_show, ShowId::from_raw($show) $(, $($skip),+)?),
666 flix_season: notsettable!(flix_season, ::flix_model::numbers::SeasonNumber::new($season) $(, $($skip),+)?),
667 flix_episode: notsettable!(flix_episode, ::flix_model::numbers::EpisodeNumber::new($episode) $(, $($skip),+)?),
668 last_update: notsettable!(last_update, NaiveDate::from_yo_opt($tshow, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
669 runtime: notsettable!(runtime, Duration::from_secs($tshow).into() $(, $($skip),+)?),
670 }.insert($db).await
671 };
672 }
673
674 make_info_show!(&db, 1);
675 make_info_season!(&db, 1, 1);
676 make_tmdb_show!(&db, 1, 1);
677 make_tmdb_season!(&db, 1, 1, 1, 1);
678
679 assert_episode!(&db, 1, 1, 1, 1, 1, 1, ForeignKeyViolation);
680 make_info_episode!(&db, 1, 1, 1);
681 assert_episode!(&db, 1, 1, 1, 1, 1, 1, Success);
682
683 assert_episode!(&db, 1, 1, 1, 1, 1, 1, UniqueViolation);
684 assert_episode!(&db, 1, 1, 1, 1, 2, 1, UniqueViolation);
685 assert_episode!(&db, 1, 1, 1, 2, 1, 1, UniqueViolation);
686 assert_episode!(&db, 1, 2, 1, 1, 1, 1, UniqueViolation);
687 assert_episode!(&db, 2, 1, 1, 1, 1, 1, UniqueViolation);
688 make_info_episode!(&db, 1, 1, 2);
689 assert_episode!(&db, 1, 1, 2, 1, 1, 2, Success);
690
691 assert_episode!(&db, 1, 1, 3, 1, 1, 3, NotNullViolation; tmdb_show);
692 assert_episode!(&db, 1, 1, 3, 1, 1, 4, NotNullViolation; tmdb_season);
693 assert_episode!(&db, 1, 1, 3, 1, 1, 5, NotNullViolation; tmdb_episode);
694 assert_episode!(&db, 1, 1, 3, 1, 1, 6, NotNullViolation; flix_show);
695 assert_episode!(&db, 1, 1, 3, 1, 1, 7, NotNullViolation; flix_season);
696 assert_episode!(&db, 1, 1, 3, 1, 1, 8, NotNullViolation; flix_episode);
697 assert_episode!(&db, 1, 1, 3, 1, 1, 9, NotNullViolation; last_update);
698 assert_episode!(&db, 1, 1, 3, 1, 1, 10, NotNullViolation; runtime);
699 }
700}