1use std::{
4 collections::HashMap,
5 fmt::{self, Debug},
6 str::FromStr,
7};
8
9use anyhow::anyhow;
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
13use strum_macros::Display;
14use uuid::Uuid;
15
16use crate::{
17 api::endpoints::PathPart,
18 domain::{
19 category::CategoryId,
20 meta::{AffiliationId, AgeRangeId},
21 module::LiteModule,
22 },
23};
24
25use super::{
26 additional_resource::AdditionalResource,
27 course::{CourseId, CourseResponse},
28 jig::{JigId, JigResponse},
29 module::body::ThemeId,
30 playlist::{PlaylistId, PlaylistResponse},
31 resource::{ResourceId, ResourceResponse},
32 user::UserId,
33};
34
35#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Debug, Display)]
37#[serde(rename_all = "kebab-case")]
38#[strum(serialize_all = "kebab-case")]
39pub enum AssetType {
40 Jig,
42
43 Resource,
45
46 Playlist,
48
49 Course,
51}
52
53impl AssetType {
54 pub fn is_jig(&self) -> bool {
56 matches!(self, Self::Jig)
57 }
58
59 pub fn is_resource(&self) -> bool {
61 matches!(self, Self::Resource)
62 }
63
64 pub fn is_playlist(&self) -> bool {
66 matches!(self, Self::Playlist)
67 }
68
69 pub fn is_course(&self) -> bool {
71 matches!(self, Self::Course)
72 }
73
74 pub fn as_str(&self) -> &'static str {
76 match self {
77 Self::Jig => "jig",
78 Self::Resource => "resource",
79 Self::Playlist => "playlist",
80 Self::Course => "course",
81 }
82 }
83
84 pub fn display_name(&self) -> &'static str {
86 match self {
87 Self::Jig => "JIG",
88 Self::Resource => "resource",
89 Self::Playlist => "playlist",
90 Self::Course => "course",
91 }
92 }
93
94 pub fn display_name_capitalized(&self) -> &'static str {
96 match self {
97 Self::Jig => "JIG",
98 Self::Resource => "Resource",
99 Self::Playlist => "Playlist",
100 Self::Course => "course",
101 }
102 }
103
104 pub fn sidebar_header(&self) -> &'static str {
106 match self {
107 Self::Jig => "JIG",
108 Self::Resource => "resource",
109 Self::Playlist => "playlist",
110 Self::Course => "course",
111 }
112 }
113
114 pub fn to_asset_id(&self, uuid: Uuid) -> AssetId {
116 match self {
117 AssetType::Jig => JigId(uuid).into(),
118 AssetType::Playlist => PlaylistId(uuid).into(),
119 AssetType::Resource => ResourceId(uuid).into(),
120 AssetType::Course => CourseId(uuid).into(),
121 }
122 }
123}
124
125impl From<&AssetId> for AssetType {
126 fn from(asset_id: &AssetId) -> Self {
127 match asset_id {
128 AssetId::JigId(_) => AssetType::Jig,
129 AssetId::PlaylistId(_) => AssetType::Playlist,
130 AssetId::ResourceId(_) => AssetType::Resource,
131 AssetId::CourseId(_) => AssetType::Course,
132 }
133 }
134}
135
136impl TryFrom<&str> for AssetType {
137 type Error = ();
138
139 fn try_from(s: &str) -> Result<Self, Self::Error> {
140 match s {
141 "jig" => Ok(Self::Jig),
142 "playlist" => Ok(Self::Playlist),
143 "resource" => Ok(Self::Resource),
144 "course" => Ok(Self::Course),
145 _ => Err(()),
146 }
147 }
148}
149
150impl PathPart for AssetType {
151 fn get_path_string(&self) -> String {
152 self.as_str().to_string()
153 }
154}
155
156#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Debug)]
158#[serde(rename_all = "camelCase")]
159pub enum AssetId {
160 JigId(JigId),
162
163 PlaylistId(PlaylistId),
165
166 ResourceId(ResourceId),
168
169 CourseId(CourseId),
171}
172
173impl From<JigId> for AssetId {
174 fn from(jig_id: JigId) -> Self {
175 Self::JigId(jig_id)
176 }
177}
178
179impl From<PlaylistId> for AssetId {
180 fn from(playlist_id: PlaylistId) -> Self {
181 Self::PlaylistId(playlist_id)
182 }
183}
184
185impl From<ResourceId> for AssetId {
186 fn from(resource_id: ResourceId) -> Self {
187 Self::ResourceId(resource_id)
188 }
189}
190
191impl From<CourseId> for AssetId {
192 fn from(course_id: CourseId) -> Self {
193 Self::CourseId(course_id)
194 }
195}
196
197impl AssetId {
198 pub fn asset_type(&self) -> AssetType {
200 self.into()
201 }
202
203 pub fn unwrap_jig(&self) -> &JigId {
205 match self {
206 Self::JigId(jig_id) => jig_id,
207 _ => panic!(),
208 }
209 }
210
211 pub fn unwrap_playlist(&self) -> &PlaylistId {
213 match self {
214 Self::PlaylistId(playlist_id) => playlist_id,
215 _ => panic!(),
216 }
217 }
218
219 pub fn unwrap_resource(&self) -> &ResourceId {
221 match self {
222 Self::ResourceId(resource_id) => resource_id,
223 _ => panic!(),
224 }
225 }
226
227 pub fn unwrap_course(&self) -> &CourseId {
229 match self {
230 Self::CourseId(course_id) => course_id,
231 _ => panic!(),
232 }
233 }
234
235 pub fn uuid(&self) -> &Uuid {
237 match self {
238 Self::JigId(jig_id) => &jig_id.0,
239 Self::PlaylistId(playlist_id) => &playlist_id.0,
240 Self::ResourceId(resource_id) => &resource_id.0,
241 Self::CourseId(course_id) => &course_id.0,
242 }
243 }
244
245 pub fn is_jig_id(&self) -> bool {
247 matches!(self, Self::JigId(_))
248 }
249
250 pub fn is_playlist_id(&self) -> bool {
252 matches!(self, Self::PlaylistId(_))
253 }
254
255 pub fn is_resource_id(&self) -> bool {
257 matches!(self, Self::ResourceId(_))
258 }
259
260 pub fn is_course_id(&self) -> bool {
262 matches!(self, Self::CourseId(_))
263 }
264}
265
266#[derive(Clone, Serialize, Deserialize, Debug)]
268#[serde(rename_all = "camelCase")]
269pub enum Asset {
270 Jig(JigResponse),
272
273 Playlist(PlaylistResponse),
275
276 Resource(ResourceResponse),
278
279 Course(CourseResponse),
281}
282
283impl From<JigResponse> for Asset {
284 fn from(jig: JigResponse) -> Self {
285 Self::Jig(jig)
286 }
287}
288
289impl From<PlaylistResponse> for Asset {
290 fn from(playlist: PlaylistResponse) -> Self {
291 Self::Playlist(playlist)
292 }
293}
294
295impl From<ResourceResponse> for Asset {
296 fn from(resource: ResourceResponse) -> Self {
297 Self::Resource(resource)
298 }
299}
300
301impl From<CourseResponse> for Asset {
302 fn from(course: CourseResponse) -> Self {
303 Self::Course(course)
304 }
305}
306
307impl Asset {
308 pub fn asset_type(&self) -> AssetType {
310 (&self.id()).into()
311 }
312
313 pub fn unwrap_jig(&self) -> &JigResponse {
315 match self {
316 Self::Jig(jig) => jig,
317 _ => panic!(),
318 }
319 }
320
321 pub fn unwrap_resource(&self) -> &ResourceResponse {
323 match self {
324 Self::Resource(resource) => resource,
325 _ => panic!(),
326 }
327 }
328
329 pub fn unwrap_playlist(&self) -> &PlaylistResponse {
331 match self {
332 Self::Playlist(playlist) => playlist,
333 _ => panic!(),
334 }
335 }
336
337 pub fn unwrap_course(&self) -> &CourseResponse {
339 match self {
340 Self::Course(course) => course,
341 _ => panic!(),
342 }
343 }
344
345 pub fn is_jig(&self) -> bool {
347 matches!(self, Self::Jig(_))
348 }
349
350 pub fn is_playlist(&self) -> bool {
352 matches!(self, Self::Playlist(_))
353 }
354
355 pub fn is_resource(&self) -> bool {
357 matches!(self, Self::Resource(_))
358 }
359
360 pub fn is_course(&self) -> bool {
362 matches!(self, Self::Course(_))
363 }
364
365 pub fn id(&self) -> AssetId {
367 match self {
368 Self::Jig(jig) => jig.id.into(),
369 Self::Playlist(playlist) => playlist.id.into(),
370 Self::Resource(resource) => resource.id.into(),
371 Self::Course(course) => course.id.into(),
372 }
373 }
374
375 pub fn published_at(&self) -> Option<DateTime<Utc>> {
377 match self {
378 Self::Jig(jig) => jig.published_at,
379 Self::Playlist(playlist) => playlist.published_at,
380 Self::Resource(resource) => resource.published_at,
381 Self::Course(course) => course.published_at,
382 }
383 }
384
385 pub fn display_name(&self) -> &String {
387 match self {
388 Self::Jig(jig) => &jig.jig_data.display_name,
389 Self::Playlist(playlist) => &playlist.playlist_data.display_name,
390 Self::Resource(resource) => &resource.resource_data.display_name,
391 Self::Course(course) => &course.course_data.display_name,
392 }
393 }
394
395 pub fn language(&self) -> &String {
397 match self {
398 Self::Jig(jig) => &jig.jig_data.language,
399 Self::Playlist(playlist) => &playlist.playlist_data.language,
400 Self::Resource(resource) => &resource.resource_data.language,
401 Self::Course(course) => &course.course_data.language,
402 }
403 }
404
405 pub fn description(&self) -> &String {
407 match self {
408 Self::Jig(jig) => &jig.jig_data.description,
409 Self::Playlist(playlist) => &playlist.playlist_data.description,
410 Self::Resource(resource) => &resource.resource_data.description,
411 Self::Course(course) => &course.course_data.description,
412 }
413 }
414
415 pub fn cover(&self) -> Option<&LiteModule> {
417 match self {
418 Self::Jig(jig) => jig.jig_data.modules.first(),
419 Self::Playlist(playlist) => playlist.playlist_data.cover.as_ref(),
420 Self::Resource(resource) => resource.resource_data.cover.as_ref(),
421 Self::Course(course) => course.course_data.cover.as_ref(),
422 }
423 }
424
425 pub fn privacy_level(&self) -> &PrivacyLevel {
427 match self {
428 Self::Jig(jig) => &jig.jig_data.privacy_level,
429 Self::Playlist(playlist) => &playlist.playlist_data.privacy_level,
430 Self::Resource(resource) => &resource.resource_data.privacy_level,
431 Self::Course(course) => &course.course_data.privacy_level,
432 }
433 }
434
435 pub fn other_keywords(&self) -> &String {
437 match self {
438 Self::Jig(jig) => &jig.jig_data.other_keywords,
439 Self::Playlist(playlist) => &playlist.playlist_data.other_keywords,
440 Self::Resource(resource) => &resource.resource_data.other_keywords,
441 Self::Course(course) => &course.course_data.other_keywords,
442 }
443 }
444
445 pub fn translated_keywords(&self) -> &String {
447 match self {
448 Self::Jig(jig) => &jig.jig_data.translated_keywords,
449 Self::Playlist(playlist) => &playlist.playlist_data.translated_keywords,
450 Self::Resource(resource) => &resource.resource_data.translated_keywords,
451 Self::Course(course) => &course.course_data.translated_keywords,
452 }
453 }
454
455 pub fn age_ranges(&self) -> &Vec<AgeRangeId> {
457 match self {
458 Self::Jig(jig) => &jig.jig_data.age_ranges,
459 Self::Playlist(playlist) => &playlist.playlist_data.age_ranges,
460 Self::Resource(resource) => &resource.resource_data.age_ranges,
461 Self::Course(_) => unimplemented!(),
462 }
463 }
464
465 pub fn affiliations(&self) -> &Vec<AffiliationId> {
467 match self {
468 Self::Jig(jig) => &jig.jig_data.affiliations,
469 Self::Playlist(playlist) => &playlist.playlist_data.affiliations,
470 Self::Resource(resource) => &resource.resource_data.affiliations,
471 Self::Course(_) => panic!(),
472 }
473 }
474
475 pub fn categories(&self) -> &Vec<CategoryId> {
477 match self {
478 Self::Jig(jig) => &jig.jig_data.categories,
479 Self::Playlist(playlist) => &playlist.playlist_data.categories,
480 Self::Resource(resource) => &resource.resource_data.categories,
481 Self::Course(course) => &course.course_data.categories,
482 }
483 }
484
485 pub fn likes(&self) -> i64 {
487 match self {
488 Self::Jig(jig) => jig.likes,
489 Self::Playlist(playlist) => playlist.likes,
490 Self::Resource(resource) => resource.likes,
491 Self::Course(course) => course.likes,
492 }
493 }
494
495 pub fn is_liked(&self) -> bool {
497 match self {
498 Self::Jig(jig) => jig.is_liked,
499 Self::Playlist(playlist) => playlist.is_liked,
500 Self::Resource(resource) => resource.is_liked,
501 Self::Course(_course) => todo!(),
502 }
503 }
504
505 pub fn plays(&self) -> i64 {
507 match self {
508 Self::Jig(jig) => jig.plays,
509 Self::Playlist(playlist) => playlist.plays,
510 Self::Resource(resource) => resource.views,
511 Self::Course(course) => course.plays,
512 }
513 }
514
515 pub fn author_id(&self) -> &Option<UserId> {
517 match self {
518 Self::Jig(jig) => &jig.author_id,
519 Self::Playlist(playlist) => &playlist.author_id,
520 Self::Resource(resource) => &resource.author_id,
521 Self::Course(course) => &course.author_id,
522 }
523 }
524
525 pub fn author_name(&self) -> &Option<String> {
527 match self {
528 Self::Jig(jig) => &jig.author_name,
529 Self::Playlist(playlist) => &playlist.author_name,
530 Self::Resource(resource) => &resource.author_name,
531 Self::Course(course) => &course.author_name,
532 }
533 }
534
535 pub fn additional_resources(&self) -> &Vec<AdditionalResource> {
537 match self {
538 Self::Jig(jig) => &jig.jig_data.additional_resources,
539 Self::Playlist(playlist) => &playlist.playlist_data.additional_resources,
540 Self::Resource(resource) => &resource.resource_data.additional_resources,
541 Self::Course(course) => &course.course_data.additional_resources,
542 }
543 }
544
545 pub fn translated_description(&self) -> &HashMap<String, String> {
547 match self {
548 Self::Jig(jig) => &jig.jig_data.translated_description,
549 Self::Playlist(playlist) => &playlist.playlist_data.translated_description,
550 Self::Resource(resource) => &resource.resource_data.translated_description,
551 Self::Course(course) => &course.course_data.translated_description,
552 }
553 }
554
555 pub fn theme(&self) -> ThemeId {
557 match self {
558 Self::Jig(jig) => jig.jig_data.theme,
559 Self::Playlist(_) => ThemeId::default(),
560 Self::Resource(_) => ThemeId::default(),
561 Self::Course(_) => ThemeId::default(),
562 }
563 }
564
565 pub fn live_up_to_date(&self) -> bool {
567 match self {
568 Self::Jig(jig) => jig.live_up_to_date,
569 Self::Playlist(playlist) => playlist.live_up_to_date,
570 Self::Resource(resource) => resource.live_up_to_date,
571 Self::Course(course) => course.live_up_to_date,
572 }
573 }
574
575 pub fn premium(&self) -> bool {
577 match self {
578 Self::Jig(asset) => asset.admin_data.premium,
579 Self::Playlist(asset) => asset.admin_data.premium,
580 Self::Resource(asset) => asset.admin_data.premium,
581 Self::Course(asset) => asset.admin_data.premium,
582 }
583 }
584}
585
586#[derive(Clone, Eq, PartialEq, Debug)]
590pub enum UserOrMe {
591 Me,
593
594 User(UserId),
596}
597
598impl From<UserId> for UserOrMe {
599 fn from(user_id: UserId) -> Self {
600 UserOrMe::User(user_id)
601 }
602}
603
604impl serde::Serialize for UserOrMe {
605 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
606 where
607 S: serde::Serializer,
608 {
609 match self {
610 UserOrMe::Me => serializer.serialize_str("me"),
611 UserOrMe::User(id) => serializer.collect_str(&id),
612 }
613 }
614}
615
616impl<'de> serde::Deserialize<'de> for UserOrMe {
617 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
618 where
619 D: serde::Deserializer<'de>,
620 {
621 struct Visitor;
622
623 impl<'de> serde::de::Visitor<'de> for Visitor {
624 type Value = UserOrMe;
625
626 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
627 formatter.write_str("`me` or `<uuid>`")
628 }
629
630 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
631 where
632 E: serde::de::Error,
633 {
634 if value == "me" {
635 Ok(UserOrMe::Me)
636 } else {
637 Uuid::from_str(value)
638 .map(|id| UserOrMe::User(UserId(id)))
639 .map_err(|e| E::custom(format!("failed to parse id: {}", e)))
640 }
641 }
642 }
643
644 deserializer.deserialize_str(Visitor)
645 }
646}
647
648#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug, Display)]
650#[cfg_attr(feature = "backend", derive(sqlx::Type))]
651#[serde(rename_all = "camelCase")]
652#[repr(i16)]
653pub enum OrderBy {
654 #[strum(serialize = "Created")]
656 CreatedAt = 0,
657
658 #[strum(serialize = "Published")]
660 PublishedAt = 1,
661}
662
663#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
665#[cfg_attr(feature = "backend", derive(sqlx::Type))]
666#[serde(rename_all = "camelCase")]
667#[repr(i16)]
668pub enum PrivacyLevel {
669 Public = 0,
671
672 Unlisted = 1,
674
675 Private = 2,
677}
678
679impl PrivacyLevel {
680 pub fn as_str(&self) -> &'static str {
682 match self {
683 Self::Public => "public",
684 Self::Unlisted => "unlisted",
685 Self::Private => "private",
686 }
687 }
688}
689
690impl FromStr for PrivacyLevel {
691 type Err = anyhow::Error;
692
693 fn from_str(s: &str) -> Result<Self, Self::Err> {
694 Ok(match s {
695 "public" => Self::Public,
696 "unlisted" => Self::Unlisted,
697 "private" => Self::Private,
698 _ => return Err(anyhow!("invalid")),
699 })
700 }
701}
702
703impl Default for PrivacyLevel {
704 fn default() -> Self {
705 Self::Public
706 }
707}
708
709#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
711#[cfg_attr(feature = "backend", derive(sqlx::Type))]
712#[serde(rename_all = "camelCase")]
713#[repr(i16)]
714pub enum DraftOrLive {
715 Draft = 0,
717 Live = 1,
719}
720
721impl Default for DraftOrLive {
722 fn default() -> Self {
723 Self::Live
724 }
725}
726
727impl DraftOrLive {
728 pub fn draft() -> Self {
730 Self::Draft
731 }
732
733 pub fn live() -> Self {
735 Self::Live
736 }
737
738 pub fn is_live(&self) -> bool {
748 matches!(*self, DraftOrLive::Live)
749 }
750
751 pub fn is_draft(&self) -> bool {
761 !self.is_live()
762 }
763
764 pub fn as_str(&self) -> &'static str {
766 match self {
767 DraftOrLive::Draft => "draft",
768 DraftOrLive::Live => "live",
769 }
770 }
771}
772
773impl FromStr for DraftOrLive {
774 type Err = String;
775 fn from_str(s: &str) -> Result<Self, Self::Err> {
776 match s {
777 "draft" => Ok(Self::Draft),
778 "live" => Ok(Self::Live),
779 s => Err(format!("Can't create DraftFroLive from {:?}", s)),
780 }
781 }
782}
783
784impl From<DraftOrLive> for bool {
785 fn from(draft_or_live: DraftOrLive) -> Self {
786 match draft_or_live {
787 DraftOrLive::Draft => false,
788 DraftOrLive::Live => true,
789 }
790 }
791}
792
793impl From<bool> for DraftOrLive {
794 fn from(draft_or_live: bool) -> Self {
795 match draft_or_live {
796 false => DraftOrLive::Draft,
797 true => DraftOrLive::Live,
798 }
799 }
800}