flix_db/entity/
watched.rs

1//! This module contains entities for storing watched information
2
3/// Collection entity
4pub mod collections {
5	use flix_model::id::{CollectionId, RawId};
6
7	use chrono::{DateTime, Utc};
8	use sea_orm::entity::prelude::*;
9
10	use crate::entity;
11
12	/// The database representation of a watched movie
13	#[sea_orm::model]
14	#[derive(Debug, Clone, DeriveEntityModel)]
15	#[sea_orm(table_name = "flix_watched_collections")]
16	pub struct Model {
17		/// The collection's ID
18		#[sea_orm(primary_key, auto_increment = false)]
19		pub id: CollectionId,
20		/// The user's ID
21		#[sea_orm(primary_key, auto_increment = false)]
22		pub user_id: RawId,
23		/// The date this collection was watched
24		pub watched_date: DateTime<Utc>,
25
26		/// The info for this collection
27		#[sea_orm(
28			belongs_to,
29			relation_enum = "Info",
30			from = "id",
31			to = "id",
32			on_update = "Cascade",
33			on_delete = "Cascade"
34		)]
35		pub info: HasOne<entity::info::collections::Entity>,
36		/// The content for this collection
37		#[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
38		pub content: HasOne<entity::content::collections::Entity>,
39	}
40
41	impl ActiveModelBehavior for ActiveModel {}
42}
43
44/// Movie entity
45pub mod movies {
46	use flix_model::id::{MovieId, RawId};
47
48	use chrono::{DateTime, Utc};
49	use sea_orm::entity::prelude::*;
50
51	use crate::entity;
52
53	/// The database representation of a watched movie
54	#[sea_orm::model]
55	#[derive(Debug, Clone, DeriveEntityModel)]
56	#[sea_orm(table_name = "flix_watched_movies")]
57	pub struct Model {
58		/// The movie's ID
59		#[sea_orm(primary_key, auto_increment = false)]
60		pub id: MovieId,
61		/// The user's ID
62		#[sea_orm(primary_key, auto_increment = false)]
63		pub user_id: RawId,
64		/// The date this movie was watched
65		pub watched_date: DateTime<Utc>,
66
67		/// The info for this movie
68		#[sea_orm(
69			belongs_to,
70			from = "id",
71			to = "id",
72			on_update = "Cascade",
73			on_delete = "Cascade"
74		)]
75		pub info: HasOne<entity::info::movies::Entity>,
76		/// The content for this movie
77		#[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
78		pub content: HasOne<entity::content::movies::Entity>,
79	}
80
81	impl ActiveModelBehavior for ActiveModel {}
82}
83
84/// Show entity
85pub mod shows {
86	use flix_model::id::{RawId, ShowId};
87
88	use chrono::{DateTime, Utc};
89	use sea_orm::entity::prelude::*;
90
91	use crate::entity;
92
93	/// The database representation of a watched movie
94	#[sea_orm::model]
95	#[derive(Debug, Clone, DeriveEntityModel)]
96	#[sea_orm(table_name = "flix_watched_shows")]
97	pub struct Model {
98		/// The show's ID
99		#[sea_orm(primary_key, auto_increment = false)]
100		pub id: ShowId,
101		/// The user's ID
102		#[sea_orm(primary_key, auto_increment = false)]
103		pub user_id: RawId,
104		/// The date this show was watched
105		pub watched_date: DateTime<Utc>,
106
107		/// The info for this show
108		#[sea_orm(
109			belongs_to,
110			relation_enum = "Info",
111			from = "id",
112			to = "id",
113			on_update = "Cascade",
114			on_delete = "Cascade"
115		)]
116		pub info: HasOne<entity::info::shows::Entity>,
117		/// The content for this show
118		#[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
119		pub content: HasOne<entity::content::shows::Entity>,
120	}
121
122	impl ActiveModelBehavior for ActiveModel {}
123}
124
125/// Season entity
126pub mod seasons {
127	use flix_model::id::{RawId, ShowId};
128	use flix_model::numbers::SeasonNumber;
129
130	use chrono::{DateTime, Utc};
131	use sea_orm::entity::prelude::*;
132
133	use crate::entity;
134
135	/// The database representation of a watched movie
136	#[sea_orm::model]
137	#[derive(Debug, Clone, DeriveEntityModel)]
138	#[sea_orm(table_name = "flix_watched_seasons")]
139	pub struct Model {
140		/// The season's show's ID
141		#[sea_orm(primary_key, auto_increment = false)]
142		pub show_id: ShowId,
143		/// The season's number
144		#[sea_orm(primary_key, auto_increment = false)]
145		pub season_number: SeasonNumber,
146		/// The user's ID
147		#[sea_orm(primary_key, auto_increment = false)]
148		pub user_id: RawId,
149		/// The date this season was watched
150		pub watched_date: DateTime<Utc>,
151
152		/// The info for this season
153		#[sea_orm(
154			belongs_to,
155			relation_enum = "Info",
156			from = "(show_id, season_number)",
157			to = "(show_id, season_number)",
158			on_update = "Cascade",
159			on_delete = "Cascade"
160		)]
161		pub info: HasOne<entity::info::seasons::Entity>,
162		/// The content for this season
163		#[sea_orm(
164			belongs_to,
165			relation_enum = "Content",
166			from = "(show_id, season_number)",
167			to = "(show_id, season_number)",
168			skip_fk
169		)]
170		pub content: HasOne<entity::content::seasons::Entity>,
171	}
172
173	impl ActiveModelBehavior for ActiveModel {}
174}
175
176/// Episode entity
177pub mod episodes {
178	use flix_model::id::{RawId, ShowId};
179	use flix_model::numbers::{EpisodeNumber, SeasonNumber};
180
181	use chrono::{DateTime, Utc};
182	use sea_orm::entity::prelude::*;
183
184	use crate::entity;
185
186	/// The database representation of a watched movie
187	#[sea_orm::model]
188	#[derive(Debug, Clone, DeriveEntityModel)]
189	#[sea_orm(table_name = "flix_watched_episodes")]
190	pub struct Model {
191		/// The episode's show's ID
192		#[sea_orm(primary_key, auto_increment = false)]
193		pub show_id: ShowId,
194		/// The episode's season's number
195		#[sea_orm(primary_key, auto_increment = false)]
196		pub season_number: SeasonNumber,
197		/// The episode's number
198		#[sea_orm(primary_key, auto_increment = false)]
199		pub episode_number: EpisodeNumber,
200		/// The user's ID
201		#[sea_orm(primary_key, auto_increment = false)]
202		pub user_id: RawId,
203		/// The date this episode was watched
204		pub watched_date: DateTime<Utc>,
205
206		/// The info for this episode
207		#[sea_orm(
208			belongs_to,
209			relation_enum = "Info",
210			from = "(show_id, season_number, episode_number)",
211			to = "(show_id, season_number, episode_number)",
212			on_update = "Cascade",
213			on_delete = "Cascade"
214		)]
215		pub info: HasOne<entity::info::episodes::Entity>,
216		/// The content for this episode
217		#[sea_orm(
218			belongs_to,
219			relation_enum = "Content",
220			from = "(show_id, season_number, episode_number)",
221			to = "(show_id, season_number, episode_number)",
222			skip_fk
223		)]
224		pub content: HasOne<entity::content::episodes::Entity>,
225	}
226
227	impl ActiveModelBehavior for ActiveModel {}
228}
229
230/// Macros for creating watched entities
231#[cfg(test)]
232pub mod test {
233	macro_rules! make_watched_movie {
234		($db:expr, $id:expr, $user:expr) => {
235			$crate::entity::watched::movies::ActiveModel {
236				id: Set(::flix_model::id::MovieId::from_raw($id)),
237				user_id: Set($user),
238				watched_date: Set(::chrono::Utc::now()),
239			}
240			.insert($db)
241			.await
242			.expect("insert");
243		};
244	}
245	pub(crate) use make_watched_movie;
246
247	macro_rules! make_watched_episode {
248		($db:expr, $show:expr, $season:expr, $episode:expr, $user:expr) => {
249			$crate::entity::watched::episodes::ActiveModel {
250				show_id: Set(::flix_model::id::ShowId::from_raw($show)),
251				season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
252				episode_number: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
253				user_id: Set($user),
254				watched_date: Set(::chrono::Utc::now()),
255			}
256			.insert($db)
257			.await
258			.expect("insert");
259		};
260	}
261	pub(crate) use make_watched_episode;
262}
263
264#[cfg(test)]
265mod tests {
266	use flix_model::id::{MovieId, ShowId};
267
268	use chrono::NaiveDate;
269	use sea_orm::ActiveValue::{NotSet, Set};
270	use sea_orm::Condition;
271	use sea_orm::entity::prelude::*;
272	use sea_orm::sqlx::error::ErrorKind;
273
274	use crate::entity::content::test::{
275		make_content_collection, make_content_episode, make_content_library, make_content_movie,
276		make_content_season, make_content_show,
277	};
278	use crate::entity::info::test::{
279		make_info_episode, make_info_movie, make_info_season, make_info_show,
280	};
281	use crate::tests::new_initialized_memory_db;
282
283	use super::super::tests::get_error_kind;
284	use super::super::tests::notsettable;
285	use super::test::{make_watched_episode, make_watched_movie};
286
287	#[tokio::test]
288	async fn use_test_macros() {
289		let db = new_initialized_memory_db().await;
290
291		make_info_movie!(&db, 1);
292		make_info_show!(&db, 1);
293		make_info_season!(&db, 1, 1);
294		make_info_episode!(&db, 1, 1, 1);
295
296		make_watched_movie!(&db, 1, 1);
297		make_watched_episode!(&db, 1, 1, 1, 1);
298	}
299
300	#[tokio::test]
301	async fn test_round_trip_movies() {
302		let db = new_initialized_memory_db().await;
303
304		macro_rules! assert_movie {
305			($db:expr, $id:literal, $uid:literal, Success $(; $($skip:ident),+)?) => {
306				let model = assert_movie!(@insert, $db, $id, $uid $(; $($skip),+)?)
307					.expect("insert");
308
309				assert_eq!(model.id, MovieId::from_raw($id));
310				assert_eq!(model.user_id, $uid);
311				assert_eq!(model.watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
312			};
313			($db:expr, $id:literal, $uid:literal, $error:ident $(; $($skip:ident),+)?) => {
314				let model = assert_movie!(@insert, $db, $id, $uid $(; $($skip),+)?)
315					.expect_err("insert");
316
317				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
318			};
319			(@insert, $db:expr, $id:literal, $uid:literal $(; $($skip:ident),+)?) => {
320				super::movies::ActiveModel {
321					id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?),
322					user_id: notsettable!(user_id, $uid $(, $($skip),+)?),
323					watched_date: notsettable!(watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
324				}.insert($db).await
325			};
326		}
327
328		assert_movie!(&db, 1, 1, ForeignKeyViolation);
329		make_info_movie!(&db, 1);
330		assert_movie!(&db, 1, 1, Success);
331		assert_movie!(&db, 1, 2, Success);
332
333		assert_movie!(&db, 1, 1, UniqueViolation);
334		make_info_movie!(&db, 2);
335		assert_movie!(&db, 2, 1, Success);
336		assert_movie!(&db, 2, 2, Success);
337
338		assert_movie!(&db, 3, 1, NotNullViolation; id);
339		assert_movie!(&db, 4, 1, NotNullViolation; user_id);
340		assert_movie!(&db, 5, 1, NotNullViolation; watched_date);
341	}
342
343	#[tokio::test]
344	async fn test_round_trip_episodes() {
345		let db = new_initialized_memory_db().await;
346
347		macro_rules! assert_episode {
348			($db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal, Success $(; $($skip:ident),+)?) => {
349				let model = assert_episode!(@insert, $db, $show, $season, $episode, $uid $(; $($skip),+)?)
350					.expect("insert");
351
352				assert_eq!(model.show_id, ShowId::from_raw($show));
353				assert_eq!(model.season_number, ::flix_model::numbers::SeasonNumber::new($season));
354				assert_eq!(model.episode_number, ::flix_model::numbers::EpisodeNumber::new($episode));
355				assert_eq!(model.user_id, $uid);
356				assert_eq!(model.watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
357			};
358			($db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal, $error:ident $(; $($skip:ident),+)?) => {
359				let model = assert_episode!(@insert, $db, $show, $season, $episode, $uid $(; $($skip),+)?)
360					.expect_err("insert");
361
362				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
363			};
364			(@insert, $db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal $(; $($skip:ident),+)?) => {
365				super::episodes::ActiveModel {
366					show_id: notsettable!(show_id, ShowId::from_raw($show) $(, $($skip),+)?),
367					season_number: notsettable!(season_number, ::flix_model::numbers::SeasonNumber::new($season) $(, $($skip),+)?),
368					episode_number: notsettable!(episode_number, ::flix_model::numbers::EpisodeNumber::new($episode) $(, $($skip),+)?),
369					user_id: notsettable!(user_id, $uid $(, $($skip),+)?),
370					watched_date: notsettable!(watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
371				}.insert($db).await
372			};
373		}
374
375		make_info_show!(&db, 1);
376		make_info_season!(&db, 1, 1);
377		make_info_show!(&db, 2);
378		make_info_season!(&db, 2, 1);
379
380		assert_episode!(&db, 1, 1, 1, 1, ForeignKeyViolation);
381		assert_episode!(&db, 2, 1, 1, 1, ForeignKeyViolation);
382		make_info_episode!(&db, 1, 1, 1);
383		make_info_episode!(&db, 2, 1, 1);
384		assert_episode!(&db, 1, 1, 1, 1, Success);
385		assert_episode!(&db, 1, 1, 1, 2, Success);
386		assert_episode!(&db, 2, 1, 1, 1, Success);
387
388		assert_episode!(&db, 1, 1, 1, 1, UniqueViolation);
389
390		assert_episode!(&db, 3, 1, 1, 1, NotNullViolation; show_id);
391		assert_episode!(&db, 4, 1, 1, 1, NotNullViolation; season_number);
392		assert_episode!(&db, 5, 1, 1, 1, NotNullViolation; episode_number);
393		assert_episode!(&db, 6, 1, 1, 1, NotNullViolation; user_id);
394		assert_episode!(&db, 7, 1, 1, 1, NotNullViolation; watched_date);
395	}
396
397	#[tokio::test]
398	async fn test_query_seasons() {
399		let db = new_initialized_memory_db().await;
400
401		macro_rules! assert_season {
402			($db:expr, $show:literal, $season:literal, $uid:literal, Watched) => {
403				assert_season!(@find, $db, $show, $season, $uid)
404					.ok_or(())
405					.expect("is none");
406			};
407			($db:expr, $show:literal, $season:literal, $uid:literal, Unwatched) => {
408				assert_season!(@find, $db, $show, $season, $uid)
409					.ok_or(())
410					.expect_err("is some");
411			};
412			(@find, $db:expr, $show:literal, $season:literal, $uid:literal) => {
413				super::seasons::Entity::find()
414					.filter(
415						Condition::all()
416							.add(super::seasons::Column::ShowId.eq($show))
417							.add(super::seasons::Column::SeasonNumber.eq($season))
418							.add(super::seasons::Column::UserId.eq($uid)),
419					)
420					.one(&db)
421					.await
422					.expect("find.filter.one")
423			};
424		}
425
426		make_content_library!(&db, 1);
427		make_content_show!(&db, 1, 1, None);
428
429		make_content_season!(&db, 1, 1, 1);
430		assert_season!(&db, 1, 1, 1, Unwatched);
431		make_content_episode!(&db, 1, 1, 1, 1);
432		assert_season!(&db, 1, 1, 1, Unwatched);
433		make_watched_episode!(&db, 1, 1, 1, 1);
434		assert_season!(&db, 1, 1, 1, Watched);
435		assert_season!(&db, 1, 1, 2, Unwatched);
436		make_content_episode!(&db, 1, 1, 1, 2);
437		assert_season!(&db, 1, 1, 1, Unwatched);
438
439		make_content_season!(&db, 1, 1, 2);
440		make_content_episode!(&db, 1, 1, 2, 1);
441		make_content_episode!(&db, 1, 1, 2, 2, >1);
442		make_info_episode!(&db, 1, 2, 3);
443		make_content_episode!(&db, 1, 1, 2, 4);
444		assert_season!(&db, 1, 2, 1, Unwatched);
445		assert_season!(&db, 1, 2, 2, Unwatched);
446		assert_season!(&db, 1, 2, 3, Unwatched);
447		make_watched_episode!(&db, 1, 2, 1, 1);
448		make_watched_episode!(&db, 1, 2, 1, 2);
449		make_watched_episode!(&db, 1, 2, 2, 3);
450		assert_season!(&db, 1, 2, 1, Unwatched);
451		assert_season!(&db, 1, 2, 2, Unwatched);
452		assert_season!(&db, 1, 2, 3, Unwatched);
453		make_watched_episode!(&db, 1, 2, 2, 1);
454		make_watched_episode!(&db, 1, 2, 2, 2);
455		make_watched_episode!(&db, 1, 2, 1, 3);
456		assert_season!(&db, 1, 2, 1, Unwatched);
457		assert_season!(&db, 1, 2, 2, Unwatched);
458		assert_season!(&db, 1, 2, 3, Unwatched);
459		make_watched_episode!(&db, 1, 2, 4, 1);
460		assert_season!(&db, 1, 2, 1, Watched);
461		assert_season!(&db, 1, 2, 2, Unwatched);
462		assert_season!(&db, 1, 2, 3, Unwatched);
463	}
464
465	#[tokio::test]
466	async fn test_query_shows() {
467		let db = new_initialized_memory_db().await;
468
469		macro_rules! assert_show {
470			($db:expr, $show:literal, $uid:literal, Watched) => {
471				assert_show!(@find, $db, $show, $uid)
472					.ok_or(())
473					.expect("is none");
474			};
475			($db:expr, $show:literal, $uid:literal, Unwatched) => {
476				assert_show!(@find, $db, $show, $uid)
477					.ok_or(())
478					.expect_err("is some");
479			};
480			(@find, $db:expr, $show:literal, $uid:literal) => {
481				super::shows::Entity::find()
482					.filter(
483						Condition::all()
484							.add(super::shows::Column::Id.eq($show))
485							.add(super::shows::Column::UserId.eq($uid)),
486					)
487					.one(&db)
488					.await
489					.expect("find.filter.one")
490			};
491		}
492
493		make_content_library!(&db, 1);
494		make_content_show!(&db, 1, 1, None);
495
496		assert_show!(&db, 1, 1, Unwatched);
497		make_content_season!(&db, 1, 1, 1);
498		assert_show!(&db, 1, 1, Unwatched);
499		make_content_episode!(&db, 1, 1, 1, 1);
500		assert_show!(&db, 1, 1, Unwatched);
501		make_watched_episode!(&db, 1, 1, 1, 1);
502		assert_show!(&db, 1, 1, Watched);
503		assert_show!(&db, 1, 2, Unwatched);
504		make_content_episode!(&db, 1, 1, 1, 2);
505		assert_show!(&db, 1, 1, Unwatched);
506		assert_show!(&db, 1, 2, Unwatched);
507		make_watched_episode!(&db, 1, 1, 2, 1);
508
509		make_content_season!(&db, 1, 1, 2);
510		assert_show!(&db, 1, 1, Unwatched);
511		assert_show!(&db, 1, 2, Unwatched);
512		make_content_episode!(&db, 1, 1, 2, 1);
513		assert_show!(&db, 1, 1, Unwatched);
514		assert_show!(&db, 1, 2, Unwatched);
515		make_watched_episode!(&db, 1, 2, 1, 1);
516		assert_show!(&db, 1, 1, Watched);
517		assert_show!(&db, 1, 2, Unwatched);
518	}
519
520	#[tokio::test]
521	async fn test_query_collections() {
522		let db = new_initialized_memory_db().await;
523
524		macro_rules! assert_collection {
525			($db:expr, $id:literal, $uid:literal, Watched) => {
526				assert_collection!(@find, $db, $id, $uid)
527					.ok_or(())
528					.expect("is none");
529			};
530			($db:expr, $id:literal, $uid:literal, Unwatched) => {
531				assert_collection!(@find, $db, $id, $uid)
532					.ok_or(())
533					.expect_err("is some");
534			};
535			(@find, $db:expr, $id:literal, $uid:literal) => {
536				super::collections::Entity::find()
537					.filter(
538						Condition::all()
539							.add(super::collections::Column::Id.eq($id))
540							.add(super::collections::Column::UserId.eq($uid)),
541					)
542					.one(&db)
543					.await
544					.expect("find.filter.one")
545			};
546		}
547
548		make_content_library!(&db, 1);
549		make_content_collection!(&db, 1, 1, None);
550		assert_collection!(&db, 1, 1, Unwatched);
551
552		make_content_movie!(&db, 1, 1, Some(1));
553		assert_collection!(&db, 1, 1, Unwatched);
554		make_info_movie!(&db, 9999);
555		make_watched_movie!(&db, 9999, 1);
556		assert_collection!(&db, 1, 1, Unwatched);
557		make_watched_movie!(&db, 1, 1);
558		assert_collection!(&db, 1, 1, Watched);
559
560		make_content_collection!(&db, 1, 2, Some(1));
561		assert_collection!(&db, 1, 1, Watched);
562		assert_collection!(&db, 2, 1, Unwatched);
563		make_content_movie!(&db, 1, 2, Some(2));
564		assert_collection!(&db, 1, 1, Unwatched);
565		assert_collection!(&db, 2, 1, Unwatched);
566		make_watched_movie!(&db, 2, 1);
567		assert_collection!(&db, 1, 1, Watched);
568		assert_collection!(&db, 2, 1, Watched);
569
570		make_content_show!(&db, 1, 1, Some(2));
571		assert_collection!(&db, 1, 1, Unwatched);
572		assert_collection!(&db, 2, 1, Unwatched);
573		make_content_season!(&db, 1, 1, 1);
574		make_content_episode!(&db, 1, 1, 1, 1);
575		make_watched_episode!(&db, 1, 1, 1, 1);
576		assert_collection!(&db, 1, 1, Watched);
577		assert_collection!(&db, 2, 1, Watched);
578	}
579}