plex_api/server/
library.rs

1use std::{future::Future, marker::PhantomData, ops::RangeBounds};
2
3use enum_dispatch::enum_dispatch;
4use futures::AsyncWrite;
5use http::StatusCode;
6use isahc::AsyncReadResponseExt;
7
8use crate::{
9    isahc_compat::StatusCodeExt,
10    media_container::{
11        server::library::{
12            CollectionMetadataSubtype, LibraryType, Media as MediaMetadata, Metadata,
13            MetadataMediaContainer, MetadataType, Part as PartMetadata, PlaylistMetadataType,
14            Protocol, SearchType, ServerLibrary,
15        },
16        MediaContainerWrapper,
17    },
18    transcode::{
19        download_queue::{DownloadQueue, QueueItem},
20        session::{create_transcode_session, TranscodeSession},
21        Context, MusicTranscodeOptions, TranscodeOptions, VideoTranscodeOptions,
22    },
23    Error, HttpClient, Result,
24};
25
26pub trait FromMetadata {
27    /// Creates an item given the http configuration and item metadata. No
28    /// validation is performed that the metadata is correct.
29    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self;
30}
31
32/// Implements MetadataItem for the given struct which must only contain `client`
33/// and `metadata` fields.
34macro_rules! derive_from_metadata {
35    ($typ:ident) => {
36        impl FromMetadata for $typ {
37            fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
38                Self { client, metadata }
39            }
40        }
41    };
42}
43
44/// Functionality shared across different items types in the Plex library.
45#[enum_dispatch]
46pub trait MetadataItem {
47    /// Returns the Plex metadata for this item.
48    fn metadata(&self) -> &Metadata;
49    /// Returns the http client for this item.
50    fn client(&self) -> &HttpClient;
51
52    /// Returns the rating key for this item.
53    ///
54    /// This can be used to re-retrieve the item at a later time through the
55    /// Server::item_by_id function.
56    fn rating_key(&self) -> &str {
57        self.metadata().rating_key.as_str()
58    }
59
60    /// Returns the title of this item.
61    fn title(&self) -> &str {
62        &self.metadata().title
63    }
64}
65
66/// Implements MetadataItem for the given struct which must contain `client`
67/// and `metadata` fields.
68macro_rules! derive_metadata_item {
69    ($typ:ident) => {
70        impl MetadataItem for $typ {
71            fn metadata(&self) -> &Metadata {
72                &self.metadata
73            }
74
75            fn client(&self) -> &HttpClient {
76                &self.client
77            }
78        }
79    };
80    ($typ:ident<$gen:ident>) => {
81        impl<$gen> MetadataItem for $typ<$gen> {
82            fn metadata(&self) -> &Metadata {
83                &self.metadata
84            }
85
86            fn client(&self) -> &HttpClient {
87                &self.client
88            }
89        }
90    };
91}
92
93/// Retrieves a list of metadata items given the lookup key.
94#[tracing::instrument(level = "trace", skip(client))]
95pub(crate) async fn metadata_items<T>(client: &HttpClient, path: &str) -> Result<Vec<T>>
96where
97    T: FromMetadata,
98{
99    let wrapper: MediaContainerWrapper<MetadataMediaContainer> = client.get(path).json().await?;
100
101    let media = wrapper
102        .media_container
103        .metadata
104        .into_iter()
105        .map(|metadata| {
106            T::from_metadata(
107                client.clone(),
108                Metadata {
109                    library_section_id: metadata
110                        .library_section_id
111                        .or(wrapper.media_container.library_section_id),
112                    library_section_title: metadata
113                        .library_section_title
114                        .or(wrapper.media_container.library_section_title.clone()),
115                    ..metadata
116                },
117            )
118        })
119        .collect();
120    Ok(media)
121}
122
123/// Attempts to retrieve the parent of this item.
124#[tracing::instrument(level = "trace", skip_all, fields(item.rating_key = item.rating_key()))]
125async fn parent<T, P>(item: &T, client: &HttpClient) -> Result<Option<P>>
126where
127    T: MetadataItem,
128    P: FromMetadata,
129{
130    if let Some(ref parent_key) = item.metadata().parent.parent_key {
131        Ok(metadata_items(client, parent_key).await?.into_iter().next())
132    } else {
133        Ok(None)
134    }
135}
136
137/// Retrieves the metadata items from a pivot from a library.
138#[tracing::instrument(level = "trace", skip(client, directory), fields(directory.key = directory.key))]
139async fn pivot_items<M>(
140    client: &HttpClient,
141    directory: &ServerLibrary,
142    context: &str,
143) -> Result<Vec<M>>
144where
145    M: FromMetadata,
146{
147    if let Some(pivot) = directory.pivots.iter().find(|p| p.context == context) {
148        metadata_items(client, &pivot.key).await
149    } else {
150        Ok(Vec::new())
151    }
152}
153
154/// A single media format for a `MediaItem`.
155#[derive(Debug, Clone)]
156pub struct Media<'a, M: MediaItem> {
157    _options: PhantomData<M>,
158    client: &'a HttpClient,
159    media_index: usize,
160    media: &'a MediaMetadata,
161    parent_metadata: &'a Metadata,
162}
163
164impl<'a, M: MediaItem> Media<'a, M> {
165    /// The different parts that make up this media. They should be played in
166    /// order.
167    pub fn parts(&'_ self) -> Vec<Part<'_, M>> {
168        self.media
169            .parts
170            .iter()
171            .enumerate()
172            .map(|(index, part)| Part {
173                _options: self._options,
174                client: self.client,
175                media_index: self.media_index,
176                part_index: index,
177                parent_metadata: self.parent_metadata,
178                part,
179            })
180            .collect()
181    }
182
183    pub fn duration(&self) -> Option<u64> {
184        self.media.duration
185    }
186
187    /// The internal metadata for the media.
188    pub fn metadata(&self) -> &MediaMetadata {
189        self.media
190    }
191}
192
193impl<'a, M: Transcodable + MediaItem + Sync> Transcodable for Media<'a, M> {
194    type Options = M::Options;
195
196    #[tracing::instrument(level = "debug", skip_all)]
197    async fn create_download_session(&self, options: M::Options) -> Result<TranscodeSession> {
198        create_transcode_session(
199            self.client,
200            self.parent_metadata,
201            Context::Static,
202            Protocol::Http,
203            Some(self.media_index),
204            None,
205            options,
206        )
207        .await
208    }
209
210    #[tracing::instrument(level = "debug", skip_all)]
211    async fn create_streaming_session(
212        &self,
213        protocol: Protocol,
214        options: M::Options,
215    ) -> Result<TranscodeSession> {
216        create_transcode_session(
217            self.client,
218            self.parent_metadata,
219            Context::Streaming,
220            protocol,
221            Some(self.media_index),
222            None,
223            options,
224        )
225        .await
226    }
227
228    #[tracing::instrument(level = "debug", skip_all)]
229    async fn queue_download(
230        &self,
231        options: Self::Options,
232        download_queue: Option<&DownloadQueue>,
233    ) -> Result<QueueItem> {
234        let queue = if let Some(q) = download_queue {
235            q.clone()
236        } else {
237            DownloadQueue::get_or_create(self.client.clone()).await?
238        };
239
240        queue
241            .add_item(self.parent_metadata, Some(self.media_index), None, options)
242            .await
243    }
244}
245
246/// One part of a `Media`.
247#[derive(Debug, Clone)]
248pub struct Part<'a, M: MediaItem> {
249    _options: PhantomData<M>,
250    pub(crate) client: &'a HttpClient,
251    pub media_index: usize,
252    pub part_index: usize,
253    part: &'a PartMetadata,
254    parent_metadata: &'a Metadata,
255}
256
257impl<'a, M: MediaItem> Part<'a, M> {
258    /// The length of this file on disk in bytes.
259    #[allow(clippy::len_without_is_empty)]
260    pub fn len(&self) -> Option<u64> {
261        self.part.size
262    }
263
264    pub fn duration(&self) -> Option<u64> {
265        self.part.duration
266    }
267
268    /// Downloads the original media file for this part writing the data into
269    /// the provided writer. A range of bytes within the file can be requested
270    /// allowing for resumable transfers.
271    ///
272    /// Configured timeout value will be ignored during downloading.
273    #[tracing::instrument(level = "debug", skip_all)]
274    pub async fn download<W, R>(&self, writer: W, range: R) -> Result
275    where
276        W: AsyncWrite + Unpin,
277        R: RangeBounds<u64>,
278    {
279        let path = format!("{}?download=1", self.part.key.as_ref().unwrap());
280
281        let start = match range.start_bound() {
282            std::ops::Bound::Included(v) => *v,
283            std::ops::Bound::Excluded(v) => v + 1,
284            std::ops::Bound::Unbounded => 0,
285        };
286
287        let end = match range.end_bound() {
288            std::ops::Bound::Included(v) => Some(*v),
289            std::ops::Bound::Excluded(v) => Some(v - 1),
290            std::ops::Bound::Unbounded => None,
291        };
292
293        let mut builder = self.client.get(path).timeout(None);
294        if start != 0 || (end.is_some() && end != self.part.size) {
295            // We're requesting part of the file.
296            let end = end.map(|v| v.to_string()).unwrap_or_default();
297            builder = builder.header("Range", format!("bytes={start}-{end}"))
298        }
299
300        let mut response = builder.send().await?;
301        match response.status().as_http_status() {
302            StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
303                response.copy_to(writer).await?;
304                Ok(())
305            }
306            _ => Err(crate::Error::from_response(response).await),
307        }
308    }
309
310    /// The internal metadata for the media.
311    pub fn metadata(&self) -> &PartMetadata {
312        self.part
313    }
314}
315
316impl<'a, M: Transcodable + MediaItem + Sync> Transcodable for Part<'a, M> {
317    type Options = M::Options;
318
319    #[tracing::instrument(level = "debug", skip_all)]
320    async fn create_download_session(&self, options: M::Options) -> Result<TranscodeSession> {
321        create_transcode_session(
322            self.client,
323            self.parent_metadata,
324            Context::Static,
325            Protocol::Http,
326            Some(self.media_index),
327            Some(self.part_index),
328            options,
329        )
330        .await
331    }
332
333    #[tracing::instrument(level = "debug", skip_all)]
334    async fn create_streaming_session(
335        &self,
336        protocol: Protocol,
337        options: M::Options,
338    ) -> Result<TranscodeSession> {
339        create_transcode_session(
340            self.client,
341            self.parent_metadata,
342            Context::Streaming,
343            protocol,
344            Some(self.media_index),
345            Some(self.part_index),
346            options,
347        )
348        .await
349    }
350
351    #[tracing::instrument(level = "debug", skip_all)]
352    async fn queue_download(
353        &self,
354        options: Self::Options,
355        download_queue: Option<&DownloadQueue>,
356    ) -> Result<QueueItem> {
357        let queue = if let Some(q) = download_queue {
358            q.clone()
359        } else {
360            DownloadQueue::get_or_create(self.client.clone()).await?
361        };
362
363        queue
364            .add_item(
365                self.parent_metadata,
366                Some(self.media_index),
367                Some(self.part_index),
368                options,
369            )
370            .await
371    }
372}
373
374/// Represents some playable media. In Plex each playable item can be available
375/// in a number of different formats which in turn can be made up of a number of
376/// different parts.
377pub trait MediaItem: MetadataItem + Sized {
378    /// The different media formats that this item is available in.
379    fn media(&'_ self) -> Vec<Media<'_, Self>> {
380        let metadata = self.metadata();
381        if let Some(ref media) = metadata.media {
382            media
383                .iter()
384                .enumerate()
385                .map(|(index, media)| Media {
386                    _options: PhantomData,
387                    client: self.client(),
388                    media_index: index,
389                    parent_metadata: metadata,
390                    media,
391                })
392                .collect()
393        } else {
394            Vec::new()
395        }
396    }
397}
398
399pub trait Transcodable {
400    type Options: TranscodeOptions + Send;
401
402    /// Starts an offline transcode using the provided options.
403    ///
404    /// The server may refuse to transcode if the options suggest that the
405    /// original media file can be played back directly. Sometimes starting a
406    /// new download session will cancel an existing session.
407    ///
408    /// Can't be called on media other than Movie, Episode or Track.
409    fn create_download_session(
410        &self,
411        options: Self::Options,
412    ) -> impl Future<Output = Result<TranscodeSession>> + Send;
413
414    /// Starts a streaming transcode using of the given media part using the
415    /// streaming protocol and provided options.
416    ///
417    /// Can't be called on media other than Movie, Episode or Track.
418    fn create_streaming_session(
419        &self,
420        protocol: Protocol,
421        options: Self::Options,
422    ) -> impl Future<Output = Result<TranscodeSession>> + Send;
423
424    /// Queues this item for download using the provided download queue.
425    ///
426    /// This is a newer API and should be preferred over
427    /// `create_download_session`. Many items can be added to the same download
428    /// queue, the server will automatically transcode them if necessary based
429    /// on the limits set in the server settings.
430    fn queue_download(
431        &self,
432        options: Self::Options,
433        download_queue: Option<&DownloadQueue>,
434    ) -> impl Future<Output = Result<QueueItem>> + Send;
435}
436
437/// A video that can be included in a video playlist.
438#[enum_dispatch(MetadataItem)]
439#[derive(Debug, Clone)]
440pub enum Video {
441    Movie,
442    Episode,
443}
444
445impl FromMetadata for Video {
446    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
447        if let Some(MetadataType::Episode) = metadata.metadata_type {
448            Episode::from_metadata(client, metadata).into()
449        } else {
450            Movie::from_metadata(client, metadata).into()
451        }
452    }
453}
454
455impl MediaItem for Video {}
456impl Transcodable for Video {
457    type Options = VideoTranscodeOptions;
458
459    #[tracing::instrument(level = "debug", skip_all)]
460    async fn create_download_session(&self, options: Self::Options) -> Result<TranscodeSession> {
461        create_transcode_session(
462            self.client(),
463            self.metadata(),
464            Context::Static,
465            Protocol::Http,
466            None,
467            None,
468            options,
469        )
470        .await
471    }
472
473    #[tracing::instrument(level = "debug", skip_all)]
474    async fn create_streaming_session(
475        &self,
476        protocol: Protocol,
477        options: Self::Options,
478    ) -> Result<TranscodeSession> {
479        create_transcode_session(
480            self.client(),
481            self.metadata(),
482            Context::Streaming,
483            protocol,
484            None,
485            None,
486            options,
487        )
488        .await
489    }
490
491    #[tracing::instrument(level = "debug", skip_all)]
492    async fn queue_download(
493        &self,
494        options: Self::Options,
495        download_queue: Option<&DownloadQueue>,
496    ) -> Result<QueueItem> {
497        let queue = if let Some(q) = download_queue {
498            q.clone()
499        } else {
500            DownloadQueue::get_or_create(self.client().clone()).await?
501        };
502
503        queue.add_item(self.metadata(), None, None, options).await
504    }
505}
506
507#[derive(Debug, Clone)]
508pub struct Playlist<M> {
509    _items: PhantomData<M>,
510    client: HttpClient,
511    metadata: Metadata,
512}
513
514impl<M> FromMetadata for Playlist<M> {
515    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
516        Self {
517            _items: PhantomData,
518            client,
519            metadata,
520        }
521    }
522}
523
524derive_metadata_item!(Playlist<M>);
525
526impl<M> Playlist<M>
527where
528    M: FromMetadata,
529{
530    #[tracing::instrument(level = "debug", skip_all)]
531    pub async fn children(&self) -> Result<Vec<M>> {
532        metadata_items(&self.client, &self.metadata.key).await
533    }
534}
535
536#[derive(Debug, Clone)]
537pub struct Collection<M> {
538    _items: PhantomData<M>,
539    client: HttpClient,
540    metadata: Metadata,
541}
542
543impl<M> FromMetadata for Collection<M> {
544    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
545        Self {
546            _items: PhantomData,
547            client,
548            metadata,
549        }
550    }
551}
552
553derive_metadata_item!(Collection<M>);
554
555impl<M> Collection<M>
556where
557    M: FromMetadata,
558{
559    #[tracing::instrument(level = "debug", skip_all)]
560    pub async fn children(&self) -> Result<Vec<M>> {
561        metadata_items(&self.client, &self.metadata.key).await
562    }
563}
564
565#[derive(Debug, Clone)]
566pub struct Movie {
567    client: HttpClient,
568    metadata: Metadata,
569}
570
571derive_from_metadata!(Movie);
572derive_metadata_item!(Movie);
573
574impl MediaItem for Movie {}
575impl Transcodable for Movie {
576    type Options = VideoTranscodeOptions;
577
578    #[tracing::instrument(level = "debug", skip_all)]
579    async fn create_download_session(&self, options: Self::Options) -> Result<TranscodeSession> {
580        create_transcode_session(
581            self.client(),
582            self.metadata(),
583            Context::Static,
584            Protocol::Http,
585            None,
586            None,
587            options,
588        )
589        .await
590    }
591
592    #[tracing::instrument(level = "debug", skip_all)]
593    async fn create_streaming_session(
594        &self,
595        protocol: Protocol,
596        options: Self::Options,
597    ) -> Result<TranscodeSession> {
598        create_transcode_session(
599            self.client(),
600            self.metadata(),
601            Context::Streaming,
602            protocol,
603            None,
604            None,
605            options,
606        )
607        .await
608    }
609
610    #[tracing::instrument(level = "debug", skip_all)]
611    async fn queue_download(
612        &self,
613        options: Self::Options,
614        download_queue: Option<&DownloadQueue>,
615    ) -> Result<QueueItem> {
616        let queue = if let Some(q) = download_queue {
617            q.clone()
618        } else {
619            DownloadQueue::get_or_create(self.client.clone()).await?
620        };
621
622        queue.add_item(self.metadata(), None, None, options).await
623    }
624}
625
626#[derive(Debug, Clone)]
627pub struct Show {
628    client: HttpClient,
629    metadata: Metadata,
630}
631
632derive_from_metadata!(Show);
633derive_metadata_item!(Show);
634
635impl Show {
636    /// Retrieves all of the seasons of this show.
637    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
638    pub async fn seasons(&self) -> Result<Vec<Season>> {
639        metadata_items(&self.client, &self.metadata.key).await
640    }
641
642    /// Retrieves all of the episodes in all seasons of this show.
643    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.rating_key = self.metadata.rating_key))]
644    pub async fn episodes(&self) -> Result<Vec<Episode>> {
645        let path = format!("/library/metadata/{}/allLeaves", self.metadata.rating_key);
646        metadata_items(&self.client, &path).await
647    }
648}
649
650#[derive(Debug, Clone)]
651pub struct Season {
652    client: HttpClient,
653    metadata: Metadata,
654}
655
656derive_from_metadata!(Season);
657derive_metadata_item!(Season);
658
659impl Season {
660    pub fn season_number(&self) -> Option<u32> {
661        self.metadata.index
662    }
663
664    /// Retrieves all of the episodes in this season.
665    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
666    pub async fn episodes(&self) -> Result<Vec<Episode>> {
667        metadata_items(&self.client, &self.metadata.key).await
668    }
669
670    /// Retrieves the show that this season is from.
671    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
672    pub async fn show(&self) -> Result<Option<Show>> {
673        parent(self, &self.client).await
674    }
675}
676
677#[derive(Debug, Clone)]
678pub struct Episode {
679    client: HttpClient,
680    metadata: Metadata,
681}
682
683derive_from_metadata!(Episode);
684derive_metadata_item!(Episode);
685
686impl MediaItem for Episode {}
687impl Transcodable for Episode {
688    type Options = VideoTranscodeOptions;
689
690    #[tracing::instrument(level = "debug", skip_all)]
691    async fn create_download_session(&self, options: Self::Options) -> Result<TranscodeSession> {
692        create_transcode_session(
693            self.client(),
694            self.metadata(),
695            Context::Static,
696            Protocol::Http,
697            None,
698            None,
699            options,
700        )
701        .await
702    }
703
704    #[tracing::instrument(level = "debug", skip_all)]
705    async fn create_streaming_session(
706        &self,
707        protocol: Protocol,
708        options: Self::Options,
709    ) -> Result<TranscodeSession> {
710        create_transcode_session(
711            self.client(),
712            self.metadata(),
713            Context::Streaming,
714            protocol,
715            None,
716            None,
717            options,
718        )
719        .await
720    }
721
722    #[tracing::instrument(level = "debug", skip_all)]
723    async fn queue_download(
724        &self,
725        options: Self::Options,
726        download_queue: Option<&DownloadQueue>,
727    ) -> Result<QueueItem> {
728        let queue = if let Some(q) = download_queue {
729            q.clone()
730        } else {
731            DownloadQueue::get_or_create(self.client.clone()).await?
732        };
733
734        queue.add_item(self.metadata(), None, None, options).await
735    }
736}
737
738impl Episode {
739    /// Returns the number of this season within the show.
740    pub fn season_number(&self) -> Option<u32> {
741        self.metadata.parent.parent_index
742    }
743
744    /// Returns the number of this episode within the season.
745    pub fn episode_number(&self) -> Option<u32> {
746        self.metadata.index
747    }
748
749    /// Retrieves the season that this episode is from.
750    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
751    pub async fn season(&self) -> Result<Option<Season>> {
752        parent(self, &self.client).await
753    }
754}
755
756#[derive(Debug, Clone)]
757pub struct Artist {
758    client: HttpClient,
759    metadata: Metadata,
760}
761
762derive_from_metadata!(Artist);
763derive_metadata_item!(Artist);
764
765impl Artist {
766    /// Retrieves all of the fully-featured studio albums (skipping Lives, EPs, etc.) by this artist.
767    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
768    pub async fn full_studio_albums(&self) -> Result<Vec<MusicAlbum>> {
769        metadata_items(&self.client, &self.metadata.key).await
770    }
771
772    /// Retrieves all of the albums by this artist.
773    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
774    pub async fn albums(&self) -> Result<Vec<MusicAlbum>> {
775        let section_id = match &self.metadata.library_section_id {
776            Some(id) => id,
777            None => return Err(Error::UnexpectedError),
778        };
779
780        let albums_search_path = format!(
781            "/library/sections/{}/all?type={}&artist.id={}",
782            section_id,
783            SearchType::Album,
784            self.metadata.rating_key
785        );
786        metadata_items(&self.client, &albums_search_path).await
787    }
788}
789
790#[derive(Debug, Clone)]
791pub struct MusicAlbum {
792    client: HttpClient,
793    metadata: Metadata,
794}
795
796derive_from_metadata!(MusicAlbum);
797derive_metadata_item!(MusicAlbum);
798
799impl MusicAlbum {
800    /// Retrieves all of the tracks in this album.
801    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
802    pub async fn tracks(&self) -> Result<Vec<Track>> {
803        metadata_items(&self.client, &self.metadata.key).await
804    }
805
806    /// Retrieves the artist for this album.
807    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
808    pub async fn artist(&self) -> Result<Option<Artist>> {
809        parent(self, &self.client).await
810    }
811}
812
813#[derive(Debug, Clone)]
814pub struct Track {
815    client: HttpClient,
816    metadata: Metadata,
817}
818
819derive_from_metadata!(Track);
820derive_metadata_item!(Track);
821
822impl MediaItem for Track {}
823impl Transcodable for Track {
824    type Options = MusicTranscodeOptions;
825
826    #[tracing::instrument(level = "debug", skip_all)]
827    async fn create_download_session(&self, options: Self::Options) -> Result<TranscodeSession> {
828        create_transcode_session(
829            self.client(),
830            self.metadata(),
831            Context::Static,
832            Protocol::Http,
833            None,
834            None,
835            options,
836        )
837        .await
838    }
839
840    #[tracing::instrument(level = "debug", skip_all)]
841    async fn create_streaming_session(
842        &self,
843        protocol: Protocol,
844        options: Self::Options,
845    ) -> Result<TranscodeSession> {
846        create_transcode_session(
847            self.client(),
848            self.metadata(),
849            Context::Streaming,
850            protocol,
851            None,
852            None,
853            options,
854        )
855        .await
856    }
857
858    #[tracing::instrument(level = "debug", skip_all)]
859    async fn queue_download(
860        &self,
861        options: Self::Options,
862        download_queue: Option<&DownloadQueue>,
863    ) -> Result<QueueItem> {
864        let queue = if let Some(q) = download_queue {
865            q.clone()
866        } else {
867            DownloadQueue::get_or_create(self.client.clone()).await?
868        };
869
870        queue.add_item(self.metadata(), None, None, options).await
871    }
872}
873
874impl Track {
875    /// Returns the number of this track within the album.
876    pub fn track_number(&self) -> Option<u32> {
877        self.metadata.index
878    }
879
880    /// Retrieves the album for this track.
881    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
882    pub async fn album(&self) -> Result<Option<MusicAlbum>> {
883        parent(self, &self.client).await
884    }
885}
886
887#[derive(Debug, Clone)]
888pub struct Photo {
889    client: HttpClient,
890    metadata: Metadata,
891}
892
893derive_from_metadata!(Photo);
894derive_metadata_item!(Photo);
895
896impl MediaItem for Photo {}
897
898impl Photo {
899    /// Retrieves the album that this photo is in.
900    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
901    pub async fn album(&self) -> Result<Option<PhotoAlbum>> {
902        parent(self, &self.client).await
903    }
904}
905
906#[derive(Debug, Clone)]
907pub struct PhotoAlbum {
908    client: HttpClient,
909    metadata: Metadata,
910}
911
912derive_from_metadata!(PhotoAlbum);
913derive_metadata_item!(PhotoAlbum);
914
915impl PhotoAlbum {
916    /// Retrieves all of the albums and photos in this album.
917    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
918    pub async fn contents(&self) -> Result<Vec<PhotoAlbumItem>> {
919        metadata_items(&self.client, &self.metadata.key).await
920    }
921
922    /// Retrieves the album that this album is in.
923    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
924    pub async fn album(&self) -> Result<Option<PhotoAlbum>> {
925        parent(self, &self.client).await
926    }
927}
928
929#[enum_dispatch(MetadataItem)]
930pub enum PhotoAlbumItem {
931    PhotoAlbum,
932    Photo,
933}
934
935impl FromMetadata for PhotoAlbumItem {
936    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
937        // This isn't a great test but there doesn't seem to be much better.
938        if metadata.key.ends_with("/children") {
939            PhotoAlbum::from_metadata(client, metadata).into()
940        } else {
941            Photo::from_metadata(client, metadata).into()
942        }
943    }
944}
945
946#[derive(Debug, Clone)]
947pub struct Clip {
948    client: HttpClient,
949    metadata: Metadata,
950}
951
952derive_from_metadata!(Clip);
953derive_metadata_item!(Clip);
954
955impl MediaItem for Clip {}
956
957#[derive(Debug, Clone)]
958pub struct UnknownItem {
959    client: HttpClient,
960    metadata: Metadata,
961}
962
963derive_from_metadata!(UnknownItem);
964derive_metadata_item!(UnknownItem);
965
966#[enum_dispatch(MetadataItem)]
967#[derive(Debug, Clone)]
968pub enum Item {
969    Movie,
970    Episode,
971    Photo,
972    Show,
973    Artist,
974    MusicAlbum,
975    Season,
976    Track,
977    Clip,
978    MovieCollection(Collection<Movie>),
979    ShowCollection(Collection<Show>),
980    VideoPlaylist(Playlist<Video>),
981    PhotoPlaylist(Playlist<Photo>),
982    MusicPlaylist(Playlist<Track>),
983    UnknownItem,
984}
985
986impl MediaItem for Item {}
987
988impl FromMetadata for Item {
989    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
990        if let Some(ref item_type) = metadata.metadata_type {
991            match item_type {
992                MetadataType::Movie => Movie::from_metadata(client, metadata).into(),
993                MetadataType::Episode => Episode::from_metadata(client, metadata).into(),
994                MetadataType::Photo => Photo::from_metadata(client, metadata).into(),
995                MetadataType::Show => Show::from_metadata(client, metadata).into(),
996                MetadataType::Artist => Artist::from_metadata(client, metadata).into(),
997                MetadataType::MusicAlbum => MusicAlbum::from_metadata(client, metadata).into(),
998                MetadataType::Season => Season::from_metadata(client, metadata).into(),
999                MetadataType::Track => Track::from_metadata(client, metadata).into(),
1000                MetadataType::Clip(_) => Clip::from_metadata(client, metadata).into(),
1001                MetadataType::Collection(CollectionMetadataSubtype::Movie) => {
1002                    Collection::<Movie>::from_metadata(client, metadata).into()
1003                }
1004                MetadataType::Collection(CollectionMetadataSubtype::Show) => {
1005                    Collection::<Show>::from_metadata(client, metadata).into()
1006                }
1007                MetadataType::Playlist(PlaylistMetadataType::Video) => {
1008                    Playlist::<Video>::from_metadata(client, metadata).into()
1009                }
1010                MetadataType::Playlist(PlaylistMetadataType::Audio) => {
1011                    Playlist::<Track>::from_metadata(client, metadata).into()
1012                }
1013                MetadataType::Playlist(PlaylistMetadataType::Photo) => {
1014                    Playlist::<Photo>::from_metadata(client, metadata).into()
1015                }
1016                #[cfg(not(feature = "tests_deny_unknown_fields"))]
1017                _ => UnknownItem::from_metadata(client, metadata).into(),
1018            }
1019        } else {
1020            UnknownItem::from_metadata(client, metadata).into()
1021        }
1022    }
1023}
1024
1025#[derive(Debug, Clone)]
1026pub struct MovieLibrary {
1027    client: HttpClient,
1028    directory: ServerLibrary,
1029}
1030
1031impl MovieLibrary {
1032    /// Returns the title of this library.
1033    pub fn title(&self) -> &str {
1034        &self.directory.title
1035    }
1036
1037    /// Retrieves all of the movies in this library.
1038    #[tracing::instrument(level = "debug", skip_all)]
1039    pub async fn movies(&self) -> Result<Vec<Movie>> {
1040        pivot_items(&self.client, &self.directory, "content.library").await
1041    }
1042
1043    /// Retrieves all of the collections in this library.
1044    #[tracing::instrument(level = "debug", skip_all)]
1045    pub async fn collections(&self) -> Result<Vec<Collection<Movie>>> {
1046        pivot_items(&self.client, &self.directory, "content.collections").await
1047    }
1048
1049    /// Retrieves all of the playlists containing movies from this library.
1050    #[tracing::instrument(level = "debug", skip_all)]
1051    pub async fn playlists(&self) -> Result<Vec<Playlist<Video>>> {
1052        pivot_items(&self.client, &self.directory, "content.playlists").await
1053    }
1054}
1055
1056#[derive(Debug, Clone)]
1057pub struct TVLibrary {
1058    client: HttpClient,
1059    directory: ServerLibrary,
1060}
1061
1062impl TVLibrary {
1063    /// Returns the title of this library.
1064    pub fn title(&self) -> &str {
1065        &self.directory.title
1066    }
1067
1068    /// Retrieves all of the shows in this library.
1069    #[tracing::instrument(level = "debug", skip_all)]
1070    pub async fn shows(&self) -> Result<Vec<Show>> {
1071        pivot_items(&self.client, &self.directory, "content.library").await
1072    }
1073
1074    /// Retrieves all of the collections in this library.
1075    #[tracing::instrument(level = "debug", skip_all)]
1076    pub async fn collections(&self) -> Result<Vec<Collection<Show>>> {
1077        pivot_items(&self.client, &self.directory, "content.collections").await
1078    }
1079
1080    /// Retrieves all of the playlists containing episodes from this library.
1081    #[tracing::instrument(level = "debug", skip_all)]
1082    pub async fn playlists(&self) -> Result<Vec<Playlist<Video>>> {
1083        pivot_items(&self.client, &self.directory, "content.playlists").await
1084    }
1085}
1086
1087#[derive(Debug, Clone)]
1088pub struct MusicLibrary {
1089    client: HttpClient,
1090    directory: ServerLibrary,
1091}
1092
1093impl MusicLibrary {
1094    /// Returns the title of this library.
1095    pub fn title(&self) -> &str {
1096        &self.directory.title
1097    }
1098
1099    /// Retrieves all of the artists in this library.
1100    #[tracing::instrument(level = "debug", skip_all)]
1101    pub async fn artists(&self) -> Result<Vec<Artist>> {
1102        pivot_items(&self.client, &self.directory, "content.library").await
1103    }
1104
1105    /// Retrieves all of the playlists containing tracks from this library.
1106    #[tracing::instrument(level = "debug", skip_all)]
1107    pub async fn playlists(&self) -> Result<Vec<Playlist<Track>>> {
1108        pivot_items(&self.client, &self.directory, "content.playlists").await
1109    }
1110}
1111
1112#[derive(Debug, Clone)]
1113pub struct PhotoLibrary {
1114    client: HttpClient,
1115    directory: ServerLibrary,
1116}
1117
1118impl PhotoLibrary {
1119    /// Returns the title of this library.
1120    pub fn title(&self) -> &str {
1121        &self.directory.title
1122    }
1123
1124    /// Retrieves all of the albums in this library.
1125    #[tracing::instrument(level = "debug", skip_all)]
1126    pub async fn albums(&self) -> Result<Vec<PhotoAlbum>> {
1127        pivot_items(&self.client, &self.directory, "content.library").await
1128    }
1129
1130    /// Retrieves all of the playlists containing photos from this library.
1131    #[tracing::instrument(level = "debug", skip_all)]
1132    pub async fn playlists(&self) -> Result<Vec<Playlist<Photo>>> {
1133        pivot_items(&self.client, &self.directory, "content.playlists").await
1134    }
1135}
1136
1137#[derive(Debug, Clone)]
1138pub enum Library {
1139    Movie(MovieLibrary),
1140    TV(TVLibrary),
1141    Music(MusicLibrary),
1142    Video(MovieLibrary),
1143    Photo(PhotoLibrary),
1144}
1145
1146impl Library {
1147    pub(super) fn new(client: HttpClient, directory: ServerLibrary) -> Self {
1148        match directory.library_type {
1149            LibraryType::Movie => {
1150                if directory.subtype.as_deref() == Some("clip") {
1151                    Library::Video(MovieLibrary { client, directory })
1152                } else {
1153                    Library::Movie(MovieLibrary { client, directory })
1154                }
1155            }
1156            LibraryType::Show => Library::TV(TVLibrary { client, directory }),
1157            LibraryType::Artist => Library::Music(MusicLibrary { client, directory }),
1158            LibraryType::Photo => Library::Photo(PhotoLibrary { client, directory }),
1159            LibraryType::Mixed => todo!("Mixed library type is not supported yet"),
1160            LibraryType::Clip => todo!("Clip library type is not supported yet"),
1161            #[cfg(not(feature = "tests_deny_unknown_fields"))]
1162            LibraryType::Unknown => panic!("Unknown library type"),
1163        }
1164    }
1165
1166    fn directory(&self) -> &ServerLibrary {
1167        match self {
1168            Self::Movie(l) => &l.directory,
1169            Self::TV(l) => &l.directory,
1170            Self::Music(l) => &l.directory,
1171            Self::Video(l) => &l.directory,
1172            Self::Photo(l) => &l.directory,
1173        }
1174    }
1175
1176    /// Returns the unique ID of this library.
1177    pub fn id(&self) -> &str {
1178        &self.directory().id
1179    }
1180
1181    /// Returns the title of this library.
1182    pub fn title(&self) -> &str {
1183        &self.directory().title
1184    }
1185
1186    pub fn library_type(&self) -> &LibraryType {
1187        &self.directory().library_type
1188    }
1189}