plex_api/media_container/server/library/
mod.rs

1mod guid;
2mod metadata_type;
3
4use crate::media_container::{
5    helpers::deserialize_option_string_from_number,
6    helpers::{deserialize_option_datetime_from_timestamp, optional_boolish},
7    preferences::Preferences,
8    MediaContainer,
9};
10pub use guid::Guid;
11pub use metadata_type::*;
12use monostate::MustBe;
13use serde::{Deserialize, Deserializer, Serialize};
14use serde_aux::prelude::{
15    deserialize_number_from_string, deserialize_option_number_from_string,
16    deserialize_string_from_number,
17};
18use serde_json::Value;
19use serde_plain::{derive_display_from_serialize, derive_fromstr_from_deserialize};
20use serde_repr::{Deserialize_repr, Serialize_repr};
21use serde_with::{formats::CommaSeparator, serde_as, StringWithSeparator};
22use time::{Date, OffsetDateTime};
23
24#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
25#[serde(rename_all = "lowercase")]
26pub enum Decision {
27    Copy,
28    Transcode,
29    Ignore,
30    DirectPlay,
31    Burn,
32    #[cfg(not(feature = "tests_deny_unknown_fields"))]
33    #[serde(other)]
34    Unknown,
35}
36
37derive_display_from_serialize!(Decision);
38
39#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
40#[serde(rename_all = "lowercase")]
41pub enum Protocol {
42    /// HTTP file download
43    Http,
44    /// HTTP Live Streaming
45    Hls,
46    /// Dynamic Adaptive Streaming over HTTP
47    Dash,
48    /// ??? Used in extras; can't be used for transcoding
49    Mp4,
50    #[cfg(not(feature = "tests_deny_unknown_fields"))]
51    #[serde(other)]
52    Unknown,
53}
54
55derive_display_from_serialize!(Protocol);
56
57#[derive(Debug, Clone, Copy, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum ChapterSource {
60    Media,
61    Mixed,
62    Agent,
63    #[cfg(not(feature = "tests_deny_unknown_fields"))]
64    #[serde(other)]
65    Unknown,
66}
67
68derive_fromstr_from_deserialize!(ChapterSource);
69
70#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
71#[serde(rename_all = "kebab-case")]
72pub enum AudioCodec {
73    Aac,
74    Ac3,
75    Dca,
76    DcaMa,
77    Eac3,
78    Mp2,
79    Mp3,
80    Opus,
81    Pcm,
82    Vorbis,
83    Flac,
84    #[serde(rename = "truehd")]
85    TrueHd,
86    #[cfg(not(feature = "tests_deny_unknown_fields"))]
87    #[serde(other)]
88    Unknown,
89}
90
91derive_fromstr_from_deserialize!(AudioCodec);
92derive_display_from_serialize!(AudioCodec);
93
94#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
95#[serde(rename_all = "kebab-case")]
96pub enum VideoCodec {
97    H264,
98    Hevc,
99    Mpeg1Video,
100    Mpeg2Video,
101    Mpeg4,
102    Msmpeg4v3,
103    Vc1,
104    Vp8,
105    Vp9,
106    #[cfg(not(feature = "tests_deny_unknown_fields"))]
107    #[serde(other)]
108    Unknown,
109}
110
111derive_fromstr_from_deserialize!(VideoCodec);
112derive_display_from_serialize!(VideoCodec);
113
114#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
115#[serde(rename_all = "kebab-case")]
116pub enum LyricCodec {
117    Lrc,
118    Txt,
119    #[cfg(not(feature = "tests_deny_unknown_fields"))]
120    #[serde(other)]
121    Unknown,
122}
123
124derive_fromstr_from_deserialize!(LyricCodec);
125derive_display_from_serialize!(LyricCodec);
126
127#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
128#[serde(rename_all = "snake_case")]
129pub enum SubtitleCodec {
130    Ass,
131    Pgs,
132    Subrip,
133    Srt,
134    DvdSubtitle,
135    MovText,
136    Vtt,
137    DvbSubtitle,
138    #[cfg(not(feature = "tests_deny_unknown_fields"))]
139    #[serde(other)]
140    Unknown,
141}
142
143derive_fromstr_from_deserialize!(SubtitleCodec);
144derive_display_from_serialize!(SubtitleCodec);
145
146#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
147#[serde(rename_all = "lowercase")]
148pub enum ContainerFormat {
149    Aac,
150    Avi,
151    Jpeg,
152    M4v,
153    Mkv,
154    Mp3,
155    Mp4,
156    Mpeg,
157    MpegTs,
158    Ogg,
159    Wav,
160    Ac3,
161    Eac3,
162    #[cfg(not(feature = "tests_deny_unknown_fields"))]
163    #[serde(other)]
164    Unknown,
165}
166
167derive_fromstr_from_deserialize!(ContainerFormat);
168derive_display_from_serialize!(ContainerFormat);
169
170#[serde_as]
171#[derive(Debug, Deserialize, Clone)]
172#[serde(rename_all = "camelCase")]
173#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
174pub struct VideoStream {
175    #[serde(default, deserialize_with = "deserialize_string_from_number")]
176    pub id: String,
177    pub stream_type: MustBe!(1),
178    pub index: Option<u32>,
179    pub codec: VideoCodec,
180    pub default: Option<bool>,
181    pub selected: Option<bool>,
182    pub title: Option<String>,
183    pub display_title: String,
184    pub extended_display_title: Option<String>,
185
186    #[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, u32>>")]
187    pub required_bandwidths: Option<Vec<u32>>,
188    pub decision: Option<Decision>,
189    pub location: Option<String>,
190
191    pub height: u32,
192    pub width: u32,
193    pub bit_depth: Option<u8>,
194    pub bitrate: Option<u32>,
195    pub chroma_location: Option<String>,
196    pub chroma_subsampling: Option<String>,
197    pub coded_height: Option<u32>,
198    pub coded_width: Option<u32>,
199    pub color_primaries: Option<String>,
200    pub color_range: Option<String>,
201    pub color_space: Option<String>,
202    pub color_trc: Option<String>,
203    pub frame_rate: Option<f32>,
204    pub has_scaling_matrix: Option<bool>,
205    pub level: Option<u32>,
206    pub profile: Option<String>,
207    pub ref_frames: Option<u32>,
208    pub scan_type: Option<String>,
209    #[serde(rename = "codecID")]
210    pub codec_id: Option<String>,
211    pub stream_identifier: Option<String>,
212    pub language: Option<String>,
213    pub language_code: Option<String>,
214    pub language_tag: Option<String>,
215    pub anamorphic: Option<bool>,
216    pub pixel_aspect_ratio: Option<String>,
217}
218
219#[serde_as]
220#[derive(Debug, Deserialize, Clone)]
221#[serde(rename_all = "camelCase")]
222#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
223pub struct AudioStream {
224    #[serde(default, deserialize_with = "deserialize_string_from_number")]
225    pub id: String,
226    pub stream_type: MustBe!(2),
227    pub index: Option<u32>,
228    pub codec: AudioCodec,
229    pub default: Option<bool>,
230    pub selected: Option<bool>,
231    pub title: Option<String>,
232    pub display_title: String,
233    pub extended_display_title: Option<String>,
234
235    #[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, u32>>")]
236    pub required_bandwidths: Option<Vec<u32>>,
237    pub decision: Option<Decision>,
238    pub location: Option<String>,
239
240    pub channels: u32,
241    pub audio_channel_layout: Option<String>,
242    pub profile: Option<String>,
243    pub sampling_rate: Option<u32>,
244    pub bitrate: Option<u32>,
245    pub bitrate_mode: Option<String>,
246    pub language: Option<String>,
247    pub language_code: Option<String>,
248    pub language_tag: Option<String>,
249    pub peak: Option<String>,
250    pub gain: Option<String>,
251    pub album_peak: Option<String>,
252    pub album_gain: Option<String>,
253    pub album_range: Option<String>,
254    pub lra: Option<String>,
255    pub loudness: Option<String>,
256    pub stream_identifier: Option<String>,
257}
258
259#[serde_as]
260#[derive(Debug, Deserialize, Clone)]
261#[serde(rename_all = "camelCase")]
262#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
263pub struct SubtitleStream {
264    #[serde(default, deserialize_with = "deserialize_string_from_number")]
265    pub id: String,
266    pub stream_type: MustBe!(3),
267    pub index: Option<u32>,
268    pub codec: SubtitleCodec,
269    pub default: Option<bool>,
270    pub selected: Option<bool>,
271    pub title: Option<String>,
272    pub display_title: String,
273    pub extended_display_title: Option<String>,
274    pub forced: Option<bool>,
275
276    #[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, u32>>")]
277    pub required_bandwidths: Option<Vec<u32>>,
278    pub decision: Option<Decision>,
279    pub location: Option<String>,
280
281    pub key: Option<String>,
282    pub format: Option<String>,
283    pub file: Option<String>,
284    pub bitrate: Option<u32>,
285    pub hearing_impaired: Option<bool>,
286    pub language: Option<String>,
287    pub language_code: Option<String>,
288    pub language_tag: Option<String>,
289    pub ignore: Option<String>,
290    pub burn: Option<String>,
291}
292
293#[derive(Debug, Deserialize, Clone)]
294#[serde(rename_all = "camelCase")]
295#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
296pub struct LyricStream {
297    #[serde(default, deserialize_with = "deserialize_string_from_number")]
298    pub id: String,
299    pub stream_type: MustBe!(4),
300    pub index: Option<u32>,
301    pub codec: LyricCodec,
302    pub default: Option<bool>,
303    pub selected: Option<bool>,
304    pub title: Option<String>,
305    pub display_title: String,
306    pub extended_display_title: Option<String>,
307
308    pub key: Option<String>,
309    pub format: Option<String>,
310    pub timed: Option<String>,
311    pub min_lines: Option<String>,
312    pub provider: Option<String>,
313}
314
315#[derive(Debug, Deserialize, Clone)]
316#[cfg_attr(not(feature = "tests_deny_unknown_fields"), serde(untagged))]
317#[cfg_attr(feature = "tests_deny_unknown_fields", serde(try_from = "Value"))]
318pub enum Stream {
319    Video(VideoStream),
320    Audio(AudioStream),
321    Subtitle(SubtitleStream),
322    Lyric(LyricStream),
323    Unknown(Value),
324}
325
326// This generates much saner errors in tests than an untagged enum.
327#[cfg(feature = "tests_deny_unknown_fields")]
328impl TryFrom<Value> for Stream {
329    type Error = String;
330
331    fn try_from(value: Value) -> Result<Self, Self::Error> {
332        let stream_type = match &value {
333            Value::Object(o) => {
334                if let Some(Value::Number(n)) = o.get("streamType") {
335                    if let Some(v) = n.as_u64() {
336                        v
337                    } else {
338                        return Err(format!(
339                            "Failed to decode Stream. Unexpected streamType `{n}`"
340                        ));
341                    }
342                } else {
343                    return Err("Failed to decode Stream. Missing streamType property.".to_string());
344                }
345            }
346            _ => return Err("Failed to decode Stream. Data was not an object.".to_string()),
347        };
348
349        if stream_type == 1 {
350            let s: VideoStream = serde_json::from_value(value)
351                .map_err(|e| format!("Failed to decode video stream: {e}"))?;
352            Ok(Self::Video(s))
353        } else if stream_type == 2 {
354            let s: AudioStream = serde_json::from_value(value)
355                .map_err(|e| format!("Failed to decode audio stream: {e}"))?;
356            Ok(Self::Audio(s))
357        } else if stream_type == 3 {
358            let s: SubtitleStream = serde_json::from_value(value)
359                .map_err(|e| format!("Failed to decode subtitle stream: {e}"))?;
360            Ok(Self::Subtitle(s))
361        } else if stream_type == 4 {
362            let s: LyricStream = serde_json::from_value(value)
363                .map_err(|e| format!("Failed to decode lyric stream: {e}"))?;
364            Ok(Self::Lyric(s))
365        } else if !cfg!(feature = "tests_deny_unknown_fields") {
366            Ok(Self::Unknown(value))
367        } else {
368            Err(format!(
369                "Failed to decode Stream. Unexpected streamType `{stream_type}`"
370            ))
371        }
372    }
373}
374
375#[serde_as]
376#[derive(Debug, Deserialize, Clone)]
377#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
378#[serde(rename_all = "camelCase")]
379pub struct Part {
380    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
381    pub id: Option<String>,
382    pub key: Option<String>,
383    pub duration: Option<u64>,
384    pub file: Option<String>,
385    pub size: Option<u64>,
386    pub container: Option<ContainerFormat>,
387    pub indexes: Option<String>,
388    pub audio_profile: Option<String>,
389    pub video_profile: Option<String>,
390    pub protocol: Option<Protocol>,
391    pub selected: Option<bool>,
392    pub decision: Option<Decision>,
393    pub width: Option<u32>,
394    pub height: Option<u32>,
395    pub packet_length: Option<u64>,
396    pub has_thumbnail: Option<String>,
397    #[serde(rename = "has64bitOffsets")]
398    pub has_64bit_offsets: Option<bool>,
399    #[serde(default, deserialize_with = "optional_boolish")]
400    pub optimized_for_streaming: Option<bool>,
401    pub has_chapter_text_stream: Option<bool>,
402    pub has_chapter_video_stream: Option<bool>,
403    pub deep_analysis_version: Option<String>,
404    #[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, u32>>")]
405    pub required_bandwidths: Option<Vec<u32>>,
406    pub bitrate: Option<u32>,
407    #[serde(rename = "Stream")]
408    pub streams: Option<Vec<Stream>>,
409    pub accessible: Option<bool>,
410    pub exists: Option<bool>,
411}
412
413#[derive(Debug, Deserialize, Clone)]
414#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
415#[serde(rename_all = "camelCase")]
416pub struct Media {
417    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
418    pub id: Option<String>,
419    pub duration: Option<u64>,
420    pub bitrate: Option<u32>,
421    pub width: Option<u32>,
422    pub height: Option<u32>,
423    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
424    pub aspect_ratio: Option<f32>,
425    pub audio_channels: Option<u8>,
426    pub protocol: Option<Protocol>,
427    pub audio_codec: Option<AudioCodec>,
428    pub video_codec: Option<VideoCodec>,
429    pub video_resolution: Option<String>,
430    pub container: Option<ContainerFormat>,
431    pub extension: Option<String>,
432    pub video_frame_rate: Option<String>,
433    pub audio_profile: Option<String>,
434    pub video_profile: Option<String>,
435    pub selected: Option<bool>,
436    #[serde(rename = "Part")]
437    pub parts: Vec<Part>,
438    #[serde(rename = "has64bitOffsets")]
439    pub has_64bit_offsets: Option<bool>,
440    #[serde(default, deserialize_with = "optional_boolish")]
441    pub optimized_for_streaming: Option<bool>,
442    pub display_offset: Option<u64>,
443    pub premium: Option<bool>,
444    #[serde(default, deserialize_with = "optional_boolish")]
445    pub has_voice_activity: Option<bool>,
446}
447
448#[derive(Debug, Deserialize, Clone)]
449#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
450pub struct Field {
451    pub locked: bool,
452    pub name: String,
453}
454
455#[derive(Debug, Deserialize, Clone)]
456#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
457pub struct Location {
458    pub path: String,
459}
460
461#[derive(Debug, Deserialize, Clone)]
462#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
463pub struct Tag {
464    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
465    pub id: Option<String>,
466    pub tag: String,
467    pub filter: Option<String>,
468    pub directory: Option<bool>,
469    #[serde(rename = "ratingKey")]
470    pub rating_key: Option<String>,
471    pub context: Option<String>,
472    pub slug: Option<String>,
473    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
474    pub count: Option<u32>,
475}
476
477#[derive(Debug, Deserialize, Clone)]
478#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
479pub struct Collection {
480    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
481    pub id: Option<String>,
482    pub art: Option<String>,
483    pub key: Option<String>,
484    pub thumb: Option<String>,
485    pub tag: String,
486    pub filter: Option<String>,
487    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
488    pub count: Option<u32>,
489    pub guid: Option<Guid>,
490    pub summary: Option<String>,
491}
492
493#[derive(Debug, Deserialize, Clone)]
494#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
495pub struct Review {
496    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
497    pub id: Option<String>,
498    pub tag: String,
499    pub filter: Option<String>,
500    pub text: String,
501    pub image: String,
502    pub link: Option<String>,
503    pub source: String,
504}
505
506#[derive(Debug, Deserialize, Clone)]
507#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
508#[serde(rename_all = "camelCase")]
509pub struct Rating {
510    #[serde(default, deserialize_with = "deserialize_number_from_string")]
511    pub count: u32,
512    pub image: String,
513    #[serde(rename = "type")]
514    pub rating_type: String,
515    #[serde(default, deserialize_with = "deserialize_number_from_string")]
516    pub value: f32,
517}
518
519#[derive(Debug, Deserialize, Clone)]
520#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
521#[serde(rename_all = "camelCase")]
522pub struct Role {
523    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
524    pub id: Option<String>,
525    pub tag: String,
526    pub key: Option<String>,
527    pub slug: Option<String>,
528    pub tag_key: Option<String>,
529    pub filter: Option<String>,
530    pub role: Option<String>,
531    pub thumb: Option<String>,
532    #[serde(rename = "type")]
533    pub role_type: Option<String>,
534    pub directory: Option<bool>,
535    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
536    pub count: Option<u32>,
537}
538
539#[derive(Debug, Deserialize, Clone)]
540#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
541#[serde(rename_all = "camelCase")]
542pub struct ParentMetadata {
543    pub parent_key: Option<String>,
544    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
545    pub parent_rating_key: Option<String>,
546    pub parent_guid: Option<Guid>,
547
548    pub parent_title: Option<String>,
549    pub parent_studio: Option<String>,
550    pub parent_year: Option<u32>,
551    pub parent_content_rating: Option<String>,
552    pub parent_index: Option<u32>,
553
554    pub parent_thumb: Option<String>,
555    pub parent_art: Option<String>,
556    pub parent_theme: Option<String>,
557}
558
559#[derive(Debug, Deserialize, Clone)]
560#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
561#[serde(rename_all = "camelCase")]
562pub struct GrandParentMetadata {
563    pub grandparent_key: Option<String>,
564    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
565    pub grandparent_rating_key: Option<String>,
566    pub grandparent_guid: Option<Guid>,
567
568    pub grandparent_title: Option<String>,
569    pub grandparent_studio: Option<String>,
570    pub grandparent_year: Option<u32>,
571    pub grandparent_content_rating: Option<String>,
572    pub grandparent_index: Option<u32>,
573
574    pub grandparent_thumb: Option<String>,
575    pub grandparent_art: Option<String>,
576    pub grandparent_theme: Option<String>,
577}
578
579#[derive(Debug, Deserialize, Clone)]
580#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
581#[serde(rename_all = "camelCase")]
582pub struct Extras {
583    pub size: u32,
584    pub key: Option<String>,
585    #[serde(default, rename = "Metadata")]
586    pub metadata: Vec<Box<Metadata>>,
587}
588
589#[derive(Debug, Deserialize, Clone)]
590#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
591#[serde(rename_all = "camelCase")]
592pub struct OnDeck {
593    #[serde(rename = "Metadata")]
594    pub metadata: Metadata,
595}
596
597#[derive(Debug, Deserialize, Clone)]
598#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
599#[serde(rename_all = "camelCase")]
600pub struct Chapter {
601    pub id: u32,
602    pub filter: Option<String>,
603    pub index: u32,
604    pub start_time_offset: u64,
605    pub end_time_offset: u64,
606    pub tag: Option<String>,
607    pub thumb: Option<String>,
608}
609
610pub(crate) fn deserialize_marker_type<'de, D>(deserializer: D) -> Result<MarkerType, D::Error>
611where
612    D: Deserializer<'de>,
613{
614    #[derive(Debug, Deserialize)]
615    #[serde(rename_all = "camelCase")]
616    struct Helper {
617        r#type: String,
618        r#final: Option<bool>,
619    }
620
621    let m = Helper::deserialize(deserializer)?;
622
623    match m.r#type.as_str() {
624        "intro" => Ok(MarkerType::Intro),
625        "credits" => Ok(MarkerType::Credits(m.r#final.unwrap_or_default())),
626        #[cfg(not(feature = "tests_deny_unknown_fields"))]
627        _ => Ok(MarkerType::Unknown(m.r#type)),
628        #[cfg(feature = "tests_deny_unknown_fields")]
629        _ => Err(serde::de::Error::unknown_variant(
630            m.r#type.as_str(),
631            &["credits", "intro"],
632        )),
633    }
634}
635
636#[derive(Debug, Clone)]
637pub enum MarkerType {
638    /// Credits marker. If the inner value is `true` then it's the latest credits sequence in the media.
639    Credits(bool),
640    Intro,
641    #[cfg(not(feature = "tests_deny_unknown_fields"))]
642    Unknown(String),
643}
644
645#[derive(Debug, Deserialize, Clone)]
646#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
647#[serde(rename_all = "camelCase")]
648pub struct MarkerAttributes {
649    pub id: u32,
650    pub version: Option<u32>,
651}
652
653#[derive(Debug, Deserialize, Clone)]
654#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
655#[serde(rename_all = "camelCase")]
656pub struct Marker {
657    pub id: u32,
658    pub start_time_offset: u32,
659    pub end_time_offset: u32,
660    #[serde(flatten, deserialize_with = "deserialize_marker_type")]
661    pub marker_type: MarkerType,
662    #[serde(rename = "Attributes")]
663    pub attributes: MarkerAttributes,
664}
665
666#[derive(Debug, Deserialize, Clone)]
667#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
668#[serde(rename_all = "camelCase")]
669pub struct Metadata {
670    pub key: String,
671    pub rating_key: String,
672    pub guid: Option<Guid>,
673    pub primary_guid: Option<Guid>,
674
675    #[serde(flatten, deserialize_with = "deserialize_option_metadata_type")]
676    pub metadata_type: Option<MetadataType>,
677    #[serde(default, deserialize_with = "optional_boolish")]
678    pub smart: Option<bool>,
679    #[serde(default, deserialize_with = "optional_boolish")]
680    pub allow_sync: Option<bool>,
681
682    pub title: String,
683    pub title_sort: Option<String>,
684    pub original_title: Option<String>,
685    pub studio: Option<String>,
686    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
687    pub year: Option<u32>,
688    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
689    pub min_year: Option<u32>,
690    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
691    pub max_year: Option<u32>,
692    pub content_rating: Option<String>,
693    pub summary: Option<String>,
694    pub rating: Option<f32>,
695    pub rating_count: Option<u32>,
696    pub rating_image: Option<String>,
697    pub audience_rating: Option<f32>,
698    pub audience_rating_image: Option<String>,
699    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
700    pub user_rating: Option<f32>,
701    #[serde(
702        default,
703        deserialize_with = "deserialize_option_datetime_from_timestamp"
704    )]
705    pub last_rated_at: Option<OffsetDateTime>,
706    pub tagline: Option<String>,
707    pub duration: Option<u64>,
708    pub originally_available_at: Option<Date>,
709
710    pub thumb: Option<String>,
711    pub art: Option<String>,
712    pub theme: Option<String>,
713    pub composite: Option<String>,
714    pub banner: Option<String>,
715    pub icon: Option<String>,
716
717    pub index: Option<u32>,
718    #[serde(rename = "playlistItemID")]
719    pub playlist_item_id: Option<u32>,
720    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
721    pub child_count: Option<u32>,
722    pub season_count: Option<u32>,
723    pub leaf_count: Option<u32>,
724    pub viewed_leaf_count: Option<u32>,
725    pub skip_children: Option<bool>,
726
727    pub view_count: Option<u64>,
728    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
729    pub skip_count: Option<u64>,
730    #[serde(default, with = "time::serde::timestamp::option")]
731    pub last_viewed_at: Option<OffsetDateTime>,
732
733    #[serde(rename = "createdAtTZOffset")]
734    pub created_at_tz_offset: Option<String>,
735    pub created_at_accuracy: Option<String>,
736    #[serde(default, with = "time::serde::timestamp::option")]
737    pub added_at: Option<OffsetDateTime>,
738    #[serde(default, with = "time::serde::timestamp::option")]
739    pub deleted_at: Option<OffsetDateTime>,
740    #[serde(default, with = "time::serde::timestamp::option")]
741    pub updated_at: Option<OffsetDateTime>,
742    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
743    pub loudness_analysis_version: Option<u32>,
744    #[serde(default, deserialize_with = "optional_boolish")]
745    pub has_premium_extras: Option<bool>,
746    #[serde(default, deserialize_with = "optional_boolish")]
747    pub has_premium_primary_extra: Option<bool>,
748    pub view_offset: Option<u64>,
749    pub chapter_source: Option<ChapterSource>,
750    pub primary_extra_key: Option<String>,
751    #[serde(default, deserialize_with = "optional_boolish")]
752    pub has_premium_lyrics: Option<bool>,
753    pub music_analysis_version: Option<String>,
754
755    #[serde(rename = "librarySectionID")]
756    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
757    pub library_section_id: Option<u32>,
758    pub library_section_title: Option<String>,
759    pub library_section_key: Option<String>,
760
761    #[serde(flatten)]
762    pub parent: Box<ParentMetadata>,
763    #[serde(flatten)]
764    pub grand_parent: Box<GrandParentMetadata>,
765
766    #[serde(default, rename = "Guid")]
767    pub guids: Vec<Guid>,
768    #[serde(default, rename = "Collection")]
769    pub collections: Vec<Collection>,
770    #[serde(default, rename = "Similar")]
771    pub similar: Vec<Tag>,
772    #[serde(default, rename = "Genre")]
773    pub genres: Vec<Tag>,
774    #[serde(default, rename = "Director")]
775    pub directors: Vec<Role>,
776    #[serde(default, rename = "Producer")]
777    pub producers: Vec<Role>,
778    #[serde(default, rename = "Writer")]
779    pub writers: Vec<Role>,
780    #[serde(default, rename = "Country")]
781    pub countries: Vec<Tag>,
782    #[serde(default, rename = "Rating")]
783    pub ratings: Vec<Rating>,
784    #[serde(default, rename = "Role")]
785    pub roles: Vec<Role>,
786    #[serde(default, rename = "Location")]
787    pub locations: Vec<Location>,
788    #[serde(default, rename = "Field")]
789    pub fields: Vec<Field>,
790    #[serde(default, rename = "Mood")]
791    pub moods: Vec<Tag>,
792    #[serde(default, rename = "Format")]
793    pub formats: Vec<Tag>,
794    #[serde(default, rename = "Subformat")]
795    pub sub_formats: Vec<Tag>,
796    #[serde(default, rename = "Style")]
797    pub styles: Vec<Tag>,
798    #[serde(default, rename = "Review")]
799    pub reviews: Vec<Review>,
800    #[serde(default, rename = "Chapter")]
801    pub chapters: Vec<Chapter>,
802    #[serde(default, rename = "Label")]
803    pub labels: Vec<Tag>,
804
805    #[serde(rename = "Preferences")]
806    pub preferences: Option<Box<Preferences>>,
807
808    #[serde(rename = "Extras")]
809    pub extras: Option<Extras>,
810
811    #[serde(rename = "OnDeck")]
812    pub on_deck: Option<Box<OnDeck>>,
813
814    #[serde(default, rename = "Marker")]
815    pub markers: Vec<Marker>,
816
817    #[serde(rename = "Media")]
818    pub media: Option<Vec<Media>>,
819
820    #[serde(rename = "Vast")]
821    pub vast: Option<Vec<Link>>,
822
823    #[serde(rename = "publicPagesURL")]
824    pub public_pages_url: Option<String>,
825    pub slug: Option<String>,
826    pub user_state: Option<bool>,
827    pub imdb_rating_count: Option<u64>,
828    pub source: Option<String>,
829    #[serde(rename = "Image")]
830    pub image: Option<Vec<Image>>,
831    #[serde(rename = "Studio")]
832    pub studios: Option<Vec<Tag>>,
833
834    pub language_override: Option<String>,
835    pub content: Option<String>,
836    pub collection_sort: Option<String>,
837    #[serde(default, deserialize_with = "optional_boolish")]
838    pub skip_parent: Option<bool>,
839}
840
841#[derive(Deserialize, Debug, Clone)]
842pub struct Image {
843    pub url: String,
844    #[serde(rename = "type")]
845    pub image_type: String,
846    pub alt: String,
847}
848
849#[derive(Deserialize, Debug, Clone)]
850pub struct Link {
851    pub url: String,
852}
853
854#[derive(Debug, Deserialize, Clone)]
855#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
856#[serde(rename_all = "camelCase")]
857pub struct MetadataMediaContainer {
858    pub key: Option<String>,
859    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
860    pub rating_key: Option<String>,
861    pub augmentation_key: Option<String>,
862
863    pub title: Option<String>,
864    pub title1: Option<String>,
865    pub title2: Option<String>,
866    pub summary: Option<String>,
867    pub duration: Option<u64>,
868
869    #[serde(default, deserialize_with = "optional_boolish")]
870    pub allow_sync: Option<bool>,
871    #[serde(rename = "nocache")]
872    pub no_cache: Option<bool>,
873    pub sort_asc: Option<bool>,
874    pub smart: Option<bool>,
875
876    pub thumb: Option<String>,
877    pub art: Option<String>,
878    pub content: Option<String>,
879    pub theme: Option<String>,
880    pub composite: Option<String>,
881    pub banner: Option<String>,
882
883    #[serde(rename = "librarySectionID")]
884    pub library_section_id: Option<u32>,
885    pub library_section_title: Option<String>,
886    #[serde(rename = "librarySectionUUID")]
887    pub library_section_uuid: Option<String>,
888
889    #[serde(flatten)]
890    pub parent: ParentMetadata,
891    #[serde(flatten)]
892    pub grand_parent: GrandParentMetadata,
893    #[serde(flatten)]
894    pub media_container: MediaContainer,
895
896    pub media_tag_prefix: Option<String>,
897    #[serde(default, with = "time::serde::timestamp::option")]
898    pub media_tag_version: Option<OffsetDateTime>,
899    pub mixed_parents: Option<bool>,
900    pub view_group: Option<String>,
901    pub view_mode: Option<u32>,
902    pub leaf_count: Option<u32>,
903    pub playlist_type: Option<PlaylistMetadataType>,
904
905    #[serde(default, rename = "Directory")]
906    pub directories: Vec<Value>,
907    #[serde(default, rename = "Metadata")]
908    pub metadata: Vec<Metadata>,
909}
910
911#[derive(Debug, Deserialize, Clone, Copy)]
912#[serde(rename_all = "lowercase")]
913pub enum PivotType {
914    Hub,
915    List,
916    View,
917    #[cfg(not(feature = "tests_deny_unknown_fields"))]
918    #[serde(other)]
919    Unknown,
920}
921
922derive_fromstr_from_deserialize!(PivotType);
923
924#[derive(Debug, Deserialize, Clone)]
925#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
926#[serde(rename_all = "camelCase")]
927pub struct Pivot {
928    pub context: String,
929    pub id: String,
930    pub key: String,
931    pub symbol: String,
932    pub title: String,
933    pub requires: Option<String>,
934    #[serde(rename = "type")]
935    pub pivot_type: PivotType,
936}
937
938#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
939#[serde(rename_all = "lowercase")]
940pub enum LibraryType {
941    Movie,
942    Show,
943    Artist,
944    Photo,
945    Mixed,
946    Clip,
947    #[cfg(not(feature = "tests_deny_unknown_fields"))]
948    #[serde(other)]
949    Unknown,
950}
951
952derive_fromstr_from_deserialize!(LibraryType);
953derive_display_from_serialize!(LibraryType);
954
955#[derive(Debug, Deserialize, Clone)]
956#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
957#[serde(rename_all = "camelCase")]
958pub struct ServerLibrary {
959    #[serde(rename = "type")]
960    pub library_type: LibraryType,
961    #[serde(rename = "Pivot")]
962    pub pivots: Vec<Pivot>,
963    pub agent: String,
964    pub hub_key: String,
965    pub id: String,
966    pub key: String,
967    pub subtype: Option<String>,
968    pub language: String,
969    pub refreshing: bool,
970    #[serde(with = "time::serde::timestamp")]
971    pub scanned_at: OffsetDateTime,
972    pub scanner: String,
973    pub title: String,
974    #[serde(with = "time::serde::timestamp")]
975    pub updated_at: OffsetDateTime,
976    pub uuid: String,
977}
978
979#[derive(Debug, Deserialize, Clone)]
980#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
981#[serde(rename_all = "camelCase")]
982pub struct ServerPlaylists {
983    #[serde(rename = "type")]
984    _type: MustBe!("playlist"),
985    #[serde(rename = "Pivot")]
986    pub pivots: Vec<Pivot>,
987    pub id: String,
988    pub key: String,
989    pub title: String,
990}
991
992#[derive(Debug, Deserialize, Clone)]
993#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
994#[serde(rename_all = "camelCase")]
995pub struct ServerHome {
996    pub hub_key: String,
997    pub title: String,
998}
999
1000#[derive(Debug, Deserialize, Clone)]
1001#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
1002#[serde(rename_all = "camelCase")]
1003pub struct DvrLibrary {
1004    #[serde(rename = "type")]
1005    pub library_type: LibraryType,
1006    pub key: Option<String>,
1007    pub title: String,
1008    pub icon: String,
1009    #[serde(default, with = "time::serde::timestamp::option")]
1010    pub updated_at: Option<OffsetDateTime>,
1011}
1012
1013#[derive(Debug, Deserialize, Clone)]
1014#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
1015#[serde(rename_all = "camelCase")]
1016pub struct LiveTv {
1017    #[serde(rename = "Pivot")]
1018    pub pivots: Vec<Pivot>,
1019    pub id: String,
1020    pub title: String,
1021    pub hub_key: String,
1022}
1023
1024#[derive(Debug, Deserialize, Clone)]
1025#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
1026#[serde(rename_all = "camelCase")]
1027pub struct OnlineLibrary {
1028    #[serde(rename = "type")]
1029    pub library_type: LibraryType,
1030    pub key: Option<String>,
1031    pub title: String,
1032    pub icon: String,
1033    pub id: String,
1034    pub context: Option<String>,
1035    pub hub_key: Option<String>,
1036    #[serde(rename = "Pivot")]
1037    pub pivots: Vec<Pivot>,
1038    #[serde(default, with = "time::serde::timestamp::option")]
1039    pub updated_at: Option<OffsetDateTime>,
1040}
1041
1042#[derive(Debug, Deserialize, Clone)]
1043#[cfg_attr(not(feature = "tests_deny_unknown_fields"), serde(untagged))]
1044#[cfg_attr(feature = "tests_deny_unknown_fields", serde(try_from = "Value"))]
1045pub enum ContentDirectory {
1046    Playlists(ServerPlaylists),
1047    #[serde(rename_all = "camelCase")]
1048    Media(Box<ServerLibrary>),
1049    #[serde(rename_all = "camelCase")]
1050    Home(ServerHome),
1051    #[serde(rename_all = "camelCase")]
1052    LiveTv(LiveTv),
1053    #[serde(rename_all = "camelCase")]
1054    Online(OnlineLibrary),
1055    #[serde(rename_all = "camelCase")]
1056    Dvr(DvrLibrary),
1057    #[cfg(not(feature = "tests_deny_unknown_fields"))]
1058    Unknown(Value),
1059}
1060
1061// This generates much saner errors in tests than an untagged enum at the cost
1062// of some manual work.
1063#[cfg(feature = "tests_deny_unknown_fields")]
1064impl TryFrom<Value> for ContentDirectory {
1065    type Error = String;
1066
1067    fn try_from(value: Value) -> Result<Self, Self::Error> {
1068        let obj = if let Value::Object(o) = &value {
1069            o
1070        } else {
1071            return Err("Failed to decode Directory. Data was not an object.".to_string());
1072        };
1073
1074        let directory_type = match obj.get("type") {
1075            Some(Value::String(n)) => n,
1076            Some(_) => {
1077                return Err("Failed to decode Directory. Unexpected type property.".to_string())
1078            }
1079            None => {
1080                if obj.contains_key("Pivot") {
1081                    let live_tv: LiveTv = serde_json::from_value(value)
1082                        .map_err(|e| format!("Failed to decode Live TV directory: {e}"))?;
1083                    return Ok(Self::LiveTv(live_tv));
1084                }
1085
1086                let home: ServerHome = serde_json::from_value(value)
1087                    .map_err(|e| format!("Failed to decode Home directory: {e}"))?;
1088                return Ok(Self::Home(home));
1089            }
1090        };
1091
1092        if directory_type.as_str() == "playlist" {
1093            let p: ServerPlaylists = serde_json::from_value(value)
1094                .map_err(|e| format!("Failed to decode playlist directory: {e}"))?;
1095            Ok(Self::Playlists(p))
1096        } else {
1097            // We're left with ServerLibrary, OnlineLibrary or DvrLibrary.
1098
1099            // It seems unlikely OnlineLibrary will ever use a scanned_at field.
1100            if obj.contains_key("scannedAt") {
1101                let l: ServerLibrary = serde_json::from_value(value)
1102                    .map_err(|e| format!("Failed to decode server library directory: {e}"))?;
1103                Ok(Self::Media(Box::new(l)))
1104            } else if obj.contains_key("id") {
1105                let l: OnlineLibrary = serde_json::from_value(value)
1106                    .map_err(|e| format!("Failed to decode online library directory: {e}"))?;
1107                Ok(Self::Online(l))
1108            } else {
1109                let l: DvrLibrary = serde_json::from_value(value)
1110                    .map_err(|e| format!("Failed to decode dvr library directory: {e}"))?;
1111                Ok(Self::Dvr(l))
1112            }
1113        }
1114    }
1115}
1116
1117#[derive(Debug, Deserialize_repr, Clone, Copy, Serialize_repr)]
1118#[repr(u16)]
1119pub enum SearchType {
1120    Movie = 1,
1121    Show = 2,
1122    Season = 3,
1123    Episode = 4,
1124    Trailer = 5,
1125    Comic = 6,
1126    Person = 7,
1127    Artist = 8,
1128    Album = 9,
1129    Track = 10,
1130    Picture = 11,
1131    Clip = 12,
1132    Photo = 13,
1133    PhotoAlbum = 14,
1134    Playlist = 15,
1135    PlaylistFolder = 16,
1136    Collection = 18,
1137    OptimizedVersion = 42,
1138    UserPlaylistItem = 1001,
1139    #[cfg(not(feature = "tests_deny_unknown_fields"))]
1140    #[serde(other)]
1141    Unknown,
1142}
1143
1144derive_display_from_serialize!(SearchType);