1use std::fmt::Formatter;
2
3use std::fmt::Display;
4
5#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8#[cfg_attr(
9 feature = "rkyv",
10 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
11)]
12#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq, Hash)))]
13pub enum ImageSize {
14 Poster(PosterSize), Backdrop(BackdropSize), Thumbnail(EpisodeSize), Profile(ProfileSize), }
19
20#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
21#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
22#[cfg_attr(
23 feature = "sqlx",
24 sqlx(type_name = "image_variant", rename_all = "lowercase")
25)]
26pub enum ImageVariant {
27 Poster,
28 Backdrop,
29 Thumbnail,
30 Profile,
31}
32
33impl Display for ImageVariant {
34 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
35 match self {
36 ImageVariant::Poster => write!(f, "poster"),
37 ImageVariant::Backdrop => write!(f, "backdrop"),
38 ImageVariant::Thumbnail => write!(f, "thumbnail"),
39 ImageVariant::Profile => write!(f, "profile"),
40 }
41 }
42}
43
44impl ImageVariant {
45 #[inline]
46 fn as_str(&self) -> &'static str {
47 match self {
48 ImageVariant::Poster => "poster",
49 ImageVariant::Backdrop => "backdrop",
50 ImageVariant::Thumbnail => "thumbnail",
51 ImageVariant::Profile => "profile",
52 }
53 }
54
55 #[inline]
56 fn as_str_path(&self) -> &'static str {
57 match self {
58 ImageVariant::Poster => "/poster/",
59 ImageVariant::Backdrop => "/backdrop/",
60 ImageVariant::Thumbnail => "/thumbnail/",
61 ImageVariant::Profile => "/profile/",
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
67#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
68#[cfg_attr(
69 feature = "sqlx",
70 sqlx(type_name = "size_variant", rename_all = "lowercase")
71)]
72pub enum SqlxImageSizeVariant {
73 Original,
74 Resized,
75 Tmdb,
76}
77
78impl Display for ImageSize {
79 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80 match self {
81 ImageSize::Thumbnail(s) => {
82 write!(f, "Thumbnail (size: {:#?})", s)
83 }
84 ImageSize::Poster(s) => write!(f, "Poster (size: {:#?})", s),
85 ImageSize::Backdrop(s) => {
86 write!(f, "Backdrop (size: {:#?})", s)
87 }
88 ImageSize::Profile(s) => write!(f, "Profile (size: {:#?})", s),
89 }
90 }
91}
92
93impl ImageSize {
94 pub const fn thumbnail() -> Self {
97 Self::Thumbnail(EpisodeSize::W512)
98 }
99
100 pub const fn poster() -> Self {
102 Self::Poster(PosterSize::W342)
103 }
104
105 pub const fn poster_large() -> Self {
107 Self::Poster(PosterSize::W780)
108 }
109
110 pub const fn backdrop() -> Self {
112 Self::Backdrop(BackdropSize::Original(None))
113 }
114
115 pub const fn profile() -> Self {
117 Self::Profile(ProfileSize::W185)
118 }
119
120 pub fn to_nearest_tmdb_size(self, original_width: ImageSize) -> Self {
122 match self {
123 ImageSize::Poster(poster_size) => match poster_size {
124 PosterSize::CustomResized(w) => {
125 for size in PosterSize::ALL.iter() {
126 if let PosterSize::Original(None) = size {
127 return original_width;
128 }
129 if w <= size.width_unchecked() {
130 return ImageSize::Poster(*size);
131 }
132 }
133 ImageSize::Poster(PosterSize::default())
134 }
135 PosterSize::Original(Some(_)) => self,
136 PosterSize::Original(None) => {
137 ImageSize::Poster(PosterSize::default())
138 }
139 _ => self,
140 },
141 ImageSize::Backdrop(backdrop_size) => match backdrop_size {
142 BackdropSize::CustomResized(w) => {
143 for size in BackdropSize::ALL.iter() {
144 if let BackdropSize::Original(None) = size {
145 return original_width;
146 }
147 if w <= size.width_unchecked() {
148 return ImageSize::Backdrop(*size);
149 }
150 }
151 ImageSize::Backdrop(BackdropSize::default())
152 }
153 BackdropSize::Original(Some(_)) => self,
154 BackdropSize::Original(None) => {
155 ImageSize::Backdrop(BackdropSize::default())
156 }
157 _ => self,
158 },
159 ImageSize::Thumbnail(episode_size) => match episode_size {
160 EpisodeSize::CustomResized(w) => {
161 for size in EpisodeSize::ALL.iter() {
162 if let EpisodeSize::Original(None) = size {
163 return original_width;
164 }
165 if w <= size.width_unchecked() {
166 return ImageSize::Thumbnail(*size);
167 }
168 }
169 ImageSize::Thumbnail(EpisodeSize::default())
170 }
171 EpisodeSize::Original(Some(_)) => self,
172 EpisodeSize::Original(None) => {
173 ImageSize::Thumbnail(EpisodeSize::default())
174 }
175 _ => self,
176 },
177 ImageSize::Profile(profile_size) => match profile_size {
178 ProfileSize::CustomResized(w) => {
179 for size in ProfileSize::ALL.iter() {
180 if let ProfileSize::Original(None) = size {
181 return original_width;
182 }
183 if w <= size.width_unchecked() {
184 return ImageSize::Profile(*size);
185 }
186 }
187 ImageSize::Profile(ProfileSize::default())
188 }
189 ProfileSize::Original(Some(_)) => self,
190 ProfileSize::Original(None) => {
191 ImageSize::Profile(ProfileSize::default())
192 }
193 _ => self,
194 },
195 }
196 }
197
198 pub const fn original(width: u32, image_variant: ImageVariant) -> Self {
199 match image_variant {
200 ImageVariant::Poster => {
201 ImageSize::Poster(PosterSize::Original(Some(width)))
202 }
203 ImageVariant::Backdrop => {
204 ImageSize::Backdrop(BackdropSize::Original(Some(width)))
205 }
206 ImageVariant::Thumbnail => {
207 ImageSize::Thumbnail(EpisodeSize::Original(Some(width)))
208 }
209 ImageVariant::Profile => {
210 ImageSize::Profile(ProfileSize::Original(Some(width)))
211 }
212 }
213 }
214
215 pub const fn original_unknown(image_variant: ImageVariant) -> Self {
216 match image_variant {
217 ImageVariant::Poster => {
218 ImageSize::Poster(PosterSize::Original(None))
219 }
220 ImageVariant::Backdrop => {
221 ImageSize::Backdrop(BackdropSize::Original(None))
222 }
223 ImageVariant::Thumbnail => {
224 ImageSize::Thumbnail(EpisodeSize::Original(None))
225 }
226 ImageVariant::Profile => {
227 ImageSize::Profile(ProfileSize::Original(None))
228 }
229 }
230 }
231
232 pub const fn is_original(self) -> bool {
233 match self {
234 ImageSize::Poster(poster_size) => {
235 matches!(poster_size, PosterSize::Original(_))
236 }
237 ImageSize::Backdrop(backdrop_size) => {
238 matches!(backdrop_size, BackdropSize::Original(_))
239 }
240 ImageSize::Thumbnail(episode_size) => {
241 matches!(episode_size, EpisodeSize::Original(_))
242 }
243 ImageSize::Profile(profile_size) => {
244 matches!(profile_size, ProfileSize::Original(_))
245 }
246 }
247 }
248
249 pub const fn custom(width: u32, image_variant: ImageVariant) -> Self {
250 match image_variant {
251 ImageVariant::Poster => {
252 ImageSize::Poster(PosterSize::CustomResized(width))
253 }
254 ImageVariant::Backdrop => {
255 ImageSize::Backdrop(BackdropSize::CustomResized(width))
256 }
257 ImageVariant::Thumbnail => {
258 ImageSize::Thumbnail(EpisodeSize::CustomResized(width))
259 }
260 ImageVariant::Profile => {
261 ImageSize::Profile(ProfileSize::CustomResized(width))
262 }
263 }
264 }
265
266 pub const fn is_resized(self) -> bool {
267 match self {
268 ImageSize::Poster(poster_size) => {
269 matches!(poster_size, PosterSize::CustomResized(_))
270 }
271 ImageSize::Backdrop(backdrop_size) => {
272 matches!(backdrop_size, BackdropSize::CustomResized(_))
273 }
274 ImageSize::Thumbnail(episode_size) => {
275 matches!(episode_size, EpisodeSize::CustomResized(_))
276 }
277 ImageSize::Profile(profile_size) => {
278 matches!(profile_size, ProfileSize::CustomResized(_))
279 }
280 }
281 }
282
283 pub fn from_size_and_variant(width: u32, variant: ImageVariant) -> Self {
284 match variant {
285 ImageVariant::Poster => {
286 ImageSize::Poster(PosterSize::from_width(width))
287 }
288 ImageVariant::Backdrop => {
289 ImageSize::Backdrop(BackdropSize::from_width(width))
290 }
291 ImageVariant::Thumbnail => {
292 ImageSize::Thumbnail(EpisodeSize::from_width(width))
293 }
294 ImageVariant::Profile => {
295 ImageSize::Profile(ProfileSize::from_width(width))
296 }
297 }
298 }
299
300 pub fn sqlx_image_size_variant(self) -> SqlxImageSizeVariant {
301 match self {
302 ImageSize::Poster(s) => s.sqlx_image_size_variant(),
303 ImageSize::Backdrop(s) => s.sqlx_image_size_variant(),
304 ImageSize::Thumbnail(s) => s.sqlx_image_size_variant(),
305 ImageSize::Profile(s) => s.sqlx_image_size_variant(),
306 }
307 }
308
309 pub fn dimensions(&self) -> Option<(u32, u32)> {
310 match self {
311 ImageSize::Poster(s) => s.dimensions(),
312 ImageSize::Backdrop(s) => s.dimensions(),
313 ImageSize::Thumbnail(s) => s.dimensions(),
314 ImageSize::Profile(s) => s.dimensions(),
315 }
316 }
317
318 pub fn dimensions_unchecked(&self) -> (u32, u32) {
320 self.dimensions().unwrap_or_else(|| {
321 panic!(
322 "dimensions_unchecked called for ImageSize with no dimensions available: {self:?}"
323 )
324 })
325 }
326
327 pub fn has_width(&self) -> bool {
328 !match self {
329 ImageSize::Poster(s) => {
330 matches!(s, PosterSize::Original(None))
331 }
332 ImageSize::Backdrop(s) => {
333 matches!(s, BackdropSize::Original(None))
334 }
335 ImageSize::Thumbnail(s) => {
336 matches!(s, EpisodeSize::Original(None))
337 }
338 ImageSize::Profile(s) => {
339 matches!(s, ProfileSize::Original(None))
340 }
341 }
342 }
343
344 pub fn image_variant(&self) -> ImageVariant {
345 match self {
346 ImageSize::Poster(_) => ImageVariant::Poster,
347 ImageSize::Backdrop(_) => ImageVariant::Backdrop,
348 ImageSize::Thumbnail(_) => ImageVariant::Thumbnail,
349 ImageSize::Profile(_) => ImageVariant::Profile,
350 }
351 }
352
353 #[inline]
354 pub fn image_variant_str(&self) -> &'static str {
355 match self {
356 ImageSize::Poster(_) => ImageVariant::Poster.as_str(),
357 ImageSize::Backdrop(_) => ImageVariant::Backdrop.as_str(),
358 ImageSize::Thumbnail(_) => ImageVariant::Thumbnail.as_str(),
359 ImageSize::Profile(_) => ImageVariant::Profile.as_str(),
360 }
361 }
362
363 #[inline]
364 pub fn image_variant_str_path(&self) -> &'static str {
365 match self {
366 ImageSize::Poster(_) => ImageVariant::Poster.as_str_path(),
367 ImageSize::Backdrop(_) => ImageVariant::Backdrop.as_str_path(),
368 ImageSize::Thumbnail(_) => ImageVariant::Thumbnail.as_str_path(),
369 ImageSize::Profile(_) => ImageVariant::Profile.as_str_path(),
370 }
371 }
372
373 #[inline]
375 pub fn to_tmdb_param(&self) -> &'static str {
376 match self {
377 ImageSize::Thumbnail(s) => s.to_tmdb_param(),
378 ImageSize::Poster(s) => s.to_tmdb_param(),
379 ImageSize::Backdrop(s) => s.to_tmdb_param(),
380 ImageSize::Profile(s) => s.to_tmdb_param(),
381 }
382 }
383
384 pub fn width(&self) -> Option<u32> {
386 match self {
387 ImageSize::Thumbnail(s) => s.width(),
388 ImageSize::Poster(s) => s.width(),
389 ImageSize::Backdrop(s) => s.width(),
390 ImageSize::Profile(s) => s.width(),
391 }
392 }
393
394 pub const fn width_unchecked(&self) -> u32 {
397 match self {
398 ImageSize::Thumbnail(s) => s.width_unchecked(),
399 ImageSize::Poster(s) => s.width_unchecked(),
400 ImageSize::Backdrop(s) => s.width_unchecked(),
401 ImageSize::Profile(s) => s.width_unchecked(),
402 }
403 }
404
405 pub fn width_name(&self) -> String {
406 match self {
407 ImageSize::Thumbnail(s) => s.width_name(),
408 ImageSize::Poster(s) => s.width_name(),
409 ImageSize::Backdrop(s) => s.width_name(),
410 ImageSize::Profile(s) => s.width_name(),
411 }
412 }
413
414 #[inline]
415 pub fn width_name_str(&self) -> &'static str {
416 match self {
417 ImageSize::Thumbnail(s) => s.width_name_str(),
418 ImageSize::Poster(s) => s.width_name_str(),
419 ImageSize::Backdrop(s) => s.width_name_str(),
420 ImageSize::Profile(s) => s.width_name_str(),
421 }
422 }
423}
424
425#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, Default)]
427#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
428#[cfg_attr(
429 feature = "rkyv",
430 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
431)]
432#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq, Hash)))]
433pub enum EpisodeSize {
434 W256,
435 #[default]
436 W512,
437 W768,
438 CustomResized(u32),
440 Original(Option<u32>),
441}
442
443impl EpisodeSize {
444 pub const ALL: [EpisodeSize; 3] = [Self::W256, Self::W512, Self::W768];
445
446 pub fn from_width(w: u32) -> Self {
447 match w {
448 256 => Self::W256,
449 512 => Self::W512,
450 768 => Self::W768,
451 w => Self::Original(Some(w)),
452 }
453 }
454
455 pub fn height_from_width(w: u32) -> u32 {
456 (w.saturating_mul(9).saturating_add(8)) / 16
460 }
461
462 pub fn dimensions(self) -> Option<(u32, u32)> {
463 match self {
464 EpisodeSize::W256 => Some((256, 144)),
465 EpisodeSize::W512 => Some((512, 288)),
466 EpisodeSize::W768 => Some((768, 432)),
467 EpisodeSize::CustomResized(w) => {
468 Some((w, Self::height_from_width(w)))
469 }
470 EpisodeSize::Original(w) => {
471 w.map(|w| (w, Self::height_from_width(w)))
472 }
473 }
474 }
475
476 pub const fn width(&self) -> Option<u32> {
477 match self {
478 Self::W256 => Some(256),
479 Self::W512 => Some(512),
480 Self::W768 => Some(768),
481 Self::CustomResized(w) => Some(*w),
482 Self::Original(Some(w)) => Some(*w),
483 Self::Original(None) => None,
484 }
485 }
486 pub const fn width_unchecked(&self) -> u32 {
489 match self {
490 Self::W256 => 256,
491 Self::W512 => 512,
492 Self::W768 => 768,
493 Self::CustomResized(w) => *w,
494 Self::Original(Some(w)) => *w,
495 Self::Original(None) => panic!(
496 "Width not available for Original variant with no width included"
497 ),
498 }
499 }
500 pub fn width_name(&self) -> String {
501 match self {
502 Self::CustomResized(_) => "custom".to_string(),
503 Self::Original(Some(_)) => "original".to_string(),
504 Self::Original(None) => "original".to_string(),
505 _ => self.to_tmdb_param().to_string(),
506 }
507 }
508
509 #[inline]
510 pub fn width_name_str(&self) -> &'static str {
511 match self {
512 Self::CustomResized(_) => "custom",
513 Self::Original(Some(_)) => "original",
514 Self::Original(None) => "original",
515 _ => self.to_tmdb_param(),
516 }
517 }
518
519 pub fn sqlx_image_size_variant(&self) -> SqlxImageSizeVariant {
520 match self {
521 Self::CustomResized(_) => SqlxImageSizeVariant::Resized,
522 Self::Original(_) => SqlxImageSizeVariant::Original,
523 _ => SqlxImageSizeVariant::Tmdb,
524 }
525 }
526
527 #[inline]
528 pub const fn to_tmdb_param(&self) -> &'static str {
529 match self {
530 Self::W256 => "w256",
531 Self::W512 => "w512",
532 Self::W768 => "w768",
533 Self::CustomResized(_) => "invalid",
534 Self::Original(_) => "original",
535 }
536 }
537}
538
539impl Display for EpisodeSize {
540 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
541 match self {
542 Self::W256 => write!(f, "256px"),
543 Self::W512 => write!(f, "512px"),
544 Self::W768 => write!(f, "768px"),
545 Self::CustomResized(w) => write!(f, "{}px", w),
546 Self::Original(_) => write!(f, "Original"),
547 }
548 }
549}
550
551#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
556#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
557#[cfg_attr(
558 feature = "rkyv",
559 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
560)]
561#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq, Hash)))]
562pub enum PosterSize {
563 W92,
565 W154,
567 W185,
569 W342,
571 W500,
573 W780,
575 CustomResized(u32),
577 Original(Option<u32>),
579}
580
581impl Default for PosterSize {
582 fn default() -> Self {
583 PosterSize::Original(None)
584 }
585}
586
587impl PosterSize {
588 pub const ALL: [PosterSize; 6] = [
590 Self::W92,
591 Self::W154,
592 Self::W185,
593 Self::W342,
594 Self::W500,
595 Self::W780,
596 ];
597
598 pub fn from_width(w: u32) -> Self {
599 match w {
600 92 => Self::W92,
601 154 => Self::W154,
602 185 => Self::W185,
603 342 => Self::W342,
604 500 => Self::W500,
605 780 => Self::W780,
606 w => Self::CustomResized(w),
607 }
608 }
609
610 pub fn original(w: u32) -> Self {
611 Self::Original(Some(w))
612 }
613
614 pub fn height_from_width(w: u32) -> u32 {
615 (w / 2) * 3
616 }
617
618 pub fn dimensions(self) -> Option<(u32, u32)> {
620 match self {
621 PosterSize::W92 => Some((92, 138)),
622 PosterSize::W154 => Some((154, 231)),
623 PosterSize::W185 => Some((185, 277)),
624 PosterSize::W342 => Some((342, 513)),
625 PosterSize::W500 => Some((500, 750)),
626 PosterSize::W780 => Some((780, 1170)),
627 PosterSize::CustomResized(w) => Some((w, (w / 2) * 3)),
628 PosterSize::Original(w) => w.map(|w| (w, (w / 2) * 3)),
629 }
630 }
631
632 pub const fn width(&self) -> Option<u32> {
634 match self {
635 Self::W92 => Some(92),
636 Self::W154 => Some(154),
637 Self::W185 => Some(185),
638 Self::W342 => Some(342),
639 Self::W500 => Some(500),
640 Self::W780 => Some(780),
641 Self::CustomResized(w) => Some(*w),
642 Self::Original(Some(w)) => Some(*w),
643 Self::Original(None) => None,
644 }
645 }
646
647 pub const fn width_unchecked(&self) -> u32 {
650 match self {
651 Self::W92 => 92,
652 Self::W154 => 154,
653 Self::W185 => 185,
654 Self::W342 => 342,
655 Self::W500 => 500,
656 Self::W780 => 780,
657 Self::CustomResized(w) => *w,
658 Self::Original(Some(w)) => *w,
659 Self::Original(None) => panic!(
660 "Width not available for Original variant with no width included"
661 ),
662 }
663 }
664
665 pub fn width_name(&self) -> String {
666 match self {
667 Self::CustomResized(_) => "custom".to_string(),
668 Self::Original(Some(_)) => "original".to_string(),
669 Self::Original(None) => "original".to_string(),
670 _ => self.to_tmdb_param().to_string(),
671 }
672 }
673
674 #[inline]
675 pub fn width_name_str(&self) -> &'static str {
676 match self {
677 Self::CustomResized(_) => "custom",
678 Self::Original(Some(_)) => "original",
679 Self::Original(None) => "original",
680 _ => self.to_tmdb_param(),
681 }
682 }
683
684 pub fn sqlx_image_size_variant(&self) -> SqlxImageSizeVariant {
685 match self {
686 Self::CustomResized(_) => SqlxImageSizeVariant::Resized,
687 Self::Original(_) => SqlxImageSizeVariant::Original,
688 _ => SqlxImageSizeVariant::Tmdb,
689 }
690 }
691
692 #[inline]
694 pub const fn to_tmdb_param(&self) -> &'static str {
695 match self {
696 Self::W92 => "w92",
697 Self::W154 => "w154",
698 Self::W185 => "w185",
699 Self::W342 => "w342",
700 Self::W500 => "w500",
701 Self::W780 => "w780",
702 Self::CustomResized(_width) => "invalid",
703 Self::Original(_) => "original",
704 }
705 }
706}
707
708impl Display for PosterSize {
709 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
710 match self {
711 Self::W92 => write!(f, "92px"),
712 Self::W154 => write!(f, "154px"),
713 Self::W185 => write!(f, "185px"),
714 Self::W342 => write!(f, "342px"),
715 Self::W500 => write!(f, "500px"),
716 Self::W780 => write!(f, "780px"),
717 Self::CustomResized(w) => write!(f, "{}px", w),
718 Self::Original(_) => write!(f, "Original"),
719 }
720 }
721}
722
723#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
725#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
726#[cfg_attr(
727 feature = "rkyv",
728 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
729)]
730#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq, Hash)))]
731pub enum BackdropSize {
732 W300,
733 W780,
734 W1280,
735 CustomResized(u32),
737 Original(Option<u32>),
738}
739impl Default for BackdropSize {
740 fn default() -> Self {
741 BackdropSize::Original(None)
742 }
743}
744
745impl BackdropSize {
746 pub const ALL: [BackdropSize; 3] = [Self::W300, Self::W780, Self::W1280];
747
748 pub fn from_width(w: u32) -> Self {
749 match w {
750 300 => Self::W300,
751 780 => Self::W780,
752 1280 => Self::W1280,
753 w => Self::Original(Some(w)),
754 }
755 }
756
757 pub fn original(w: u32) -> Self {
758 Self::Original(Some(w))
759 }
760
761 pub fn height_from_width(w: u32) -> u32 {
764 (w.saturating_mul(9).saturating_add(8)) / 16
766 }
767
768 pub fn dimensions(self) -> Option<(u32, u32)> {
769 match self {
770 Self::W300 => Some((300, 169)),
771 Self::W780 => Some((780, 439)),
772 Self::W1280 => Some((1280, 720)),
773 Self::CustomResized(w) => Some((w, Self::height_from_width(w))),
774 Self::Original(w) => w.map(|w| (w, Self::height_from_width(w))),
775 }
776 }
777
778 pub const fn width(&self) -> Option<u32> {
779 match self {
780 Self::W300 => Some(300),
781 Self::W780 => Some(780),
782 Self::W1280 => Some(1280),
783 Self::CustomResized(w) => Some(*w),
784 Self::Original(Some(w)) => Some(*w),
785 Self::Original(None) => None,
786 }
787 }
788 pub const fn width_unchecked(&self) -> u32 {
791 match self {
792 Self::W300 => 300,
793 Self::W780 => 780,
794 Self::W1280 => 1280,
795 Self::CustomResized(w) => *w,
796 Self::Original(Some(w)) => *w,
797 Self::Original(None) => panic!(
798 "Width not available for Original variant with no width included"
799 ),
800 }
801 }
802
803 pub fn width_name(&self) -> String {
804 match self {
805 Self::CustomResized(_) => "custom".to_string(),
806 Self::Original(Some(_)) => "original".to_string(),
807 Self::Original(None) => "original".to_string(),
808 _ => self.to_tmdb_param().to_string(),
809 }
810 }
811
812 #[inline]
813 pub fn width_name_str(&self) -> &'static str {
814 match self {
815 Self::CustomResized(_) => "custom",
816 Self::Original(Some(_)) => "original",
817 Self::Original(None) => "original",
818 _ => self.to_tmdb_param(),
819 }
820 }
821
822 pub fn sqlx_image_size_variant(&self) -> SqlxImageSizeVariant {
823 match self {
824 Self::CustomResized(_) => SqlxImageSizeVariant::Resized,
825 Self::Original(_) => SqlxImageSizeVariant::Original,
826 _ => SqlxImageSizeVariant::Tmdb,
827 }
828 }
829
830 #[inline]
831 pub const fn to_tmdb_param(&self) -> &'static str {
832 match self {
833 Self::W300 => "w300",
834 Self::W780 => "w780",
835 Self::W1280 => "w1280",
836 Self::CustomResized(_) => "invalid",
837 Self::Original(_) => "original",
838 }
839 }
840}
841
842impl Display for BackdropSize {
843 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
844 match self {
845 Self::W300 => write!(f, "300px"),
846 Self::W780 => write!(f, "780px"),
847 Self::W1280 => write!(f, "1280px"),
848 Self::CustomResized(w) => write!(f, "Custom({}px)", w),
849 Self::Original(Some(w)) => write!(f, "Original({}px)", w),
850 Self::Original(None) => write!(f, "Original"),
851 }
852 }
853}
854
855#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, Default)]
857#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
858#[cfg_attr(
859 feature = "rkyv",
860 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
861)]
862#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq, Hash)))]
863pub enum ProfileSize {
864 W45,
865 #[default]
866 W185,
867 W632,
868 CustomResized(u32),
870 Original(Option<u32>),
871}
872
873impl ProfileSize {
874 pub const ALL: [ProfileSize; 3] = [Self::W45, Self::W185, Self::W632];
875
876 pub fn from_width(w: u32) -> Self {
877 match w {
878 45 => Self::W45,
879 185 => Self::W185,
880 632 => Self::W632,
881 w => Self::Original(Some(w)),
882 }
883 }
884
885 pub fn height_from_width(w: u32) -> u32 {
888 (w / 2) * 3
889 }
890
891 pub fn dimensions(self) -> Option<(u32, u32)> {
892 match self {
893 ProfileSize::W45 => Some((45, 138)),
894 ProfileSize::W185 => Some((185, 270)),
895 ProfileSize::W632 => Some((632, 450)),
896 ProfileSize::CustomResized(w) => Some((w, (w / 2) * 3)),
897 ProfileSize::Original(w) => w.map(|w| (w, (w / 2) * 3)),
898 }
899 }
900
901 pub const fn width(&self) -> Option<u32> {
902 match self {
903 Self::W45 => Some(45),
904 Self::W185 => Some(185),
905 Self::W632 => Some(632),
906 Self::CustomResized(w) => Some(*w),
907 Self::Original(Some(w)) => Some(*w),
908 Self::Original(None) => None,
909 }
910 }
911 pub const fn width_unchecked(&self) -> u32 {
914 match self {
915 Self::W45 => 45,
916 Self::W185 => 185,
917 Self::W632 => 632,
918 Self::CustomResized(w) => *w,
919 Self::Original(Some(w)) => *w,
920 Self::Original(None) => panic!(
921 "Width not available for Original variant with no width included"
922 ),
923 }
924 }
925
926 pub fn width_name(&self) -> String {
927 match self {
928 ProfileSize::W45 | ProfileSize::W185 | ProfileSize::W632 => {
929 self.to_tmdb_param().to_string()
930 }
931 ProfileSize::CustomResized(_) => "custom".to_string(),
932 ProfileSize::Original(Some(_)) => "original".to_string(),
933 ProfileSize::Original(None) => "original".to_string(),
934 }
935 }
936
937 #[inline]
938 pub fn width_name_str(&self) -> &'static str {
939 match self {
940 Self::CustomResized(_) => "custom",
941 Self::Original(Some(_)) => "original",
942 Self::Original(None) => "original",
943 _ => self.to_tmdb_param(),
944 }
945 }
946
947 pub fn sqlx_image_size_variant(&self) -> SqlxImageSizeVariant {
948 match self {
949 Self::CustomResized(_) => SqlxImageSizeVariant::Resized,
950 Self::Original(_) => SqlxImageSizeVariant::Original,
951 _ => SqlxImageSizeVariant::Tmdb,
952 }
953 }
954
955 #[inline]
956 pub const fn to_tmdb_param(&self) -> &'static str {
957 match self {
958 Self::W45 => "w45",
959 Self::W185 => "w185",
960 Self::W632 => "w632",
961 Self::CustomResized(_) => "invalid",
962 Self::Original(_) => "original",
963 }
964 }
965}
966
967impl Display for ProfileSize {
968 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
969 match self {
970 Self::W45 => write!(f, "45px"),
971 Self::W185 => write!(f, "185px"),
972 Self::W632 => write!(f, "632px"),
973 Self::CustomResized(w) => write!(f, "{}px", w),
974 Self::Original(w) => {
975 if let Some(w) = w {
976 write!(f, "{}px", w)
977 } else {
978 write!(f, "Original")
979 }
980 }
981 }
982 }
983}
984
985#[cfg(test)]
986mod tests {
987 use super::*;
988
989 #[test]
990 fn backdrop_height_from_width_matches_known_tmdb_sizes() {
991 assert_eq!(BackdropSize::height_from_width(300), 169);
992 assert_eq!(BackdropSize::height_from_width(780), 439);
993 assert_eq!(BackdropSize::height_from_width(1280), 720);
994 }
995
996 #[test]
997 fn episode_height_from_width_matches_known_tmdb_sizes() {
998 assert_eq!(EpisodeSize::height_from_width(256), 144);
999 assert_eq!(EpisodeSize::height_from_width(512), 288);
1000 assert_eq!(EpisodeSize::height_from_width(768), 432);
1001 }
1002
1003 #[test]
1004 fn image_size_dimensions_unchecked_is_available_for_fixed_sizes() {
1005 let samples = [
1006 ImageSize::poster(),
1007 ImageSize::poster_large(),
1008 ImageSize::Backdrop(BackdropSize::W780),
1009 ImageSize::Thumbnail(EpisodeSize::W512),
1010 ImageSize::Profile(ProfileSize::W185),
1011 ];
1012
1013 for imz in samples {
1014 let dims = imz.dimensions_unchecked();
1015 assert!(dims.0 > 0);
1016 assert!(dims.1 > 0);
1017 }
1018 }
1019}