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 pub transport: super::stream_selection::Transport,
481}
482
483#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
485#[serde(rename_all = "camelCase")]
486pub struct Genre {
487 pub id: String,
488 pub name: String,
489 pub album_count: Option<u32>,
493}
494
495#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
497pub enum ImageType {
498 Primary,
499 Backdrop,
500 Banner,
501 Thumb,
502 Logo,
503}
504
505impl ImageType {
506 pub fn as_str(&self) -> &str {
507 match self {
508 ImageType::Primary => "Primary",
509 ImageType::Backdrop => "Backdrop",
510 ImageType::Banner => "Banner",
511 ImageType::Thumb => "Thumb",
512 ImageType::Logo => "Logo",
513 }
514 }
515}
516
517#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
519#[serde(rename_all = "camelCase")]
520pub struct ImageOptions {
521 #[serde(skip_serializing_if = "Option::is_none")]
522 pub max_width: Option<u32>,
523 #[serde(skip_serializing_if = "Option::is_none")]
524 pub max_height: Option<u32>,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub quality: Option<u32>,
527 #[serde(skip_serializing_if = "Option::is_none")]
528 pub tag: Option<String>,
529}
530
531pub trait MeaningfulContent {
533 fn has_content(&self) -> bool;
534}
535
536impl MeaningfulContent for Vec<Library> {
537 fn has_content(&self) -> bool {
538 !self.is_empty()
539 }
540}
541
542impl MeaningfulContent for Vec<MediaItem> {
543 fn has_content(&self) -> bool {
544 !self.is_empty()
545 }
546}
547
548impl MeaningfulContent for SearchResult {
549 fn has_content(&self) -> bool {
550 !self.items.is_empty()
551 }
552}
553
554impl MeaningfulContent for MediaItem {
555 fn has_content(&self) -> bool {
556 true }
558}
559
560impl MeaningfulContent for Vec<Genre> {
561 fn has_content(&self) -> bool {
562 !self.is_empty()
563 }
564}
565
566impl MeaningfulContent for PlaybackInfo {
567 fn has_content(&self) -> bool {
568 !self.stream_url.is_empty()
569 }
570}
571
572#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
577#[serde(rename_all = "camelCase")]
578pub struct PlaylistEntry {
579 pub playlist_item_id: String,
581 #[serde(flatten)]
583 pub item: MediaItem,
584}
585
586#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
590#[serde(rename_all = "camelCase")]
591pub struct PlaylistCreatedResult {
592 pub id: String,
593}
594
595impl MeaningfulContent for Vec<PlaylistEntry> {
596 fn has_content(&self) -> bool {
597 !self.is_empty()
598 }
599}
600
601impl MeaningfulContent for PlaylistCreatedResult {
602 fn has_content(&self) -> bool {
603 !self.id.is_empty()
604 }
605}
606
607#[cfg(test)]
608mod search_scope_tests {
609 use super::*;
610
611 #[test]
619 fn music_scope_expands_to_music_item_types() {
620 assert_eq!(
621 SearchScope::Music.item_types(),
622 Some(vec![
623 "MusicAlbum".to_string(),
624 "MusicArtist".to_string(),
625 "Audio".to_string(),
626 "Playlist".to_string(),
627 ])
628 );
629 }
630
631 #[test]
633 fn movies_scope_expands_to_movie_only() {
634 assert_eq!(
635 SearchScope::Movies.item_types(),
636 Some(vec!["Movie".to_string()])
637 );
638 }
639
640 #[test]
642 fn tv_scope_expands_to_series_and_episode() {
643 assert_eq!(
644 SearchScope::Tv.item_types(),
645 Some(vec!["Series".to_string(), "Episode".to_string()])
646 );
647 }
648
649 #[test]
657 fn all_scope_sends_no_filter() {
658 assert_eq!(SearchScope::All.item_types(), None);
659 }
660
661 #[test]
665 fn resolve_scope_overrides_include_item_types() {
666 let mut options = SearchOptions {
667 include_item_types: Some(vec!["Movie".to_string()]),
668 scope: Some(SearchScope::Music),
669 ..Default::default()
670 };
671 options.resolve_scope();
672
673 assert_eq!(
674 options.include_item_types,
675 Some(vec![
676 "MusicAlbum".to_string(),
677 "MusicArtist".to_string(),
678 "Audio".to_string(),
679 "Playlist".to_string(),
680 ])
681 );
682 }
683
684 #[test]
688 fn resolve_all_scope_clears_include_item_types() {
689 let mut options = SearchOptions {
690 include_item_types: Some(vec!["Movie".to_string()]),
691 scope: Some(SearchScope::All),
692 ..Default::default()
693 };
694 options.resolve_scope();
695
696 assert_eq!(options.include_item_types, None);
697 }
698
699 #[test]
704 fn resolve_without_scope_preserves_include_item_types() {
705 let mut options = SearchOptions {
706 include_item_types: Some(vec!["MusicAlbum".to_string()]),
707 scope: None,
708 ..Default::default()
709 };
710 options.resolve_scope();
711
712 assert_eq!(
713 options.include_item_types,
714 Some(vec!["MusicAlbum".to_string()])
715 );
716 }
717
718 #[test]
722 fn scope_deserializes_from_camel_case() {
723 let options: SearchOptions =
724 serde_json::from_str(r#"{"scope": "music", "limit": 10}"#).unwrap();
725 assert!(matches!(options.scope, Some(SearchScope::Music)));
726
727 let all: SearchOptions = serde_json::from_str(r#"{"scope": "all"}"#).unwrap();
728 assert!(matches!(all.scope, Some(SearchScope::All)));
729 }
730
731 #[test]
733 fn test_collection_type_maps_to_its_favorites_scope() {
734 assert_eq!(
735 SearchScope::for_collection_type("movies"),
736 Some(SearchScope::Movies)
737 );
738 assert_eq!(
739 SearchScope::for_collection_type("tvshows"),
740 Some(SearchScope::Tv)
741 );
742 assert_eq!(
743 SearchScope::for_collection_type("music"),
744 Some(SearchScope::Music)
745 );
746 }
747
748 #[test]
754 fn test_uncategorised_collection_types_have_no_favorites_scope() {
755 for collection_type in ["livetv", "channels", "boxsets", "books", "unknown", ""] {
756 assert_eq!(
757 SearchScope::for_collection_type(collection_type),
758 None,
759 "{collection_type} should not carry a favourites scope"
760 );
761 }
762 }
763
764 #[test]
766 fn test_library_carries_its_favorites_scope_to_the_frontend() {
767 let music = Library::new("1".into(), "Music".into(), "music".into(), None);
768 assert_eq!(music.favorites_scope, Some(SearchScope::Music));
769
770 let json = serde_json::to_value(&music).unwrap();
771 assert_eq!(json["favoritesScope"], "music");
772
773 let livetv = Library::new("2".into(), "Live TV".into(), "livetv".into(), None);
775 let json = serde_json::to_value(&livetv).unwrap();
776 assert!(json.get("favoritesScope").is_none());
777 }
778}
779
780#[cfg(test)]
781mod tests {
782 use super::*;
783
784 #[test]
785 fn test_artist_item_deserialize_pascal_case() {
786 let json = r#"{"Id": "artist123", "Name": "Bob Dylan"}"#;
788 let result: Result<ArtistItem, _> = serde_json::from_str(json);
789
790 assert!(result.is_ok());
791 let artist = result.unwrap();
792 assert_eq!(artist.id, "artist123");
793 assert_eq!(artist.name, "Bob Dylan");
794 }
795
796 #[test]
797 fn test_artist_item_deserialize_array() {
798 let json = r#"[
800 {"Id": "artist1", "Name": "Bob Dylan"},
801 {"Id": "artist2", "Name": "Johnny Cash"}
802 ]"#;
803 let result: Result<Vec<ArtistItem>, _> = serde_json::from_str(json);
804
805 assert!(result.is_ok());
806 let artists = result.unwrap();
807 assert_eq!(artists.len(), 2);
808 assert_eq!(artists[0].id, "artist1");
809 assert_eq!(artists[0].name, "Bob Dylan");
810 assert_eq!(artists[1].id, "artist2");
811 assert_eq!(artists[1].name, "Johnny Cash");
812 }
813
814 #[test]
815 fn test_artist_item_serialize() {
816 let artist = ArtistItem {
819 id: "test-id".to_string(),
820 name: "Test Artist".to_string(),
821 };
822
823 let json = serde_json::to_string(&artist).expect("Failed to serialize");
824 assert!(json.contains(r#""id":"test-id""#));
825 assert!(json.contains(r#""name":"Test Artist""#));
826
827 let from_pascal: ArtistItem =
828 serde_json::from_str(r#"{"Id":"x","Name":"Y"}"#).expect("Failed to deserialize");
829 assert_eq!(from_pascal.id, "x");
830 assert_eq!(from_pascal.name, "Y");
831 }
832
833 #[test]
834 fn test_media_item_with_primary_image_tag() {
835 let json = r#"{
837 "id": "item123",
838 "name": "Test Item",
839 "type": "MusicAlbum",
840 "serverId": "server1",
841 "primaryImageTag": "tag123"
842 }"#;
843
844 let result: Result<MediaItem, _> = serde_json::from_str(json);
845 assert!(result.is_ok());
846
847 let item = result.unwrap();
848 assert_eq!(item.id, "item123");
849 assert_eq!(item.name, "Test Item");
850 assert_eq!(item.primary_image_tag, Some("tag123".to_string()));
851 }
852
853 #[test]
854 fn test_media_item_with_artists() {
855 let json = r#"{
857 "id": "track1",
858 "name": "Test Track",
859 "type": "Audio",
860 "serverId": "server1",
861 "artists": ["Artist 1", "Artist 2"]
862 }"#;
863
864 let result: Result<MediaItem, _> = serde_json::from_str(json);
865 assert!(result.is_ok());
866
867 let item = result.unwrap();
868 let artists = item.artists.expect("Expected artists");
869 assert_eq!(artists.len(), 2);
870 assert_eq!(artists[0], "Artist 1");
871 assert_eq!(artists[1], "Artist 2");
872 }
873
874 #[test]
875 fn test_search_result_meaningful_content() {
876 let empty_result = SearchResult {
878 items: vec![],
879 total_record_count: 0,
880 };
881 assert!(!empty_result.has_content());
882
883 let non_empty_result = SearchResult {
884 items: vec![MediaItem {
885 id: "1".to_string(),
886 name: "Test".to_string(),
887 item_type: "Audio".to_string(),
888 kind: crate::domain::MediaKind::Track,
889 is_folder: false,
890 server_id: "server1".to_string(),
891 parent_id: None,
892 library_id: None,
893 overview: None,
894 genres: None,
895 production_year: None,
896 premiere_date: None,
897 community_rating: None,
898 official_rating: None,
899 runtime_ticks: None,
900 duration_ms: None,
901 primary_image_tag: None,
902 image_id: None,
903 backdrop_image_tags: None,
904 parent_backdrop_image_tags: None,
905 album_id: None,
906 album_name: None,
907 album_artist: None,
908 artists: None,
909 artist_items: None,
910 index_number: None,
911 parent_index_number: None,
912 series_id: None,
913 series_name: None,
914 season_id: None,
915 season_name: None,
916 user_data: None,
917 media_streams: None,
918 media_sources: None,
919 people: None,
920 }],
921 total_record_count: 1,
922 };
923 assert!(non_empty_result.has_content());
924 }
925
926 #[test]
927 fn test_person_deserialize_complete() {
928 let json = r#"{
930 "Id": "person123",
931 "Name": "Tom Hanks",
932 "Type": "Actor",
933 "Role": "Lead Actor",
934 "PrimaryImageTag": "tag456"
935 }"#;
936
937 let result: Result<Person, _> = serde_json::from_str(json);
938 assert!(result.is_ok(), "Failed to deserialize: {:?}", result.err());
939
940 let person = result.unwrap();
941 assert_eq!(person.id, "person123");
942 assert_eq!(person.name, "Tom Hanks");
943 assert_eq!(person.person_type, "Actor");
944 assert_eq!(person.role, Some("Lead Actor".to_string()));
945 assert_eq!(person.primary_image_tag, Some("tag456".to_string()));
946
947 let serialized = serde_json::to_string(&person).expect("Failed to serialize");
949 assert!(
950 serialized.contains(r#""type":"Actor""#),
951 "Serialized form should use 'type' not 'Type'"
952 );
953 assert!(serialized.contains(r#""id":"person123""#));
954 assert!(serialized.contains(r#""primaryImageTag":"tag456""#));
955 }
956
957 #[test]
958 fn test_person_deserialize_minimal() {
959 let json = r#"{
961 "Id": "person456",
962 "Name": "Meryl Streep",
963 "Type": "Actress"
964 }"#;
965
966 let result: Result<Person, _> = serde_json::from_str(json);
967 assert!(result.is_ok());
968
969 let person = result.unwrap();
970 assert_eq!(person.id, "person456");
971 assert_eq!(person.name, "Meryl Streep");
972 assert_eq!(person.person_type, "Actress");
973 assert_eq!(person.role, None);
974 assert_eq!(person.primary_image_tag, None);
975 }
976
977 #[test]
978 fn test_person_array_deserialize() {
979 let json = r#"[
981 {"Id": "actor1", "Name": "Actor One", "Type": "Actor"},
982 {"Id": "director1", "Name": "Director One", "Type": "Director", "Role": "Director"}
983 ]"#;
984
985 let result: Result<Vec<Person>, _> = serde_json::from_str(json);
986 assert!(result.is_ok());
987
988 let people = result.unwrap();
989 assert_eq!(people.len(), 2);
990 assert_eq!(people[0].name, "Actor One");
991 assert_eq!(people[1].person_type, "Director");
992 assert_eq!(people[1].role, Some("Director".to_string()));
993 }
994
995 #[test]
996 fn test_media_item_with_people() {
997 let json = r#"{
999 "id": "movie1",
1000 "name": "Test Movie",
1001 "type": "Movie",
1002 "serverId": "server1",
1003 "people": [
1004 {"Id": "actor1", "Name": "John Doe", "Type": "Actor"},
1005 {"Id": "director1", "Name": "Jane Smith", "Type": "Director"}
1006 ]
1007 }"#;
1008
1009 let result: Result<MediaItem, _> = serde_json::from_str(json);
1010 assert!(result.is_ok());
1011
1012 let item = result.unwrap();
1013 let people = item.people.as_ref().expect("Expected people array");
1014 assert_eq!(people.len(), 2);
1015 assert_eq!(people[0].name, "John Doe");
1016 assert_eq!(people[0].person_type, "Actor");
1017 assert_eq!(people[1].person_type, "Director");
1018
1019 let serialized = serde_json::to_string(&item).expect("Failed to serialize");
1021 let re_parsed: serde_json::Value =
1022 serde_json::from_str(&serialized).expect("Failed to parse serialized");
1023 let people_array = re_parsed["people"]
1024 .as_array()
1025 .expect("people should be array");
1026 assert!(
1027 people_array[0].get("type").is_some(),
1028 "Serialized person should have 'type' field"
1029 );
1030 assert_eq!(people_array[0]["type"].as_str().unwrap(), "Actor");
1031 }
1032
1033 #[test]
1034 fn test_playlist_entry_serialization() {
1035 let entry = PlaylistEntry {
1036 playlist_item_id: "entry-abc-123".to_string(),
1037 item: MediaItem {
1038 id: "track1".to_string(),
1039 name: "Test Track".to_string(),
1040 item_type: "Audio".to_string(),
1041 kind: crate::domain::MediaKind::Track,
1042 is_folder: false,
1043 server_id: "server1".to_string(),
1044 parent_id: None,
1045 library_id: None,
1046 overview: None,
1047 genres: None,
1048 production_year: None,
1049 premiere_date: None,
1050 community_rating: None,
1051 official_rating: None,
1052 runtime_ticks: None,
1053 duration_ms: None,
1054 primary_image_tag: None,
1055 image_id: None,
1056 backdrop_image_tags: None,
1057 parent_backdrop_image_tags: None,
1058 album_id: None,
1059 album_name: None,
1060 album_artist: None,
1061 artists: Some(vec!["Artist One".to_string()]),
1062 artist_items: None,
1063 index_number: None,
1064 parent_index_number: None,
1065 series_id: None,
1066 series_name: None,
1067 season_id: None,
1068 season_name: None,
1069 user_data: None,
1070 media_streams: None,
1071 media_sources: None,
1072 people: None,
1073 },
1074 };
1075
1076 let json = serde_json::to_string(&entry).expect("Failed to serialize");
1077 assert!(json.contains(r#""playlistItemId":"entry-abc-123""#));
1079 assert!(json.contains(r#""id":"track1""#));
1081 assert!(json.contains(r#""name":"Test Track""#));
1082 assert!(json.contains(r#""type":"Audio""#));
1083 }
1084
1085 #[test]
1086 fn test_playlist_created_result_serialization() {
1087 let result = PlaylistCreatedResult {
1088 id: "playlist-new-123".to_string(),
1089 };
1090 let json = serde_json::to_string(&result).expect("Failed to serialize");
1091 assert!(json.contains(r#""id":"playlist-new-123""#));
1092 }
1093
1094 #[test]
1095 fn test_playlist_entry_meaningful_content() {
1096 let empty: Vec<PlaylistEntry> = vec![];
1097 assert!(!empty.has_content());
1098
1099 let non_empty = vec![PlaylistEntry {
1100 playlist_item_id: "e1".to_string(),
1101 item: MediaItem {
1102 id: "1".to_string(),
1103 name: "Track".to_string(),
1104 item_type: "Audio".to_string(),
1105 kind: crate::domain::MediaKind::Track,
1106 is_folder: false,
1107 server_id: "s1".to_string(),
1108 parent_id: None,
1109 library_id: None,
1110 overview: None,
1111 genres: None,
1112 production_year: None,
1113 premiere_date: None,
1114 community_rating: None,
1115 official_rating: None,
1116 runtime_ticks: None,
1117 duration_ms: None,
1118 primary_image_tag: None,
1119 image_id: None,
1120 backdrop_image_tags: None,
1121 parent_backdrop_image_tags: None,
1122 album_id: None,
1123 album_name: None,
1124 album_artist: None,
1125 artists: None,
1126 artist_items: None,
1127 index_number: None,
1128 parent_index_number: None,
1129 series_id: None,
1130 series_name: None,
1131 season_id: None,
1132 season_name: None,
1133 user_data: None,
1134 media_streams: None,
1135 media_sources: None,
1136 people: None,
1137 },
1138 }];
1139 assert!(non_empty.has_content());
1140 }
1141}