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