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}
410
411#[derive(Debug, Deserialize, Clone)]
412#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
413#[serde(rename_all = "camelCase")]
414pub struct Media {
415    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
416    pub id: Option<String>,
417    pub duration: Option<u64>,
418    pub bitrate: Option<u32>,
419    pub width: Option<u32>,
420    pub height: Option<u32>,
421    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
422    pub aspect_ratio: Option<f32>,
423    pub audio_channels: Option<u8>,
424    pub protocol: Option<Protocol>,
425    pub audio_codec: Option<AudioCodec>,
426    pub video_codec: Option<VideoCodec>,
427    pub video_resolution: Option<String>,
428    pub container: Option<ContainerFormat>,
429    pub extension: Option<String>,
430    pub video_frame_rate: Option<String>,
431    pub audio_profile: Option<String>,
432    pub video_profile: Option<String>,
433    pub selected: Option<bool>,
434    #[serde(rename = "Part")]
435    pub parts: Vec<Part>,
436    #[serde(rename = "has64bitOffsets")]
437    pub has_64bit_offsets: Option<bool>,
438    #[serde(default, deserialize_with = "optional_boolish")]
439    pub optimized_for_streaming: Option<bool>,
440    pub display_offset: Option<u64>,
441    pub premium: Option<bool>,
442}
443
444#[derive(Debug, Deserialize, Clone)]
445#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
446pub struct Field {
447    pub locked: bool,
448    pub name: String,
449}
450
451#[derive(Debug, Deserialize, Clone)]
452#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
453pub struct Location {
454    pub path: String,
455}
456
457#[derive(Debug, Deserialize, Clone)]
458#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
459pub struct Tag {
460    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
461    pub id: Option<String>,
462    pub tag: String,
463    pub filter: Option<String>,
464    pub directory: Option<bool>,
465    #[serde(rename = "ratingKey")]
466    pub rating_key: Option<String>,
467    pub context: Option<String>,
468    pub slug: Option<String>,
469    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
470    pub count: Option<u32>,
471}
472
473#[derive(Debug, Deserialize, Clone)]
474#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
475pub struct Collection {
476    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
477    pub id: Option<String>,
478    pub art: Option<String>,
479    pub key: Option<String>,
480    pub thumb: Option<String>,
481    pub tag: String,
482    pub filter: Option<String>,
483    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
484    pub count: Option<u32>,
485    pub guid: Option<Guid>,
486    pub summary: Option<String>,
487}
488
489#[derive(Debug, Deserialize, Clone)]
490#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
491pub struct Review {
492    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
493    pub id: Option<String>,
494    pub tag: String,
495    pub filter: Option<String>,
496    pub text: String,
497    pub image: String,
498    pub link: Option<String>,
499    pub source: String,
500}
501
502#[derive(Debug, Deserialize, Clone)]
503#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
504#[serde(rename_all = "camelCase")]
505pub struct Rating {
506    #[serde(default, deserialize_with = "deserialize_number_from_string")]
507    pub count: u32,
508    pub image: String,
509    #[serde(rename = "type")]
510    pub rating_type: String,
511    #[serde(default, deserialize_with = "deserialize_number_from_string")]
512    pub value: f32,
513}
514
515#[derive(Debug, Deserialize, Clone)]
516#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
517#[serde(rename_all = "camelCase")]
518pub struct Role {
519    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
520    pub id: Option<String>,
521    pub tag: String,
522    pub key: Option<String>,
523    pub slug: Option<String>,
524    pub tag_key: Option<String>,
525    pub filter: Option<String>,
526    pub role: Option<String>,
527    pub thumb: Option<String>,
528    #[serde(rename = "type")]
529    pub role_type: Option<String>,
530    pub directory: Option<bool>,
531    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
532    pub count: Option<u32>,
533}
534
535#[derive(Debug, Deserialize, Clone)]
536#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
537#[serde(rename_all = "camelCase")]
538pub struct ParentMetadata {
539    pub parent_key: Option<String>,
540    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
541    pub parent_rating_key: Option<String>,
542    pub parent_guid: Option<Guid>,
543
544    pub parent_title: Option<String>,
545    pub parent_studio: Option<String>,
546    pub parent_year: Option<u32>,
547    pub parent_content_rating: Option<String>,
548    pub parent_index: Option<u32>,
549
550    pub parent_thumb: Option<String>,
551    pub parent_art: Option<String>,
552    pub parent_theme: Option<String>,
553}
554
555#[derive(Debug, Deserialize, Clone)]
556#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
557#[serde(rename_all = "camelCase")]
558pub struct GrandParentMetadata {
559    pub grandparent_key: Option<String>,
560    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
561    pub grandparent_rating_key: Option<String>,
562    pub grandparent_guid: Option<Guid>,
563
564    pub grandparent_title: Option<String>,
565    pub grandparent_studio: Option<String>,
566    pub grandparent_year: Option<u32>,
567    pub grandparent_content_rating: Option<String>,
568    pub grandparent_index: Option<u32>,
569
570    pub grandparent_thumb: Option<String>,
571    pub grandparent_art: Option<String>,
572    pub grandparent_theme: Option<String>,
573}
574
575#[derive(Debug, Deserialize, Clone)]
576#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
577#[serde(rename_all = "camelCase")]
578pub struct Extras {
579    pub size: u32,
580    pub key: Option<String>,
581    #[serde(default, rename = "Metadata")]
582    pub metadata: Vec<Box<Metadata>>,
583}
584
585#[derive(Debug, Deserialize, Clone)]
586#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
587#[serde(rename_all = "camelCase")]
588pub struct OnDeck {
589    #[serde(rename = "Metadata")]
590    pub metadata: Metadata,
591}
592
593#[derive(Debug, Deserialize, Clone)]
594#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
595#[serde(rename_all = "camelCase")]
596pub struct Chapter {
597    pub id: u32,
598    pub filter: Option<String>,
599    pub index: u32,
600    pub start_time_offset: u64,
601    pub end_time_offset: u64,
602    pub tag: Option<String>,
603    pub thumb: Option<String>,
604}
605
606pub(crate) fn deserialize_marker_type<'de, D>(deserializer: D) -> Result<MarkerType, D::Error>
607where
608    D: Deserializer<'de>,
609{
610    #[derive(Debug, Deserialize)]
611    #[serde(rename_all = "camelCase")]
612    struct Helper {
613        r#type: String,
614        r#final: Option<bool>,
615    }
616
617    let m = Helper::deserialize(deserializer)?;
618
619    match m.r#type.as_str() {
620        "intro" => Ok(MarkerType::Intro),
621        "credits" => Ok(MarkerType::Credits(m.r#final.unwrap_or_default())),
622        #[cfg(not(feature = "tests_deny_unknown_fields"))]
623        _ => Ok(MarkerType::Unknown(m.r#type)),
624        #[cfg(feature = "tests_deny_unknown_fields")]
625        _ => {
626            return Err(serde::de::Error::unknown_variant(
627                m.r#type.as_str(),
628                &["credits", "intro"],
629            ))
630        }
631    }
632}
633
634#[derive(Debug, Clone)]
635pub enum MarkerType {
636    /// Credits marker. If the inner value is `true` then it's the latest credits sequence in the media.
637    Credits(bool),
638    Intro,
639    #[cfg(not(feature = "tests_deny_unknown_fields"))]
640    Unknown(String),
641}
642
643#[derive(Debug, Deserialize, Clone)]
644#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
645#[serde(rename_all = "camelCase")]
646pub struct MarkerAttributes {
647    pub id: u32,
648    pub version: Option<u32>,
649}
650
651#[derive(Debug, Deserialize, Clone)]
652#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
653#[serde(rename_all = "camelCase")]
654pub struct Marker {
655    pub id: u32,
656    pub start_time_offset: u32,
657    pub end_time_offset: u32,
658    #[serde(flatten, deserialize_with = "deserialize_marker_type")]
659    pub marker_type: MarkerType,
660    #[serde(rename = "Attributes")]
661    pub attributes: MarkerAttributes,
662}
663
664#[derive(Debug, Deserialize, Clone)]
665#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
666#[serde(rename_all = "camelCase")]
667pub struct Metadata {
668    pub key: String,
669    pub rating_key: String,
670    pub guid: Option<Guid>,
671    pub primary_guid: Option<Guid>,
672
673    #[serde(flatten, deserialize_with = "deserialize_option_metadata_type")]
674    pub metadata_type: Option<MetadataType>,
675    #[serde(default, deserialize_with = "optional_boolish")]
676    pub smart: Option<bool>,
677    #[serde(default, deserialize_with = "optional_boolish")]
678    pub allow_sync: Option<bool>,
679
680    pub title: String,
681    pub title_sort: Option<String>,
682    pub original_title: Option<String>,
683    pub studio: Option<String>,
684    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
685    pub year: Option<u32>,
686    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
687    pub min_year: Option<u32>,
688    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
689    pub max_year: Option<u32>,
690    pub content_rating: Option<String>,
691    pub summary: Option<String>,
692    pub rating: Option<f32>,
693    pub rating_count: Option<u32>,
694    pub rating_image: Option<String>,
695    pub audience_rating: Option<f32>,
696    pub audience_rating_image: Option<String>,
697    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
698    pub user_rating: Option<f32>,
699    #[serde(
700        default,
701        deserialize_with = "deserialize_option_datetime_from_timestamp"
702    )]
703    pub last_rated_at: Option<OffsetDateTime>,
704    pub tagline: Option<String>,
705    pub duration: Option<u64>,
706    pub originally_available_at: Option<Date>,
707
708    pub thumb: Option<String>,
709    pub art: Option<String>,
710    pub theme: Option<String>,
711    pub composite: Option<String>,
712    pub banner: Option<String>,
713    pub icon: Option<String>,
714
715    pub index: Option<u32>,
716    #[serde(rename = "playlistItemID")]
717    pub playlist_item_id: Option<u32>,
718    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
719    pub child_count: Option<u32>,
720    pub season_count: Option<u32>,
721    pub leaf_count: Option<u32>,
722    pub viewed_leaf_count: Option<u32>,
723    pub skip_children: Option<bool>,
724
725    pub view_count: Option<u64>,
726    pub skip_count: Option<u64>,
727    #[serde(default, with = "time::serde::timestamp::option")]
728    pub last_viewed_at: Option<OffsetDateTime>,
729
730    #[serde(rename = "createdAtTZOffset")]
731    pub created_at_tz_offset: Option<String>,
732    pub created_at_accuracy: Option<String>,
733    #[serde(default, with = "time::serde::timestamp::option")]
734    pub added_at: Option<OffsetDateTime>,
735    #[serde(default, with = "time::serde::timestamp::option")]
736    pub deleted_at: Option<OffsetDateTime>,
737    #[serde(default, with = "time::serde::timestamp::option")]
738    pub updated_at: Option<OffsetDateTime>,
739    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
740    pub loudness_analysis_version: Option<u32>,
741    #[serde(default, deserialize_with = "optional_boolish")]
742    pub has_premium_extras: Option<bool>,
743    #[serde(default, deserialize_with = "optional_boolish")]
744    pub has_premium_primary_extra: Option<bool>,
745    pub view_offset: Option<u64>,
746    pub chapter_source: Option<ChapterSource>,
747    pub primary_extra_key: Option<String>,
748    #[serde(default, deserialize_with = "optional_boolish")]
749    pub has_premium_lyrics: Option<bool>,
750    pub music_analysis_version: Option<String>,
751
752    #[serde(rename = "librarySectionID")]
753    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
754    pub library_section_id: Option<u32>,
755    pub library_section_title: Option<String>,
756    pub library_section_key: Option<String>,
757
758    #[serde(flatten)]
759    pub parent: Box<ParentMetadata>,
760    #[serde(flatten)]
761    pub grand_parent: Box<GrandParentMetadata>,
762
763    #[serde(default, rename = "Guid")]
764    pub guids: Vec<Guid>,
765    #[serde(default, rename = "Collection")]
766    pub collections: Vec<Collection>,
767    #[serde(default, rename = "Similar")]
768    pub similar: Vec<Tag>,
769    #[serde(default, rename = "Genre")]
770    pub genres: Vec<Tag>,
771    #[serde(default, rename = "Director")]
772    pub directors: Vec<Role>,
773    #[serde(default, rename = "Producer")]
774    pub producers: Vec<Role>,
775    #[serde(default, rename = "Writer")]
776    pub writers: Vec<Role>,
777    #[serde(default, rename = "Country")]
778    pub countries: Vec<Tag>,
779    #[serde(default, rename = "Rating")]
780    pub ratings: Vec<Rating>,
781    #[serde(default, rename = "Role")]
782    pub roles: Vec<Role>,
783    #[serde(default, rename = "Location")]
784    pub locations: Vec<Location>,
785    #[serde(default, rename = "Field")]
786    pub fields: Vec<Field>,
787    #[serde(default, rename = "Mood")]
788    pub moods: Vec<Tag>,
789    #[serde(default, rename = "Format")]
790    pub formats: Vec<Tag>,
791    #[serde(default, rename = "Subformat")]
792    pub sub_formats: Vec<Tag>,
793    #[serde(default, rename = "Style")]
794    pub styles: Vec<Tag>,
795    #[serde(default, rename = "Review")]
796    pub reviews: Vec<Review>,
797    #[serde(default, rename = "Chapter")]
798    pub chapters: Vec<Chapter>,
799    #[serde(default, rename = "Label")]
800    pub labels: Vec<Tag>,
801
802    #[serde(rename = "Preferences")]
803    pub preferences: Option<Box<Preferences>>,
804
805    #[serde(rename = "Extras")]
806    pub extras: Option<Extras>,
807
808    #[serde(rename = "OnDeck")]
809    pub on_deck: Option<Box<OnDeck>>,
810
811    #[serde(default, rename = "Marker")]
812    pub markers: Vec<Marker>,
813
814    #[serde(rename = "Media")]
815    pub media: Option<Vec<Media>>,
816
817    #[serde(rename = "Vast")]
818    pub vast: Option<Vec<Link>>,
819
820    #[serde(rename = "publicPagesURL")]
821    pub public_pages_url: Option<String>,
822    pub slug: Option<String>,
823    pub user_state: Option<bool>,
824    pub imdb_rating_count: Option<u64>,
825    pub source: Option<String>,
826    #[serde(rename = "Image")]
827    pub image: Option<Vec<Image>>,
828    #[serde(rename = "Studio")]
829    pub studios: Option<Vec<Tag>>,
830
831    pub language_override: Option<String>,
832    pub content: Option<String>,
833    pub collection_sort: Option<String>,
834    pub skip_parent: Option<bool>,
835}
836
837#[derive(Deserialize, Debug, Clone)]
838pub struct Image {
839    pub url: String,
840    #[serde(rename = "type")]
841    pub image_type: String,
842    pub alt: String,
843}
844
845#[derive(Deserialize, Debug, Clone)]
846pub struct Link {
847    pub url: String,
848}
849
850#[derive(Debug, Deserialize, Clone)]
851#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
852#[serde(rename_all = "camelCase")]
853pub struct MetadataMediaContainer {
854    pub key: Option<String>,
855    #[serde(default, deserialize_with = "deserialize_option_string_from_number")]
856    pub rating_key: Option<String>,
857    pub augmentation_key: Option<String>,
858
859    pub title: Option<String>,
860    pub title1: Option<String>,
861    pub title2: Option<String>,
862    pub summary: Option<String>,
863    pub duration: Option<u64>,
864
865    #[serde(default, deserialize_with = "optional_boolish")]
866    pub allow_sync: Option<bool>,
867    #[serde(rename = "nocache")]
868    pub no_cache: Option<bool>,
869    pub sort_asc: Option<bool>,
870    pub smart: Option<bool>,
871
872    pub thumb: Option<String>,
873    pub art: Option<String>,
874    pub theme: Option<String>,
875    pub composite: Option<String>,
876    pub banner: Option<String>,
877
878    #[serde(rename = "librarySectionID")]
879    pub library_section_id: Option<u32>,
880    pub library_section_title: Option<String>,
881    #[serde(rename = "librarySectionUUID")]
882    pub library_section_uuid: Option<String>,
883
884    #[serde(flatten)]
885    pub parent: ParentMetadata,
886    #[serde(flatten)]
887    pub grand_parent: GrandParentMetadata,
888    #[serde(flatten)]
889    pub media_container: MediaContainer,
890
891    pub media_tag_prefix: Option<String>,
892    #[serde(default, with = "time::serde::timestamp::option")]
893    pub media_tag_version: Option<OffsetDateTime>,
894    pub mixed_parents: Option<bool>,
895    pub view_group: Option<String>,
896    pub view_mode: Option<u32>,
897    pub leaf_count: Option<u32>,
898    pub playlist_type: Option<PlaylistMetadataType>,
899
900    #[serde(default, rename = "Directory")]
901    pub directories: Vec<Value>,
902    #[serde(default, rename = "Metadata")]
903    pub metadata: Vec<Metadata>,
904}
905
906#[derive(Debug, Deserialize, Clone, Copy)]
907#[serde(rename_all = "lowercase")]
908pub enum PivotType {
909    Hub,
910    List,
911    View,
912    #[cfg(not(feature = "tests_deny_unknown_fields"))]
913    #[serde(other)]
914    Unknown,
915}
916
917derive_fromstr_from_deserialize!(PivotType);
918
919#[derive(Debug, Deserialize, Clone)]
920#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
921#[serde(rename_all = "camelCase")]
922pub struct Pivot {
923    pub context: String,
924    pub id: String,
925    pub key: String,
926    pub symbol: String,
927    pub title: String,
928    pub requires: Option<String>,
929    #[serde(rename = "type")]
930    pub pivot_type: PivotType,
931}
932
933#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
934#[serde(rename_all = "lowercase")]
935pub enum LibraryType {
936    Movie,
937    Show,
938    Artist,
939    Photo,
940    Mixed,
941    Clip,
942    #[cfg(not(feature = "tests_deny_unknown_fields"))]
943    #[serde(other)]
944    Unknown,
945}
946
947derive_fromstr_from_deserialize!(LibraryType);
948derive_display_from_serialize!(LibraryType);
949
950#[derive(Debug, Deserialize, Clone)]
951#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
952#[serde(rename_all = "camelCase")]
953pub struct ServerLibrary {
954    #[serde(rename = "type")]
955    pub library_type: LibraryType,
956    #[serde(rename = "Pivot")]
957    pub pivots: Vec<Pivot>,
958    pub agent: String,
959    pub hub_key: String,
960    pub id: String,
961    pub key: String,
962    pub subtype: Option<String>,
963    pub language: String,
964    pub refreshing: bool,
965    #[serde(with = "time::serde::timestamp")]
966    pub scanned_at: OffsetDateTime,
967    pub scanner: String,
968    pub title: String,
969    #[serde(with = "time::serde::timestamp")]
970    pub updated_at: OffsetDateTime,
971    pub uuid: String,
972}
973
974#[derive(Debug, Deserialize, Clone)]
975#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
976#[serde(rename_all = "camelCase")]
977pub struct ServerPlaylists {
978    #[serde(rename = "type")]
979    _type: MustBe!("playlist"),
980    #[serde(rename = "Pivot")]
981    pub pivots: Vec<Pivot>,
982    pub id: String,
983    pub key: String,
984    pub title: String,
985}
986
987#[derive(Debug, Deserialize, Clone)]
988#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
989#[serde(rename_all = "camelCase")]
990pub struct ServerHome {
991    pub hub_key: String,
992    pub title: String,
993}
994
995#[derive(Debug, Deserialize, Clone)]
996#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
997#[serde(rename_all = "camelCase")]
998pub struct DvrLibrary {
999    #[serde(rename = "type")]
1000    pub library_type: LibraryType,
1001    pub key: Option<String>,
1002    pub title: String,
1003    pub icon: String,
1004    #[serde(default, with = "time::serde::timestamp::option")]
1005    pub updated_at: Option<OffsetDateTime>,
1006}
1007
1008#[derive(Debug, Deserialize, Clone)]
1009#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
1010#[serde(rename_all = "camelCase")]
1011pub struct LiveTv {
1012    #[serde(rename = "Pivot")]
1013    pub pivots: Vec<Pivot>,
1014    pub id: String,
1015    pub title: String,
1016    pub hub_key: String,
1017}
1018
1019#[derive(Debug, Deserialize, Clone)]
1020#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
1021#[serde(rename_all = "camelCase")]
1022pub struct OnlineLibrary {
1023    #[serde(rename = "type")]
1024    pub library_type: LibraryType,
1025    pub key: Option<String>,
1026    pub title: String,
1027    pub icon: String,
1028    pub id: String,
1029    pub context: Option<String>,
1030    pub hub_key: Option<String>,
1031    #[serde(rename = "Pivot")]
1032    pub pivots: Vec<Pivot>,
1033    #[serde(default, with = "time::serde::timestamp::option")]
1034    pub updated_at: Option<OffsetDateTime>,
1035}
1036
1037#[derive(Debug, Deserialize, Clone)]
1038#[cfg_attr(not(feature = "tests_deny_unknown_fields"), serde(untagged))]
1039#[cfg_attr(feature = "tests_deny_unknown_fields", serde(try_from = "Value"))]
1040pub enum ContentDirectory {
1041    Playlists(ServerPlaylists),
1042    #[serde(rename_all = "camelCase")]
1043    Media(Box<ServerLibrary>),
1044    #[serde(rename_all = "camelCase")]
1045    Home(ServerHome),
1046    #[serde(rename_all = "camelCase")]
1047    LiveTv(LiveTv),
1048    #[serde(rename_all = "camelCase")]
1049    Online(OnlineLibrary),
1050    #[serde(rename_all = "camelCase")]
1051    Dvr(DvrLibrary),
1052    #[cfg(not(feature = "tests_deny_unknown_fields"))]
1053    Unknown(Value),
1054}
1055
1056// This generates much saner errors in tests than an untagged enum at the cost
1057// of some manual work.
1058#[cfg(feature = "tests_deny_unknown_fields")]
1059impl TryFrom<Value> for ContentDirectory {
1060    type Error = String;
1061
1062    fn try_from(value: Value) -> Result<Self, Self::Error> {
1063        let obj = if let Value::Object(o) = &value {
1064            o
1065        } else {
1066            return Err("Failed to decode Directory. Data was not an object.".to_string());
1067        };
1068
1069        let directory_type = match obj.get("type") {
1070            Some(Value::String(n)) => n,
1071            Some(_) => {
1072                return Err("Failed to decode Directory. Unexpected type property.".to_string())
1073            }
1074            None => {
1075                if obj.contains_key("Pivot") {
1076                    let live_tv: LiveTv = serde_json::from_value(value)
1077                        .map_err(|e| format!("Failed to decode Live TV directory: {e}"))?;
1078                    return Ok(Self::LiveTv(live_tv));
1079                }
1080
1081                let home: ServerHome = serde_json::from_value(value)
1082                    .map_err(|e| format!("Failed to decode Home directory: {e}"))?;
1083                return Ok(Self::Home(home));
1084            }
1085        };
1086
1087        if directory_type.as_str() == "playlist" {
1088            let p: ServerPlaylists = serde_json::from_value(value)
1089                .map_err(|e| format!("Failed to decode playlist directory: {e}"))?;
1090            Ok(Self::Playlists(p))
1091        } else {
1092            // We're left with ServerLibrary, OnlineLibrary or DvrLibrary.
1093
1094            // It seems unlikely OnlineLibrary will ever use a scanned_at field.
1095            if obj.contains_key("scannedAt") {
1096                let l: ServerLibrary = serde_json::from_value(value)
1097                    .map_err(|e| format!("Failed to decode server library directory: {e}"))?;
1098                Ok(Self::Media(Box::new(l)))
1099            } else if obj.contains_key("id") {
1100                let l: OnlineLibrary = serde_json::from_value(value)
1101                    .map_err(|e| format!("Failed to decode online library directory: {e}"))?;
1102                Ok(Self::Online(l))
1103            } else {
1104                let l: DvrLibrary = serde_json::from_value(value)
1105                    .map_err(|e| format!("Failed to decode dvr library directory: {e}"))?;
1106                Ok(Self::Dvr(l))
1107            }
1108        }
1109    }
1110}
1111
1112#[derive(Debug, Deserialize_repr, Clone, Copy, Serialize_repr)]
1113#[repr(u16)]
1114pub enum SearchType {
1115    Movie = 1,
1116    Show = 2,
1117    Season = 3,
1118    Episode = 4,
1119    Trailer = 5,
1120    Comic = 6,
1121    Person = 7,
1122    Artist = 8,
1123    Album = 9,
1124    Track = 10,
1125    Picture = 11,
1126    Clip = 12,
1127    Photo = 13,
1128    PhotoAlbum = 14,
1129    Playlist = 15,
1130    PlaylistFolder = 16,
1131    Collection = 18,
1132    OptimizedVersion = 42,
1133    UserPlaylistItem = 1001,
1134    #[cfg(not(feature = "tests_deny_unknown_fields"))]
1135    #[serde(other)]
1136    Unknown,
1137}
1138
1139derive_display_from_serialize!(SearchType);