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 #[serde(skip_serializing_if = "Option::is_none")]
352 pub parent_kind: Option<crate::domain::MediaKind>,
353}
354
355pub fn default_listing_sort(
369 parent_kind: Option<crate::domain::MediaKind>,
370) -> Option<(&'static str, &'static str)> {
371 match parent_kind? {
372 crate::domain::MediaKind::ChannelFolder => Some(("PremiereDate", "Descending")),
373 _ => Some(("SortName", "Ascending")),
374 }
375}
376
377#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
387#[serde(rename_all = "camelCase")]
388pub enum SearchScope {
389 All,
390 Music,
391 Movies,
392 Tv,
393}
394
395impl SearchScope {
396 pub fn item_types(self) -> Option<Vec<String>> {
405 match self {
406 SearchScope::All => None,
407 SearchScope::Music => Some(
408 ["MusicAlbum", "MusicArtist", "Audio", "Playlist"]
409 .into_iter()
410 .map(String::from)
411 .collect(),
412 ),
413 SearchScope::Movies => Some(vec!["Movie".to_string()]),
414 SearchScope::Tv => Some(
415 ["Series", "Episode"]
416 .into_iter()
417 .map(String::from)
418 .collect(),
419 ),
420 }
421 }
422
423 pub fn for_collection_type(collection_type: &str) -> Option<SearchScope> {
436 match collection_type {
437 "movies" => Some(SearchScope::Movies),
438 "tvshows" => Some(SearchScope::Tv),
439 "music" => Some(SearchScope::Music),
440 _ => None,
441 }
442 }
443}
444
445#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
447#[serde(rename_all = "camelCase")]
448pub struct SearchOptions {
449 #[serde(skip_serializing_if = "Option::is_none")]
450 pub limit: Option<usize>,
451 #[serde(skip_serializing_if = "Option::is_none")]
452 pub include_item_types: Option<Vec<String>>,
453 #[serde(skip_serializing_if = "Option::is_none")]
454 pub search_term: Option<String>,
455 #[serde(skip_serializing_if = "Option::is_none")]
459 pub scope: Option<SearchScope>,
460}
461
462impl SearchOptions {
463 pub fn resolve_scope(&mut self) {
471 if let Some(scope) = self.scope {
472 self.include_item_types = scope.item_types();
475 }
476 }
477}
478
479#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
481#[serde(rename_all = "camelCase")]
482pub struct PlaybackInfo {
483 pub media_source_id: String,
484 pub play_session_id: String,
485 pub stream_url: String,
486 pub direct_play: bool,
487 pub needs_transcoding: bool,
488}
489
490#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct LiveStreamInfo {
498 pub stream_url: String,
499 pub play_session_id: Option<String>,
500 pub live_stream_id: Option<String>,
501 pub media_source_id: Option<String>,
502 pub transport: super::stream_selection::Transport,
511}
512
513#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
515#[serde(rename_all = "camelCase")]
516pub struct Genre {
517 pub id: String,
518 pub name: String,
519 pub album_count: Option<u32>,
523}
524
525#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
527pub enum ImageType {
528 Primary,
529 Backdrop,
530 Banner,
531 Thumb,
532 Logo,
533}
534
535impl ImageType {
536 pub fn as_str(&self) -> &str {
537 match self {
538 ImageType::Primary => "Primary",
539 ImageType::Backdrop => "Backdrop",
540 ImageType::Banner => "Banner",
541 ImageType::Thumb => "Thumb",
542 ImageType::Logo => "Logo",
543 }
544 }
545}
546
547#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
549#[serde(rename_all = "camelCase")]
550pub struct ImageOptions {
551 #[serde(skip_serializing_if = "Option::is_none")]
552 pub max_width: Option<u32>,
553 #[serde(skip_serializing_if = "Option::is_none")]
554 pub max_height: Option<u32>,
555 #[serde(skip_serializing_if = "Option::is_none")]
556 pub quality: Option<u32>,
557 #[serde(skip_serializing_if = "Option::is_none")]
558 pub tag: Option<String>,
559}
560
561pub trait MeaningfulContent {
563 fn has_content(&self) -> bool;
564}
565
566impl MeaningfulContent for Vec<Library> {
567 fn has_content(&self) -> bool {
568 !self.is_empty()
569 }
570}
571
572impl MeaningfulContent for Vec<MediaItem> {
573 fn has_content(&self) -> bool {
574 !self.is_empty()
575 }
576}
577
578impl MeaningfulContent for SearchResult {
579 fn has_content(&self) -> bool {
580 !self.items.is_empty()
581 }
582}
583
584impl MeaningfulContent for MediaItem {
585 fn has_content(&self) -> bool {
586 true }
588}
589
590impl MeaningfulContent for Vec<Genre> {
591 fn has_content(&self) -> bool {
592 !self.is_empty()
593 }
594}
595
596impl MeaningfulContent for PlaybackInfo {
597 fn has_content(&self) -> bool {
598 !self.stream_url.is_empty()
599 }
600}
601
602#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
607#[serde(rename_all = "camelCase")]
608pub struct PlaylistEntry {
609 pub playlist_item_id: String,
611 #[serde(flatten)]
613 pub item: MediaItem,
614}
615
616#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
620#[serde(rename_all = "camelCase")]
621pub struct PlaylistCreatedResult {
622 pub id: String,
623}
624
625impl MeaningfulContent for Vec<PlaylistEntry> {
626 fn has_content(&self) -> bool {
627 !self.is_empty()
628 }
629}
630
631impl MeaningfulContent for PlaylistCreatedResult {
632 fn has_content(&self) -> bool {
633 !self.id.is_empty()
634 }
635}
636
637#[cfg(test)]
638mod search_scope_tests {
639 use super::*;
640
641 #[test]
649 fn music_scope_expands_to_music_item_types() {
650 assert_eq!(
651 SearchScope::Music.item_types(),
652 Some(vec![
653 "MusicAlbum".to_string(),
654 "MusicArtist".to_string(),
655 "Audio".to_string(),
656 "Playlist".to_string(),
657 ])
658 );
659 }
660
661 #[test]
663 fn movies_scope_expands_to_movie_only() {
664 assert_eq!(
665 SearchScope::Movies.item_types(),
666 Some(vec!["Movie".to_string()])
667 );
668 }
669
670 #[test]
672 fn tv_scope_expands_to_series_and_episode() {
673 assert_eq!(
674 SearchScope::Tv.item_types(),
675 Some(vec!["Series".to_string(), "Episode".to_string()])
676 );
677 }
678
679 #[test]
687 fn all_scope_sends_no_filter() {
688 assert_eq!(SearchScope::All.item_types(), None);
689 }
690
691 #[test]
695 fn resolve_scope_overrides_include_item_types() {
696 let mut options = SearchOptions {
697 include_item_types: Some(vec!["Movie".to_string()]),
698 scope: Some(SearchScope::Music),
699 ..Default::default()
700 };
701 options.resolve_scope();
702
703 assert_eq!(
704 options.include_item_types,
705 Some(vec![
706 "MusicAlbum".to_string(),
707 "MusicArtist".to_string(),
708 "Audio".to_string(),
709 "Playlist".to_string(),
710 ])
711 );
712 }
713
714 #[test]
718 fn resolve_all_scope_clears_include_item_types() {
719 let mut options = SearchOptions {
720 include_item_types: Some(vec!["Movie".to_string()]),
721 scope: Some(SearchScope::All),
722 ..Default::default()
723 };
724 options.resolve_scope();
725
726 assert_eq!(options.include_item_types, None);
727 }
728
729 #[test]
734 fn resolve_without_scope_preserves_include_item_types() {
735 let mut options = SearchOptions {
736 include_item_types: Some(vec!["MusicAlbum".to_string()]),
737 scope: None,
738 ..Default::default()
739 };
740 options.resolve_scope();
741
742 assert_eq!(
743 options.include_item_types,
744 Some(vec!["MusicAlbum".to_string()])
745 );
746 }
747
748 #[test]
752 fn scope_deserializes_from_camel_case() {
753 let options: SearchOptions =
754 serde_json::from_str(r#"{"scope": "music", "limit": 10}"#).unwrap();
755 assert!(matches!(options.scope, Some(SearchScope::Music)));
756
757 let all: SearchOptions = serde_json::from_str(r#"{"scope": "all"}"#).unwrap();
758 assert!(matches!(all.scope, Some(SearchScope::All)));
759 }
760
761 #[test]
763 fn test_collection_type_maps_to_its_favorites_scope() {
764 assert_eq!(
765 SearchScope::for_collection_type("movies"),
766 Some(SearchScope::Movies)
767 );
768 assert_eq!(
769 SearchScope::for_collection_type("tvshows"),
770 Some(SearchScope::Tv)
771 );
772 assert_eq!(
773 SearchScope::for_collection_type("music"),
774 Some(SearchScope::Music)
775 );
776 }
777
778 #[test]
784 fn test_uncategorised_collection_types_have_no_favorites_scope() {
785 for collection_type in ["livetv", "channels", "boxsets", "books", "unknown", ""] {
786 assert_eq!(
787 SearchScope::for_collection_type(collection_type),
788 None,
789 "{collection_type} should not carry a favourites scope"
790 );
791 }
792 }
793
794 #[test]
796 fn test_library_carries_its_favorites_scope_to_the_frontend() {
797 let music = Library::new("1".into(), "Music".into(), "music".into(), None);
798 assert_eq!(music.favorites_scope, Some(SearchScope::Music));
799
800 let json = serde_json::to_value(&music).unwrap();
801 assert_eq!(json["favoritesScope"], "music");
802
803 let livetv = Library::new("2".into(), "Live TV".into(), "livetv".into(), None);
805 let json = serde_json::to_value(&livetv).unwrap();
806 assert!(json.get("favoritesScope").is_none());
807 }
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813
814 #[test]
815 fn test_artist_item_deserialize_pascal_case() {
816 let json = r#"{"Id": "artist123", "Name": "Bob Dylan"}"#;
818 let result: Result<ArtistItem, _> = serde_json::from_str(json);
819
820 assert!(result.is_ok());
821 let artist = result.unwrap();
822 assert_eq!(artist.id, "artist123");
823 assert_eq!(artist.name, "Bob Dylan");
824 }
825
826 #[test]
827 fn test_artist_item_deserialize_array() {
828 let json = r#"[
830 {"Id": "artist1", "Name": "Bob Dylan"},
831 {"Id": "artist2", "Name": "Johnny Cash"}
832 ]"#;
833 let result: Result<Vec<ArtistItem>, _> = serde_json::from_str(json);
834
835 assert!(result.is_ok());
836 let artists = result.unwrap();
837 assert_eq!(artists.len(), 2);
838 assert_eq!(artists[0].id, "artist1");
839 assert_eq!(artists[0].name, "Bob Dylan");
840 assert_eq!(artists[1].id, "artist2");
841 assert_eq!(artists[1].name, "Johnny Cash");
842 }
843
844 #[test]
845 fn test_artist_item_serialize() {
846 let artist = ArtistItem {
849 id: "test-id".to_string(),
850 name: "Test Artist".to_string(),
851 };
852
853 let json = serde_json::to_string(&artist).expect("Failed to serialize");
854 assert!(json.contains(r#""id":"test-id""#));
855 assert!(json.contains(r#""name":"Test Artist""#));
856
857 let from_pascal: ArtistItem =
858 serde_json::from_str(r#"{"Id":"x","Name":"Y"}"#).expect("Failed to deserialize");
859 assert_eq!(from_pascal.id, "x");
860 assert_eq!(from_pascal.name, "Y");
861 }
862
863 #[test]
864 fn test_media_item_with_primary_image_tag() {
865 let json = r#"{
867 "id": "item123",
868 "name": "Test Item",
869 "type": "MusicAlbum",
870 "serverId": "server1",
871 "primaryImageTag": "tag123"
872 }"#;
873
874 let result: Result<MediaItem, _> = serde_json::from_str(json);
875 assert!(result.is_ok());
876
877 let item = result.unwrap();
878 assert_eq!(item.id, "item123");
879 assert_eq!(item.name, "Test Item");
880 assert_eq!(item.primary_image_tag, Some("tag123".to_string()));
881 }
882
883 #[test]
884 fn test_media_item_with_artists() {
885 let json = r#"{
887 "id": "track1",
888 "name": "Test Track",
889 "type": "Audio",
890 "serverId": "server1",
891 "artists": ["Artist 1", "Artist 2"]
892 }"#;
893
894 let result: Result<MediaItem, _> = serde_json::from_str(json);
895 assert!(result.is_ok());
896
897 let item = result.unwrap();
898 let artists = item.artists.expect("Expected artists");
899 assert_eq!(artists.len(), 2);
900 assert_eq!(artists[0], "Artist 1");
901 assert_eq!(artists[1], "Artist 2");
902 }
903
904 #[test]
905 fn test_search_result_meaningful_content() {
906 let empty_result = SearchResult {
908 items: vec![],
909 total_record_count: 0,
910 };
911 assert!(!empty_result.has_content());
912
913 let non_empty_result = SearchResult {
914 items: vec![MediaItem {
915 id: "1".to_string(),
916 name: "Test".to_string(),
917 item_type: "Audio".to_string(),
918 kind: crate::domain::MediaKind::Track,
919 is_folder: false,
920 server_id: "server1".to_string(),
921 parent_id: None,
922 library_id: None,
923 overview: None,
924 genres: None,
925 production_year: None,
926 premiere_date: None,
927 community_rating: None,
928 official_rating: None,
929 runtime_ticks: None,
930 duration_ms: None,
931 primary_image_tag: None,
932 image_id: None,
933 backdrop_image_tags: None,
934 parent_backdrop_image_tags: None,
935 album_id: None,
936 album_name: None,
937 album_artist: None,
938 artists: None,
939 artist_items: None,
940 index_number: None,
941 parent_index_number: None,
942 series_id: None,
943 series_name: None,
944 season_id: None,
945 season_name: None,
946 user_data: None,
947 media_streams: None,
948 media_sources: None,
949 people: None,
950 }],
951 total_record_count: 1,
952 };
953 assert!(non_empty_result.has_content());
954 }
955
956 #[test]
957 fn test_person_deserialize_complete() {
958 let json = r#"{
960 "Id": "person123",
961 "Name": "Tom Hanks",
962 "Type": "Actor",
963 "Role": "Lead Actor",
964 "PrimaryImageTag": "tag456"
965 }"#;
966
967 let result: Result<Person, _> = serde_json::from_str(json);
968 assert!(result.is_ok(), "Failed to deserialize: {:?}", result.err());
969
970 let person = result.unwrap();
971 assert_eq!(person.id, "person123");
972 assert_eq!(person.name, "Tom Hanks");
973 assert_eq!(person.person_type, "Actor");
974 assert_eq!(person.role, Some("Lead Actor".to_string()));
975 assert_eq!(person.primary_image_tag, Some("tag456".to_string()));
976
977 let serialized = serde_json::to_string(&person).expect("Failed to serialize");
979 assert!(
980 serialized.contains(r#""type":"Actor""#),
981 "Serialized form should use 'type' not 'Type'"
982 );
983 assert!(serialized.contains(r#""id":"person123""#));
984 assert!(serialized.contains(r#""primaryImageTag":"tag456""#));
985 }
986
987 #[test]
988 fn test_person_deserialize_minimal() {
989 let json = r#"{
991 "Id": "person456",
992 "Name": "Meryl Streep",
993 "Type": "Actress"
994 }"#;
995
996 let result: Result<Person, _> = serde_json::from_str(json);
997 assert!(result.is_ok());
998
999 let person = result.unwrap();
1000 assert_eq!(person.id, "person456");
1001 assert_eq!(person.name, "Meryl Streep");
1002 assert_eq!(person.person_type, "Actress");
1003 assert_eq!(person.role, None);
1004 assert_eq!(person.primary_image_tag, None);
1005 }
1006
1007 #[test]
1008 fn test_person_array_deserialize() {
1009 let json = r#"[
1011 {"Id": "actor1", "Name": "Actor One", "Type": "Actor"},
1012 {"Id": "director1", "Name": "Director One", "Type": "Director", "Role": "Director"}
1013 ]"#;
1014
1015 let result: Result<Vec<Person>, _> = serde_json::from_str(json);
1016 assert!(result.is_ok());
1017
1018 let people = result.unwrap();
1019 assert_eq!(people.len(), 2);
1020 assert_eq!(people[0].name, "Actor One");
1021 assert_eq!(people[1].person_type, "Director");
1022 assert_eq!(people[1].role, Some("Director".to_string()));
1023 }
1024
1025 #[test]
1026 fn test_media_item_with_people() {
1027 let json = r#"{
1029 "id": "movie1",
1030 "name": "Test Movie",
1031 "type": "Movie",
1032 "serverId": "server1",
1033 "people": [
1034 {"Id": "actor1", "Name": "John Doe", "Type": "Actor"},
1035 {"Id": "director1", "Name": "Jane Smith", "Type": "Director"}
1036 ]
1037 }"#;
1038
1039 let result: Result<MediaItem, _> = serde_json::from_str(json);
1040 assert!(result.is_ok());
1041
1042 let item = result.unwrap();
1043 let people = item.people.as_ref().expect("Expected people array");
1044 assert_eq!(people.len(), 2);
1045 assert_eq!(people[0].name, "John Doe");
1046 assert_eq!(people[0].person_type, "Actor");
1047 assert_eq!(people[1].person_type, "Director");
1048
1049 let serialized = serde_json::to_string(&item).expect("Failed to serialize");
1051 let re_parsed: serde_json::Value =
1052 serde_json::from_str(&serialized).expect("Failed to parse serialized");
1053 let people_array = re_parsed["people"]
1054 .as_array()
1055 .expect("people should be array");
1056 assert!(
1057 people_array[0].get("type").is_some(),
1058 "Serialized person should have 'type' field"
1059 );
1060 assert_eq!(people_array[0]["type"].as_str().unwrap(), "Actor");
1061 }
1062
1063 #[test]
1064 fn test_playlist_entry_serialization() {
1065 let entry = PlaylistEntry {
1066 playlist_item_id: "entry-abc-123".to_string(),
1067 item: MediaItem {
1068 id: "track1".to_string(),
1069 name: "Test Track".to_string(),
1070 item_type: "Audio".to_string(),
1071 kind: crate::domain::MediaKind::Track,
1072 is_folder: false,
1073 server_id: "server1".to_string(),
1074 parent_id: None,
1075 library_id: None,
1076 overview: None,
1077 genres: None,
1078 production_year: None,
1079 premiere_date: None,
1080 community_rating: None,
1081 official_rating: None,
1082 runtime_ticks: None,
1083 duration_ms: None,
1084 primary_image_tag: None,
1085 image_id: None,
1086 backdrop_image_tags: None,
1087 parent_backdrop_image_tags: None,
1088 album_id: None,
1089 album_name: None,
1090 album_artist: None,
1091 artists: Some(vec!["Artist One".to_string()]),
1092 artist_items: None,
1093 index_number: None,
1094 parent_index_number: None,
1095 series_id: None,
1096 series_name: None,
1097 season_id: None,
1098 season_name: None,
1099 user_data: None,
1100 media_streams: None,
1101 media_sources: None,
1102 people: None,
1103 },
1104 };
1105
1106 let json = serde_json::to_string(&entry).expect("Failed to serialize");
1107 assert!(json.contains(r#""playlistItemId":"entry-abc-123""#));
1109 assert!(json.contains(r#""id":"track1""#));
1111 assert!(json.contains(r#""name":"Test Track""#));
1112 assert!(json.contains(r#""type":"Audio""#));
1113 }
1114
1115 #[test]
1116 fn test_playlist_created_result_serialization() {
1117 let result = PlaylistCreatedResult {
1118 id: "playlist-new-123".to_string(),
1119 };
1120 let json = serde_json::to_string(&result).expect("Failed to serialize");
1121 assert!(json.contains(r#""id":"playlist-new-123""#));
1122 }
1123
1124 #[test]
1125 fn test_playlist_entry_meaningful_content() {
1126 let empty: Vec<PlaylistEntry> = vec![];
1127 assert!(!empty.has_content());
1128
1129 let non_empty = vec![PlaylistEntry {
1130 playlist_item_id: "e1".to_string(),
1131 item: MediaItem {
1132 id: "1".to_string(),
1133 name: "Track".to_string(),
1134 item_type: "Audio".to_string(),
1135 kind: crate::domain::MediaKind::Track,
1136 is_folder: false,
1137 server_id: "s1".to_string(),
1138 parent_id: None,
1139 library_id: None,
1140 overview: None,
1141 genres: None,
1142 production_year: None,
1143 premiere_date: None,
1144 community_rating: None,
1145 official_rating: None,
1146 runtime_ticks: None,
1147 duration_ms: None,
1148 primary_image_tag: None,
1149 image_id: None,
1150 backdrop_image_tags: None,
1151 parent_backdrop_image_tags: None,
1152 album_id: None,
1153 album_name: None,
1154 album_artist: None,
1155 artists: None,
1156 artist_items: None,
1157 index_number: None,
1158 parent_index_number: None,
1159 series_id: None,
1160 series_name: None,
1161 season_id: None,
1162 season_name: None,
1163 user_data: None,
1164 media_streams: None,
1165 media_sources: None,
1166 people: None,
1167 },
1168 }];
1169 assert!(non_empty.has_content());
1170 }
1171}