1use serde::{Deserialize, Serialize};
2
3#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
5#[serde(tag = "type", rename_all = "lowercase")]
6pub enum RepoError {
7 Network { message: String },
8 Authentication { message: String },
9 NotFound { message: String },
10 Server { message: String },
11 Database { message: String },
12 Offline,
13}
14
15impl std::fmt::Display for RepoError {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 match self {
18 RepoError::Network { message } => write!(f, "Network error: {}", message),
19 RepoError::Authentication { message } => write!(f, "Authentication error: {}", message),
20 RepoError::NotFound { message } => write!(f, "Not found: {}", message),
21 RepoError::Server { message } => write!(f, "Server error: {}", message),
22 RepoError::Database { message } => write!(f, "Database error: {}", message),
23 RepoError::Offline => write!(f, "Offline - no server connection"),
24 }
25 }
26}
27
28impl std::error::Error for RepoError {}
29
30#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct Library {
34 pub id: String,
35 pub name: String,
36 pub collection_type: String,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub image_tag: Option<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub favorites_scope: Option<SearchScope>,
50}
51
52impl Library {
53 pub fn new(
57 id: String,
58 name: String,
59 collection_type: String,
60 image_tag: Option<String>,
61 ) -> Self {
62 let favorites_scope = SearchScope::for_collection_type(&collection_type);
63 Self {
64 id,
65 name,
66 collection_type,
67 image_tag,
68 favorites_scope,
69 }
70 }
71}
72
73#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
75#[serde(rename_all = "camelCase")]
76pub struct UserData {
77 #[serde(skip_serializing_if = "Option::is_none")]
81 pub playback_position_ticks: Option<i64>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub playback_position_ms: Option<i64>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub is_played: Option<bool>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub is_favorite: Option<bool>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub play_count: Option<i32>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub last_played_date: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub playback_context_type: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub playback_context_id: Option<String>,
99}
100
101#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103#[serde(rename_all = "camelCase")]
104pub struct ArtistItem {
105 #[serde(alias = "Id")]
106 pub id: String,
107 #[serde(alias = "Name")]
108 pub name: String,
109}
110
111#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
113#[serde(rename_all = "camelCase")]
114pub struct Person {
115 #[serde(alias = "Id")]
117 #[serde(default)]
118 pub id: String,
119 #[serde(alias = "Name")]
121 #[serde(default)]
122 pub name: String,
123 #[serde(rename = "type")]
126 #[serde(alias = "Type")]
127 #[serde(default)]
128 pub person_type: String,
129 #[serde(alias = "Role")]
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub role: Option<String>,
133 #[serde(alias = "PrimaryImageTag")]
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub primary_image_tag: Option<String>,
137}
138
139#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
141#[serde(rename_all = "camelCase")]
142pub struct MediaItem {
143 pub id: String,
144 pub name: String,
145 #[serde(rename = "type")]
152 pub item_type: String,
153 #[serde(default)]
157 pub kind: crate::domain::MediaKind,
158 #[serde(default)]
161 pub is_folder: bool,
162 pub server_id: String,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub parent_id: Option<String>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub library_id: Option<String>,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub overview: Option<String>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub genres: Option<Vec<String>>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub production_year: Option<i32>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub premiere_date: Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub community_rating: Option<f64>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub official_rating: Option<String>,
181 #[serde(skip_serializing_if = "Option::is_none")]
185 #[serde(rename = "runTimeTicks")]
186 pub runtime_ticks: Option<i64>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub duration_ms: Option<i64>,
191 #[serde(skip_serializing_if = "Option::is_none")]
194 pub primary_image_tag: Option<String>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub image_id: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub backdrop_image_tags: Option<Vec<String>>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub parent_backdrop_image_tags: Option<Vec<String>>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub album_id: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub album_name: Option<String>,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub album_artist: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub artists: Option<Vec<String>>,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub artist_items: Option<Vec<ArtistItem>>,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub index_number: Option<i32>,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub parent_index_number: Option<i32>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub series_id: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub series_name: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub season_id: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub season_name: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub user_data: Option<UserData>,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub media_streams: Option<Vec<MediaStream>>,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub media_sources: Option<Vec<MediaSource>>,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub people: Option<Vec<Person>>,
234}
235
236#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct MediaStream {
240 #[serde(rename = "type")]
243 pub stream_type: String,
244 #[serde(default)]
246 pub kind: crate::domain::StreamKind,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub codec: Option<String>,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub language: Option<String>,
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub display_title: Option<String>,
253 pub index: i32,
254 pub is_default: bool,
255 pub is_forced: bool,
256 #[serde(default)]
267 pub supports_external_delivery: Option<bool>,
268}
269
270#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct MediaSource {
274 pub id: String,
275 pub name: String,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub container: Option<String>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub size: Option<i64>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub bitrate: Option<i32>,
282 pub supports_direct_play: bool,
283 pub supports_direct_stream: bool,
284 pub supports_transcoding: bool,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub direct_stream_url: Option<String>,
287}
288
289#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct SearchResult {
293 pub items: Vec<MediaItem>,
294 pub total_record_count: usize,
295}
296
297#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
305#[serde(rename_all = "camelCase")]
306pub struct DownloadDiskUsage {
307 pub sizes: std::collections::HashMap<String, i64>,
309 pub partial_containers: std::collections::HashMap<String, bool>,
313 pub device_total_bytes: i64,
315 pub item_count: u32,
317}
318
319#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
321#[serde(rename_all = "camelCase")]
322pub struct GetItemsOptions {
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub start_index: Option<usize>,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub limit: Option<usize>,
327 #[serde(skip_serializing_if = "Option::is_none")]
328 pub sort_by: Option<String>,
329 #[serde(skip_serializing_if = "Option::is_none")]
330 pub sort_order: Option<String>,
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub include_item_types: Option<Vec<String>>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub recursive: Option<bool>,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub fields: Option<Vec<String>>,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub genres: Option<Vec<String>>,
339 #[serde(skip_serializing_if = "Option::is_none")]
344 pub favorites_only: Option<bool>,
345}
346
347#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub enum SearchScope {
359 All,
360 Music,
361 Movies,
362 Tv,
363}
364
365impl SearchScope {
366 pub fn item_types(self) -> Option<Vec<String>> {
375 match self {
376 SearchScope::All => None,
377 SearchScope::Music => Some(
378 ["MusicAlbum", "MusicArtist", "Audio", "Playlist"]
379 .into_iter()
380 .map(String::from)
381 .collect(),
382 ),
383 SearchScope::Movies => Some(vec!["Movie".to_string()]),
384 SearchScope::Tv => Some(
385 ["Series", "Episode"]
386 .into_iter()
387 .map(String::from)
388 .collect(),
389 ),
390 }
391 }
392
393 pub fn for_collection_type(collection_type: &str) -> Option<SearchScope> {
406 match collection_type {
407 "movies" => Some(SearchScope::Movies),
408 "tvshows" => Some(SearchScope::Tv),
409 "music" => Some(SearchScope::Music),
410 _ => None,
411 }
412 }
413}
414
415#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
417#[serde(rename_all = "camelCase")]
418pub struct SearchOptions {
419 #[serde(skip_serializing_if = "Option::is_none")]
420 pub limit: Option<usize>,
421 #[serde(skip_serializing_if = "Option::is_none")]
422 pub include_item_types: Option<Vec<String>>,
423 #[serde(skip_serializing_if = "Option::is_none")]
424 pub search_term: Option<String>,
425 #[serde(skip_serializing_if = "Option::is_none")]
429 pub scope: Option<SearchScope>,
430}
431
432impl SearchOptions {
433 pub fn resolve_scope(&mut self) {
441 if let Some(scope) = self.scope {
442 self.include_item_types = scope.item_types();
445 }
446 }
447}
448
449#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
451#[serde(rename_all = "camelCase")]
452pub struct PlaybackInfo {
453 pub media_source_id: String,
454 pub play_session_id: String,
455 pub stream_url: String,
456 pub direct_play: bool,
457 pub needs_transcoding: bool,
458}
459
460#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct LiveStreamInfo {
468 pub stream_url: String,
469 pub play_session_id: Option<String>,
470 pub live_stream_id: Option<String>,
471 pub media_source_id: Option<String>,
472}
473
474#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct Genre {
478 pub id: String,
479 pub name: String,
480 pub album_count: Option<u32>,
484}
485
486#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
488pub enum ImageType {
489 Primary,
490 Backdrop,
491 Banner,
492 Thumb,
493 Logo,
494}
495
496impl ImageType {
497 pub fn as_str(&self) -> &str {
498 match self {
499 ImageType::Primary => "Primary",
500 ImageType::Backdrop => "Backdrop",
501 ImageType::Banner => "Banner",
502 ImageType::Thumb => "Thumb",
503 ImageType::Logo => "Logo",
504 }
505 }
506}
507
508#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
510#[serde(rename_all = "camelCase")]
511pub struct ImageOptions {
512 #[serde(skip_serializing_if = "Option::is_none")]
513 pub max_width: Option<u32>,
514 #[serde(skip_serializing_if = "Option::is_none")]
515 pub max_height: Option<u32>,
516 #[serde(skip_serializing_if = "Option::is_none")]
517 pub quality: Option<u32>,
518 #[serde(skip_serializing_if = "Option::is_none")]
519 pub tag: Option<String>,
520}
521
522pub trait MeaningfulContent {
524 fn has_content(&self) -> bool;
525}
526
527impl MeaningfulContent for Vec<Library> {
528 fn has_content(&self) -> bool {
529 !self.is_empty()
530 }
531}
532
533impl MeaningfulContent for Vec<MediaItem> {
534 fn has_content(&self) -> bool {
535 !self.is_empty()
536 }
537}
538
539impl MeaningfulContent for SearchResult {
540 fn has_content(&self) -> bool {
541 !self.items.is_empty()
542 }
543}
544
545impl MeaningfulContent for MediaItem {
546 fn has_content(&self) -> bool {
547 true }
549}
550
551impl MeaningfulContent for Vec<Genre> {
552 fn has_content(&self) -> bool {
553 !self.is_empty()
554 }
555}
556
557impl MeaningfulContent for PlaybackInfo {
558 fn has_content(&self) -> bool {
559 !self.stream_url.is_empty()
560 }
561}
562
563#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
568#[serde(rename_all = "camelCase")]
569pub struct PlaylistEntry {
570 pub playlist_item_id: String,
572 #[serde(flatten)]
574 pub item: MediaItem,
575}
576
577#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
581#[serde(rename_all = "camelCase")]
582pub struct PlaylistCreatedResult {
583 pub id: String,
584}
585
586impl MeaningfulContent for Vec<PlaylistEntry> {
587 fn has_content(&self) -> bool {
588 !self.is_empty()
589 }
590}
591
592impl MeaningfulContent for PlaylistCreatedResult {
593 fn has_content(&self) -> bool {
594 !self.id.is_empty()
595 }
596}
597
598#[cfg(test)]
599mod search_scope_tests {
600 use super::*;
601
602 #[test]
610 fn music_scope_expands_to_music_item_types() {
611 assert_eq!(
612 SearchScope::Music.item_types(),
613 Some(vec![
614 "MusicAlbum".to_string(),
615 "MusicArtist".to_string(),
616 "Audio".to_string(),
617 "Playlist".to_string(),
618 ])
619 );
620 }
621
622 #[test]
624 fn movies_scope_expands_to_movie_only() {
625 assert_eq!(
626 SearchScope::Movies.item_types(),
627 Some(vec!["Movie".to_string()])
628 );
629 }
630
631 #[test]
633 fn tv_scope_expands_to_series_and_episode() {
634 assert_eq!(
635 SearchScope::Tv.item_types(),
636 Some(vec!["Series".to_string(), "Episode".to_string()])
637 );
638 }
639
640 #[test]
648 fn all_scope_sends_no_filter() {
649 assert_eq!(SearchScope::All.item_types(), None);
650 }
651
652 #[test]
656 fn resolve_scope_overrides_include_item_types() {
657 let mut options = SearchOptions {
658 include_item_types: Some(vec!["Movie".to_string()]),
659 scope: Some(SearchScope::Music),
660 ..Default::default()
661 };
662 options.resolve_scope();
663
664 assert_eq!(
665 options.include_item_types,
666 Some(vec![
667 "MusicAlbum".to_string(),
668 "MusicArtist".to_string(),
669 "Audio".to_string(),
670 "Playlist".to_string(),
671 ])
672 );
673 }
674
675 #[test]
679 fn resolve_all_scope_clears_include_item_types() {
680 let mut options = SearchOptions {
681 include_item_types: Some(vec!["Movie".to_string()]),
682 scope: Some(SearchScope::All),
683 ..Default::default()
684 };
685 options.resolve_scope();
686
687 assert_eq!(options.include_item_types, None);
688 }
689
690 #[test]
695 fn resolve_without_scope_preserves_include_item_types() {
696 let mut options = SearchOptions {
697 include_item_types: Some(vec!["MusicAlbum".to_string()]),
698 scope: None,
699 ..Default::default()
700 };
701 options.resolve_scope();
702
703 assert_eq!(
704 options.include_item_types,
705 Some(vec!["MusicAlbum".to_string()])
706 );
707 }
708
709 #[test]
713 fn scope_deserializes_from_camel_case() {
714 let options: SearchOptions =
715 serde_json::from_str(r#"{"scope": "music", "limit": 10}"#).unwrap();
716 assert!(matches!(options.scope, Some(SearchScope::Music)));
717
718 let all: SearchOptions = serde_json::from_str(r#"{"scope": "all"}"#).unwrap();
719 assert!(matches!(all.scope, Some(SearchScope::All)));
720 }
721
722 #[test]
724 fn test_collection_type_maps_to_its_favorites_scope() {
725 assert_eq!(
726 SearchScope::for_collection_type("movies"),
727 Some(SearchScope::Movies)
728 );
729 assert_eq!(
730 SearchScope::for_collection_type("tvshows"),
731 Some(SearchScope::Tv)
732 );
733 assert_eq!(
734 SearchScope::for_collection_type("music"),
735 Some(SearchScope::Music)
736 );
737 }
738
739 #[test]
745 fn test_uncategorised_collection_types_have_no_favorites_scope() {
746 for collection_type in ["livetv", "channels", "boxsets", "books", "unknown", ""] {
747 assert_eq!(
748 SearchScope::for_collection_type(collection_type),
749 None,
750 "{collection_type} should not carry a favourites scope"
751 );
752 }
753 }
754
755 #[test]
757 fn test_library_carries_its_favorites_scope_to_the_frontend() {
758 let music = Library::new("1".into(), "Music".into(), "music".into(), None);
759 assert_eq!(music.favorites_scope, Some(SearchScope::Music));
760
761 let json = serde_json::to_value(&music).unwrap();
762 assert_eq!(json["favoritesScope"], "music");
763
764 let livetv = Library::new("2".into(), "Live TV".into(), "livetv".into(), None);
766 let json = serde_json::to_value(&livetv).unwrap();
767 assert!(json.get("favoritesScope").is_none());
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774
775 #[test]
776 fn test_artist_item_deserialize_pascal_case() {
777 let json = r#"{"Id": "artist123", "Name": "Bob Dylan"}"#;
779 let result: Result<ArtistItem, _> = serde_json::from_str(json);
780
781 assert!(result.is_ok());
782 let artist = result.unwrap();
783 assert_eq!(artist.id, "artist123");
784 assert_eq!(artist.name, "Bob Dylan");
785 }
786
787 #[test]
788 fn test_artist_item_deserialize_array() {
789 let json = r#"[
791 {"Id": "artist1", "Name": "Bob Dylan"},
792 {"Id": "artist2", "Name": "Johnny Cash"}
793 ]"#;
794 let result: Result<Vec<ArtistItem>, _> = serde_json::from_str(json);
795
796 assert!(result.is_ok());
797 let artists = result.unwrap();
798 assert_eq!(artists.len(), 2);
799 assert_eq!(artists[0].id, "artist1");
800 assert_eq!(artists[0].name, "Bob Dylan");
801 assert_eq!(artists[1].id, "artist2");
802 assert_eq!(artists[1].name, "Johnny Cash");
803 }
804
805 #[test]
806 fn test_artist_item_serialize() {
807 let artist = ArtistItem {
810 id: "test-id".to_string(),
811 name: "Test Artist".to_string(),
812 };
813
814 let json = serde_json::to_string(&artist).expect("Failed to serialize");
815 assert!(json.contains(r#""id":"test-id""#));
816 assert!(json.contains(r#""name":"Test Artist""#));
817
818 let from_pascal: ArtistItem =
819 serde_json::from_str(r#"{"Id":"x","Name":"Y"}"#).expect("Failed to deserialize");
820 assert_eq!(from_pascal.id, "x");
821 assert_eq!(from_pascal.name, "Y");
822 }
823
824 #[test]
825 fn test_media_item_with_primary_image_tag() {
826 let json = r#"{
828 "id": "item123",
829 "name": "Test Item",
830 "type": "MusicAlbum",
831 "serverId": "server1",
832 "primaryImageTag": "tag123"
833 }"#;
834
835 let result: Result<MediaItem, _> = serde_json::from_str(json);
836 assert!(result.is_ok());
837
838 let item = result.unwrap();
839 assert_eq!(item.id, "item123");
840 assert_eq!(item.name, "Test Item");
841 assert_eq!(item.primary_image_tag, Some("tag123".to_string()));
842 }
843
844 #[test]
845 fn test_media_item_with_artists() {
846 let json = r#"{
848 "id": "track1",
849 "name": "Test Track",
850 "type": "Audio",
851 "serverId": "server1",
852 "artists": ["Artist 1", "Artist 2"]
853 }"#;
854
855 let result: Result<MediaItem, _> = serde_json::from_str(json);
856 assert!(result.is_ok());
857
858 let item = result.unwrap();
859 let artists = item.artists.expect("Expected artists");
860 assert_eq!(artists.len(), 2);
861 assert_eq!(artists[0], "Artist 1");
862 assert_eq!(artists[1], "Artist 2");
863 }
864
865 #[test]
866 fn test_search_result_meaningful_content() {
867 let empty_result = SearchResult {
869 items: vec![],
870 total_record_count: 0,
871 };
872 assert!(!empty_result.has_content());
873
874 let non_empty_result = SearchResult {
875 items: vec![MediaItem {
876 id: "1".to_string(),
877 name: "Test".to_string(),
878 item_type: "Audio".to_string(),
879 kind: crate::domain::MediaKind::Track,
880 is_folder: false,
881 server_id: "server1".to_string(),
882 parent_id: None,
883 library_id: None,
884 overview: None,
885 genres: None,
886 production_year: None,
887 premiere_date: None,
888 community_rating: None,
889 official_rating: None,
890 runtime_ticks: None,
891 duration_ms: None,
892 primary_image_tag: None,
893 image_id: None,
894 backdrop_image_tags: None,
895 parent_backdrop_image_tags: None,
896 album_id: None,
897 album_name: None,
898 album_artist: None,
899 artists: None,
900 artist_items: None,
901 index_number: None,
902 parent_index_number: None,
903 series_id: None,
904 series_name: None,
905 season_id: None,
906 season_name: None,
907 user_data: None,
908 media_streams: None,
909 media_sources: None,
910 people: None,
911 }],
912 total_record_count: 1,
913 };
914 assert!(non_empty_result.has_content());
915 }
916
917 #[test]
918 fn test_person_deserialize_complete() {
919 let json = r#"{
921 "Id": "person123",
922 "Name": "Tom Hanks",
923 "Type": "Actor",
924 "Role": "Lead Actor",
925 "PrimaryImageTag": "tag456"
926 }"#;
927
928 let result: Result<Person, _> = serde_json::from_str(json);
929 assert!(result.is_ok(), "Failed to deserialize: {:?}", result.err());
930
931 let person = result.unwrap();
932 assert_eq!(person.id, "person123");
933 assert_eq!(person.name, "Tom Hanks");
934 assert_eq!(person.person_type, "Actor");
935 assert_eq!(person.role, Some("Lead Actor".to_string()));
936 assert_eq!(person.primary_image_tag, Some("tag456".to_string()));
937
938 let serialized = serde_json::to_string(&person).expect("Failed to serialize");
940 assert!(
941 serialized.contains(r#""type":"Actor""#),
942 "Serialized form should use 'type' not 'Type'"
943 );
944 assert!(serialized.contains(r#""id":"person123""#));
945 assert!(serialized.contains(r#""primaryImageTag":"tag456""#));
946 }
947
948 #[test]
949 fn test_person_deserialize_minimal() {
950 let json = r#"{
952 "Id": "person456",
953 "Name": "Meryl Streep",
954 "Type": "Actress"
955 }"#;
956
957 let result: Result<Person, _> = serde_json::from_str(json);
958 assert!(result.is_ok());
959
960 let person = result.unwrap();
961 assert_eq!(person.id, "person456");
962 assert_eq!(person.name, "Meryl Streep");
963 assert_eq!(person.person_type, "Actress");
964 assert_eq!(person.role, None);
965 assert_eq!(person.primary_image_tag, None);
966 }
967
968 #[test]
969 fn test_person_array_deserialize() {
970 let json = r#"[
972 {"Id": "actor1", "Name": "Actor One", "Type": "Actor"},
973 {"Id": "director1", "Name": "Director One", "Type": "Director", "Role": "Director"}
974 ]"#;
975
976 let result: Result<Vec<Person>, _> = serde_json::from_str(json);
977 assert!(result.is_ok());
978
979 let people = result.unwrap();
980 assert_eq!(people.len(), 2);
981 assert_eq!(people[0].name, "Actor One");
982 assert_eq!(people[1].person_type, "Director");
983 assert_eq!(people[1].role, Some("Director".to_string()));
984 }
985
986 #[test]
987 fn test_media_item_with_people() {
988 let json = r#"{
990 "id": "movie1",
991 "name": "Test Movie",
992 "type": "Movie",
993 "serverId": "server1",
994 "people": [
995 {"Id": "actor1", "Name": "John Doe", "Type": "Actor"},
996 {"Id": "director1", "Name": "Jane Smith", "Type": "Director"}
997 ]
998 }"#;
999
1000 let result: Result<MediaItem, _> = serde_json::from_str(json);
1001 assert!(result.is_ok());
1002
1003 let item = result.unwrap();
1004 let people = item.people.as_ref().expect("Expected people array");
1005 assert_eq!(people.len(), 2);
1006 assert_eq!(people[0].name, "John Doe");
1007 assert_eq!(people[0].person_type, "Actor");
1008 assert_eq!(people[1].person_type, "Director");
1009
1010 let serialized = serde_json::to_string(&item).expect("Failed to serialize");
1012 let re_parsed: serde_json::Value =
1013 serde_json::from_str(&serialized).expect("Failed to parse serialized");
1014 let people_array = re_parsed["people"]
1015 .as_array()
1016 .expect("people should be array");
1017 assert!(
1018 people_array[0].get("type").is_some(),
1019 "Serialized person should have 'type' field"
1020 );
1021 assert_eq!(people_array[0]["type"].as_str().unwrap(), "Actor");
1022 }
1023
1024 #[test]
1025 fn test_playlist_entry_serialization() {
1026 let entry = PlaylistEntry {
1027 playlist_item_id: "entry-abc-123".to_string(),
1028 item: MediaItem {
1029 id: "track1".to_string(),
1030 name: "Test Track".to_string(),
1031 item_type: "Audio".to_string(),
1032 kind: crate::domain::MediaKind::Track,
1033 is_folder: false,
1034 server_id: "server1".to_string(),
1035 parent_id: None,
1036 library_id: None,
1037 overview: None,
1038 genres: None,
1039 production_year: None,
1040 premiere_date: None,
1041 community_rating: None,
1042 official_rating: None,
1043 runtime_ticks: None,
1044 duration_ms: None,
1045 primary_image_tag: None,
1046 image_id: None,
1047 backdrop_image_tags: None,
1048 parent_backdrop_image_tags: None,
1049 album_id: None,
1050 album_name: None,
1051 album_artist: None,
1052 artists: Some(vec!["Artist One".to_string()]),
1053 artist_items: None,
1054 index_number: None,
1055 parent_index_number: None,
1056 series_id: None,
1057 series_name: None,
1058 season_id: None,
1059 season_name: None,
1060 user_data: None,
1061 media_streams: None,
1062 media_sources: None,
1063 people: None,
1064 },
1065 };
1066
1067 let json = serde_json::to_string(&entry).expect("Failed to serialize");
1068 assert!(json.contains(r#""playlistItemId":"entry-abc-123""#));
1070 assert!(json.contains(r#""id":"track1""#));
1072 assert!(json.contains(r#""name":"Test Track""#));
1073 assert!(json.contains(r#""type":"Audio""#));
1074 }
1075
1076 #[test]
1077 fn test_playlist_created_result_serialization() {
1078 let result = PlaylistCreatedResult {
1079 id: "playlist-new-123".to_string(),
1080 };
1081 let json = serde_json::to_string(&result).expect("Failed to serialize");
1082 assert!(json.contains(r#""id":"playlist-new-123""#));
1083 }
1084
1085 #[test]
1086 fn test_playlist_entry_meaningful_content() {
1087 let empty: Vec<PlaylistEntry> = vec![];
1088 assert!(!empty.has_content());
1089
1090 let non_empty = vec![PlaylistEntry {
1091 playlist_item_id: "e1".to_string(),
1092 item: MediaItem {
1093 id: "1".to_string(),
1094 name: "Track".to_string(),
1095 item_type: "Audio".to_string(),
1096 kind: crate::domain::MediaKind::Track,
1097 is_folder: false,
1098 server_id: "s1".to_string(),
1099 parent_id: None,
1100 library_id: None,
1101 overview: None,
1102 genres: None,
1103 production_year: None,
1104 premiere_date: None,
1105 community_rating: None,
1106 official_rating: None,
1107 runtime_ticks: None,
1108 duration_ms: None,
1109 primary_image_tag: None,
1110 image_id: None,
1111 backdrop_image_tags: None,
1112 parent_backdrop_image_tags: None,
1113 album_id: None,
1114 album_name: None,
1115 album_artist: None,
1116 artists: None,
1117 artist_items: None,
1118 index_number: None,
1119 parent_index_number: None,
1120 series_id: None,
1121 series_name: None,
1122 season_id: None,
1123 season_name: None,
1124 user_data: None,
1125 media_streams: None,
1126 media_sources: None,
1127 people: None,
1128 },
1129 }];
1130 assert!(non_empty.has_content());
1131 }
1132}