plex_api/server/
library.rs

1use std::{marker::PhantomData, ops::RangeBounds};
2
3use enum_dispatch::enum_dispatch;
4use futures::AsyncWrite;
5use http::StatusCode;
6use isahc::AsyncReadResponseExt;
7
8use crate::{
9    media_container::{
10        server::library::{
11            CollectionMetadataSubtype, LibraryType, Media as MediaMetadata, Metadata,
12            MetadataMediaContainer, MetadataType, Part as PartMetadata, PlaylistMetadataType,
13            Protocol, SearchType, ServerLibrary,
14        },
15        MediaContainerWrapper,
16    },
17    transcode::{MusicTranscodeOptions, TranscodeSession, VideoTranscodeOptions},
18    Error, HttpClient, Result,
19};
20
21use super::transcode::{create_transcode_session, Context, TranscodeOptions};
22
23pub trait FromMetadata {
24    /// Creates an item given the http configuration and item metadata. No
25    /// validation is performed that the metadata is correct.
26    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self;
27}
28
29/// Implements MetadataItem for the given struct which must only contain `client`
30/// and `metadata` fields.
31macro_rules! derive_from_metadata {
32    ($typ:ident) => {
33        impl FromMetadata for $typ {
34            fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
35                Self { client, metadata }
36            }
37        }
38    };
39}
40
41/// Functionality shared across different items types in the Plex library.
42#[enum_dispatch]
43pub trait MetadataItem {
44    /// Returns the Plex metadata for this item.
45    fn metadata(&self) -> &Metadata;
46    /// Returns the http client for this item.
47    fn client(&self) -> &HttpClient;
48
49    /// Returns the rating key for this item.
50    ///
51    /// This can be used to re-retrieve the item at a later time through the
52    /// Server::item_by_id function.
53    fn rating_key(&self) -> &str {
54        self.metadata().rating_key.as_str()
55    }
56
57    /// Returns the title of this item.
58    fn title(&self) -> &str {
59        &self.metadata().title
60    }
61}
62
63/// Implements MetadataItem for the given struct which must contain `client`
64/// and `metadata` fields.
65macro_rules! derive_metadata_item {
66    ($typ:ident) => {
67        impl MetadataItem for $typ {
68            fn metadata(&self) -> &Metadata {
69                &self.metadata
70            }
71
72            fn client(&self) -> &HttpClient {
73                &self.client
74            }
75        }
76    };
77    ($typ:ident<$gen:ident>) => {
78        impl<$gen> MetadataItem for $typ<$gen> {
79            fn metadata(&self) -> &Metadata {
80                &self.metadata
81            }
82
83            fn client(&self) -> &HttpClient {
84                &self.client
85            }
86        }
87    };
88}
89
90/// Retrieves a list of metadata items given the lookup key.
91#[tracing::instrument(level = "trace", skip(client))]
92pub(crate) async fn metadata_items<T>(client: &HttpClient, path: &str) -> Result<Vec<T>>
93where
94    T: FromMetadata,
95{
96    let wrapper: MediaContainerWrapper<MetadataMediaContainer> = client.get(path).json().await?;
97
98    let media = wrapper
99        .media_container
100        .metadata
101        .into_iter()
102        .map(|metadata| {
103            T::from_metadata(
104                client.clone(),
105                Metadata {
106                    library_section_id: metadata
107                        .library_section_id
108                        .or(wrapper.media_container.library_section_id),
109                    library_section_title: metadata
110                        .library_section_title
111                        .or(wrapper.media_container.library_section_title.clone()),
112                    ..metadata
113                },
114            )
115        })
116        .collect();
117    Ok(media)
118}
119
120/// Attempts to retrieve the parent of this item.
121#[tracing::instrument(level = "trace", skip_all, fields(item.rating_key = item.rating_key()))]
122async fn parent<T, P>(item: &T, client: &HttpClient) -> Result<Option<P>>
123where
124    T: MetadataItem,
125    P: FromMetadata,
126{
127    if let Some(ref parent_key) = item.metadata().parent.parent_key {
128        Ok(metadata_items(client, parent_key).await?.into_iter().next())
129    } else {
130        Ok(None)
131    }
132}
133
134/// Retrieves the metadata items from a pivot from a library.
135#[tracing::instrument(level = "trace", skip(client, directory), fields(directory.key = directory.key))]
136async fn pivot_items<M>(
137    client: &HttpClient,
138    directory: &ServerLibrary,
139    context: &str,
140) -> Result<Vec<M>>
141where
142    M: FromMetadata,
143{
144    if let Some(pivot) = directory.pivots.iter().find(|p| p.context == context) {
145        metadata_items(client, &pivot.key).await
146    } else {
147        Ok(Vec::new())
148    }
149}
150
151/// A single media format for a `MediaItem`.
152#[derive(Debug, Clone)]
153pub struct Media<'a, M: MediaItem> {
154    _options: PhantomData<M>,
155    client: &'a HttpClient,
156    media_index: usize,
157    media: &'a MediaMetadata,
158    parent_metadata: &'a Metadata,
159}
160
161impl<'a, M: MediaItem> Media<'a, M> {
162    /// The different parts that make up this media. They should be played in
163    /// order.
164    pub fn parts(&self) -> Vec<Part<M>> {
165        self.media
166            .parts
167            .iter()
168            .enumerate()
169            .map(|(index, part)| Part {
170                _options: self._options,
171                client: self.client,
172                media_index: self.media_index,
173                part_index: index,
174                parent_metadata: self.parent_metadata,
175                part,
176            })
177            .collect()
178    }
179
180    /// The internal metadata for the media.
181    pub fn metadata(&self) -> &MediaMetadata {
182        self.media
183    }
184}
185
186/// One part of a `Media`.
187#[derive(Debug, Clone)]
188pub struct Part<'a, M: MediaItem> {
189    _options: PhantomData<M>,
190    pub(crate) client: &'a HttpClient,
191    pub media_index: usize,
192    pub part_index: usize,
193    part: &'a PartMetadata,
194    parent_metadata: &'a Metadata,
195}
196
197impl<'a, M: MediaItem> Part<'a, M> {
198    /// The length of this file on disk in bytes.
199    #[allow(clippy::len_without_is_empty)]
200    pub fn len(&self) -> Option<u64> {
201        self.part.size
202    }
203
204    /// Downloads the original media file for this part writing the data into
205    /// the provided writer. A range of bytes within the file can be requested
206    /// allowing for resumable transfers.
207    ///
208    /// Configured timeout value will be ignored during downloading.
209    #[tracing::instrument(level = "debug", skip_all)]
210    pub async fn download<W, R>(&self, writer: W, range: R) -> Result
211    where
212        W: AsyncWrite + Unpin,
213        R: RangeBounds<u64>,
214    {
215        let path = format!("{}?download=1", self.part.key.as_ref().unwrap());
216
217        let start = match range.start_bound() {
218            std::ops::Bound::Included(v) => *v,
219            std::ops::Bound::Excluded(v) => v + 1,
220            std::ops::Bound::Unbounded => 0,
221        };
222
223        let end = match range.end_bound() {
224            std::ops::Bound::Included(v) => Some(*v),
225            std::ops::Bound::Excluded(v) => Some(v - 1),
226            std::ops::Bound::Unbounded => None,
227        };
228
229        let mut builder = self.client.get(path).timeout(None);
230        if start != 0 || (end.is_some() && end != self.part.size) {
231            // We're requesting part of the file.
232            let end = end.map(|v| v.to_string()).unwrap_or_default();
233            builder = builder.header("Range", format!("bytes={start}-{end}"))
234        }
235
236        let mut response = builder.send().await?;
237        match response.status() {
238            StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
239                response.copy_to(writer).await?;
240                Ok(())
241            }
242            _ => Err(crate::Error::from_response(response).await),
243        }
244    }
245
246    /// The internal metadata for the media.
247    pub fn metadata(&self) -> &PartMetadata {
248        self.part
249    }
250}
251
252impl<'a, M: MediaItemWithTranscoding> Part<'a, M> {
253    /// Starts an offline transcode using the provided options.
254    ///
255    /// The server may refuse to transcode if the options suggest that the
256    /// original media file can be played back directly.
257    ///
258    /// Can't be called on media other than Movie, Episode or Track.
259    #[tracing::instrument(level = "debug", skip_all)]
260    pub async fn create_download_session(&self, options: M::Options) -> Result<TranscodeSession> {
261        create_transcode_session(
262            self.parent_metadata,
263            self,
264            Context::Static,
265            Protocol::Http,
266            options,
267        )
268        .await
269    }
270
271    /// Starts a streaming transcode using of the given media part using the
272    /// streaming protocol and provided options.
273    ///
274    /// Can't be called on media other than Movie, Episode or Track.
275    #[tracing::instrument(level = "debug", skip_all)]
276    pub async fn create_streaming_session(
277        &self,
278        protocol: Protocol,
279        options: M::Options,
280    ) -> Result<TranscodeSession> {
281        create_transcode_session(
282            self.parent_metadata,
283            self,
284            Context::Streaming,
285            protocol,
286            options,
287        )
288        .await
289    }
290}
291
292/// Represents some playable media. In Plex each playable item can be available
293/// in a number of different formats which in turn can be made up of a number of
294/// different parts.
295pub trait MediaItem: MetadataItem + Sized {
296    /// The different media formats that this item is available in.
297    fn media(&self) -> Vec<Media<Self>> {
298        let metadata = self.metadata();
299        if let Some(ref media) = metadata.media {
300            media
301                .iter()
302                .enumerate()
303                .map(|(index, media)| Media {
304                    _options: PhantomData,
305                    client: self.client(),
306                    media_index: index,
307                    parent_metadata: metadata,
308                    media,
309                })
310                .collect()
311        } else {
312            Vec::new()
313        }
314    }
315}
316
317pub trait MediaItemWithTranscoding: MediaItem {
318    type Options: TranscodeOptions;
319}
320
321/// A video that can be included in a video playlist.
322#[enum_dispatch(MetadataItem)]
323#[derive(Debug, Clone)]
324pub enum Video {
325    Movie,
326    Episode,
327}
328
329impl FromMetadata for Video {
330    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
331        if let Some(MetadataType::Episode) = metadata.metadata_type {
332            Episode::from_metadata(client, metadata).into()
333        } else {
334            Movie::from_metadata(client, metadata).into()
335        }
336    }
337}
338
339impl MediaItem for Video {}
340impl MediaItemWithTranscoding for Video {
341    type Options = VideoTranscodeOptions;
342}
343
344#[derive(Debug, Clone)]
345pub struct Playlist<M> {
346    _items: PhantomData<M>,
347    client: HttpClient,
348    metadata: Metadata,
349}
350
351impl<M> FromMetadata for Playlist<M> {
352    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
353        Self {
354            _items: PhantomData,
355            client,
356            metadata,
357        }
358    }
359}
360
361derive_metadata_item!(Playlist<M>);
362
363impl<M> Playlist<M>
364where
365    M: FromMetadata,
366{
367    #[tracing::instrument(level = "debug", skip_all)]
368    pub async fn children(&self) -> Result<Vec<M>> {
369        metadata_items(&self.client, &self.metadata.key).await
370    }
371}
372
373#[derive(Debug, Clone)]
374pub struct Collection<M> {
375    _items: PhantomData<M>,
376    client: HttpClient,
377    metadata: Metadata,
378}
379
380impl<M> FromMetadata for Collection<M> {
381    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
382        Self {
383            _items: PhantomData,
384            client,
385            metadata,
386        }
387    }
388}
389
390derive_metadata_item!(Collection<M>);
391
392impl<M> Collection<M>
393where
394    M: FromMetadata,
395{
396    #[tracing::instrument(level = "debug", skip_all)]
397    pub async fn children(&self) -> Result<Vec<M>> {
398        metadata_items(&self.client, &self.metadata.key).await
399    }
400}
401
402#[derive(Debug, Clone)]
403pub struct Movie {
404    client: HttpClient,
405    metadata: Metadata,
406}
407
408derive_from_metadata!(Movie);
409derive_metadata_item!(Movie);
410
411impl MediaItem for Movie {}
412impl MediaItemWithTranscoding for Movie {
413    type Options = VideoTranscodeOptions;
414}
415
416#[derive(Debug, Clone)]
417pub struct Show {
418    client: HttpClient,
419    metadata: Metadata,
420}
421
422derive_from_metadata!(Show);
423derive_metadata_item!(Show);
424
425impl Show {
426    /// Retrieves all of the seasons of this show.
427    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
428    pub async fn seasons(&self) -> Result<Vec<Season>> {
429        metadata_items(&self.client, &self.metadata.key).await
430    }
431
432    /// Retrieves all of the episodes in all seasons of this show.
433    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.rating_key = self.metadata.rating_key))]
434    pub async fn episodes(&self) -> Result<Vec<Episode>> {
435        let path = format!("/library/metadata/{}/allLeaves", self.metadata.rating_key);
436        metadata_items(&self.client, &path).await
437    }
438}
439
440#[derive(Debug, Clone)]
441pub struct Season {
442    client: HttpClient,
443    metadata: Metadata,
444}
445
446derive_from_metadata!(Season);
447derive_metadata_item!(Season);
448
449impl Season {
450    pub fn season_number(&self) -> Option<u32> {
451        self.metadata.index
452    }
453
454    /// Retrieves all of the episodes in this season.
455    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
456    pub async fn episodes(&self) -> Result<Vec<Episode>> {
457        metadata_items(&self.client, &self.metadata.key).await
458    }
459
460    /// Retrieves the show that this season is from.
461    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
462    pub async fn show(&self) -> Result<Option<Show>> {
463        parent(self, &self.client).await
464    }
465}
466
467#[derive(Debug, Clone)]
468pub struct Episode {
469    client: HttpClient,
470    metadata: Metadata,
471}
472
473derive_from_metadata!(Episode);
474derive_metadata_item!(Episode);
475
476impl MediaItem for Episode {}
477impl MediaItemWithTranscoding for Episode {
478    type Options = VideoTranscodeOptions;
479}
480
481impl Episode {
482    /// Returns the number of this season within the show.
483    pub fn season_number(&self) -> Option<u32> {
484        self.metadata.parent.parent_index
485    }
486
487    /// Returns the number of this episode within the season.
488    pub fn episode_number(&self) -> Option<u32> {
489        self.metadata.index
490    }
491
492    /// Retrieves the season that this episode is from.
493    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
494    pub async fn season(&self) -> Result<Option<Season>> {
495        parent(self, &self.client).await
496    }
497}
498
499#[derive(Debug, Clone)]
500pub struct Artist {
501    client: HttpClient,
502    metadata: Metadata,
503}
504
505derive_from_metadata!(Artist);
506derive_metadata_item!(Artist);
507
508impl Artist {
509    /// Retrieves all of the fully-featured studio albums (skipping Lives, EPs, etc.) by this artist.
510    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
511    pub async fn full_studio_albums(&self) -> Result<Vec<MusicAlbum>> {
512        metadata_items(&self.client, &self.metadata.key).await
513    }
514
515    /// Retrieves all of the albums by this artist.
516    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
517    pub async fn albums(&self) -> Result<Vec<MusicAlbum>> {
518        let section_id = match &self.metadata.library_section_id {
519            Some(id) => id,
520            None => return Err(Error::UnexpectedError),
521        };
522
523        let albums_search_path = format!(
524            "/library/sections/{}/all?type={}&artist.id={}",
525            section_id,
526            SearchType::Album,
527            self.metadata.rating_key
528        );
529        metadata_items(&self.client, &albums_search_path).await
530    }
531}
532
533#[derive(Debug, Clone)]
534pub struct MusicAlbum {
535    client: HttpClient,
536    metadata: Metadata,
537}
538
539derive_from_metadata!(MusicAlbum);
540derive_metadata_item!(MusicAlbum);
541
542impl MusicAlbum {
543    /// Retrieves all of the tracks in this album.
544    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
545    pub async fn tracks(&self) -> Result<Vec<Track>> {
546        metadata_items(&self.client, &self.metadata.key).await
547    }
548
549    /// Retrieves the artist for this album.
550    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
551    pub async fn artist(&self) -> Result<Option<Artist>> {
552        parent(self, &self.client).await
553    }
554}
555
556#[derive(Debug, Clone)]
557pub struct Track {
558    client: HttpClient,
559    metadata: Metadata,
560}
561
562derive_from_metadata!(Track);
563derive_metadata_item!(Track);
564
565impl MediaItem for Track {}
566impl MediaItemWithTranscoding for Track {
567    type Options = MusicTranscodeOptions;
568}
569
570impl Track {
571    /// Returns the number of this track within the album.
572    pub fn track_number(&self) -> Option<u32> {
573        self.metadata.index
574    }
575
576    /// Retrieves the album for this track.
577    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
578    pub async fn album(&self) -> Result<Option<MusicAlbum>> {
579        parent(self, &self.client).await
580    }
581}
582
583#[derive(Debug, Clone)]
584pub struct Photo {
585    client: HttpClient,
586    metadata: Metadata,
587}
588
589derive_from_metadata!(Photo);
590derive_metadata_item!(Photo);
591
592impl MediaItem for Photo {}
593
594impl Photo {
595    /// Retrieves the album that this photo is in.
596    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
597    pub async fn album(&self) -> Result<Option<PhotoAlbum>> {
598        parent(self, &self.client).await
599    }
600}
601
602#[derive(Debug, Clone)]
603pub struct PhotoAlbum {
604    client: HttpClient,
605    metadata: Metadata,
606}
607
608derive_from_metadata!(PhotoAlbum);
609derive_metadata_item!(PhotoAlbum);
610
611impl PhotoAlbum {
612    /// Retrieves all of the albums and photos in this album.
613    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
614    pub async fn contents(&self) -> Result<Vec<PhotoAlbumItem>> {
615        metadata_items(&self.client, &self.metadata.key).await
616    }
617
618    /// Retrieves the album that this album is in.
619    #[tracing::instrument(level = "debug", skip_all, fields(self.metadata.key = self.metadata.key))]
620    pub async fn album(&self) -> Result<Option<PhotoAlbum>> {
621        parent(self, &self.client).await
622    }
623}
624
625#[enum_dispatch(MetadataItem)]
626pub enum PhotoAlbumItem {
627    PhotoAlbum,
628    Photo,
629}
630
631impl FromMetadata for PhotoAlbumItem {
632    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
633        // This isn't a great test but there doesn't seem to be much better.
634        if metadata.key.ends_with("/children") {
635            PhotoAlbum::from_metadata(client, metadata).into()
636        } else {
637            Photo::from_metadata(client, metadata).into()
638        }
639    }
640}
641
642#[derive(Debug, Clone)]
643pub struct Clip {
644    client: HttpClient,
645    metadata: Metadata,
646}
647
648derive_from_metadata!(Clip);
649derive_metadata_item!(Clip);
650
651impl MediaItem for Clip {}
652
653#[derive(Debug, Clone)]
654pub struct UnknownItem {
655    client: HttpClient,
656    metadata: Metadata,
657}
658
659derive_from_metadata!(UnknownItem);
660derive_metadata_item!(UnknownItem);
661
662#[enum_dispatch(MetadataItem)]
663#[derive(Debug, Clone)]
664pub enum Item {
665    Movie,
666    Episode,
667    Photo,
668    Show,
669    Artist,
670    MusicAlbum,
671    Season,
672    Track,
673    Clip,
674    MovieCollection(Collection<Movie>),
675    ShowCollection(Collection<Show>),
676    VideoPlaylist(Playlist<Video>),
677    PhotoPlaylist(Playlist<Photo>),
678    MusicPlaylist(Playlist<Track>),
679    UnknownItem,
680}
681
682impl MediaItem for Item {}
683
684impl FromMetadata for Item {
685    fn from_metadata(client: HttpClient, metadata: Metadata) -> Self {
686        if let Some(ref item_type) = metadata.metadata_type {
687            match item_type {
688                MetadataType::Movie => Movie::from_metadata(client, metadata).into(),
689                MetadataType::Episode => Episode::from_metadata(client, metadata).into(),
690                MetadataType::Photo => Photo::from_metadata(client, metadata).into(),
691                MetadataType::Show => Show::from_metadata(client, metadata).into(),
692                MetadataType::Artist => Artist::from_metadata(client, metadata).into(),
693                MetadataType::MusicAlbum => MusicAlbum::from_metadata(client, metadata).into(),
694                MetadataType::Season => Season::from_metadata(client, metadata).into(),
695                MetadataType::Track => Track::from_metadata(client, metadata).into(),
696                MetadataType::Clip(_) => Clip::from_metadata(client, metadata).into(),
697                MetadataType::Collection(CollectionMetadataSubtype::Movie) => {
698                    Collection::<Movie>::from_metadata(client, metadata).into()
699                }
700                MetadataType::Collection(CollectionMetadataSubtype::Show) => {
701                    Collection::<Show>::from_metadata(client, metadata).into()
702                }
703                MetadataType::Playlist(PlaylistMetadataType::Video) => {
704                    Playlist::<Video>::from_metadata(client, metadata).into()
705                }
706                MetadataType::Playlist(PlaylistMetadataType::Audio) => {
707                    Playlist::<Track>::from_metadata(client, metadata).into()
708                }
709                MetadataType::Playlist(PlaylistMetadataType::Photo) => {
710                    Playlist::<Photo>::from_metadata(client, metadata).into()
711                }
712                #[cfg(not(feature = "tests_deny_unknown_fields"))]
713                _ => UnknownItem::from_metadata(client, metadata).into(),
714            }
715        } else {
716            UnknownItem::from_metadata(client, metadata).into()
717        }
718    }
719}
720
721#[derive(Debug, Clone)]
722pub struct MovieLibrary {
723    client: HttpClient,
724    directory: ServerLibrary,
725}
726
727impl MovieLibrary {
728    /// Returns the title of this library.
729    pub fn title(&self) -> &str {
730        &self.directory.title
731    }
732
733    /// Retrieves all of the movies in this library.
734    #[tracing::instrument(level = "debug", skip_all)]
735    pub async fn movies(&self) -> Result<Vec<Movie>> {
736        pivot_items(&self.client, &self.directory, "content.library").await
737    }
738
739    /// Retrieves all of the collections in this library.
740    #[tracing::instrument(level = "debug", skip_all)]
741    pub async fn collections(&self) -> Result<Vec<Collection<Movie>>> {
742        pivot_items(&self.client, &self.directory, "content.collections").await
743    }
744
745    /// Retrieves all of the playlists containing movies from this library.
746    #[tracing::instrument(level = "debug", skip_all)]
747    pub async fn playlists(&self) -> Result<Vec<Playlist<Video>>> {
748        pivot_items(&self.client, &self.directory, "content.playlists").await
749    }
750}
751
752#[derive(Debug, Clone)]
753pub struct TVLibrary {
754    client: HttpClient,
755    directory: ServerLibrary,
756}
757
758impl TVLibrary {
759    /// Returns the title of this library.
760    pub fn title(&self) -> &str {
761        &self.directory.title
762    }
763
764    /// Retrieves all of the shows in this library.
765    #[tracing::instrument(level = "debug", skip_all)]
766    pub async fn shows(&self) -> Result<Vec<Show>> {
767        pivot_items(&self.client, &self.directory, "content.library").await
768    }
769
770    /// Retrieves all of the collections in this library.
771    #[tracing::instrument(level = "debug", skip_all)]
772    pub async fn collections(&self) -> Result<Vec<Collection<Show>>> {
773        pivot_items(&self.client, &self.directory, "content.collections").await
774    }
775
776    /// Retrieves all of the playlists containing episodes from this library.
777    #[tracing::instrument(level = "debug", skip_all)]
778    pub async fn playlists(&self) -> Result<Vec<Playlist<Video>>> {
779        pivot_items(&self.client, &self.directory, "content.playlists").await
780    }
781}
782
783#[derive(Debug, Clone)]
784pub struct MusicLibrary {
785    client: HttpClient,
786    directory: ServerLibrary,
787}
788
789impl MusicLibrary {
790    /// Returns the title of this library.
791    pub fn title(&self) -> &str {
792        &self.directory.title
793    }
794
795    /// Retrieves all of the artists in this library.
796    #[tracing::instrument(level = "debug", skip_all)]
797    pub async fn artists(&self) -> Result<Vec<Artist>> {
798        pivot_items(&self.client, &self.directory, "content.library").await
799    }
800
801    /// Retrieves all of the playlists containing tracks from this library.
802    #[tracing::instrument(level = "debug", skip_all)]
803    pub async fn playlists(&self) -> Result<Vec<Playlist<Track>>> {
804        pivot_items(&self.client, &self.directory, "content.playlists").await
805    }
806}
807
808#[derive(Debug, Clone)]
809pub struct PhotoLibrary {
810    client: HttpClient,
811    directory: ServerLibrary,
812}
813
814impl PhotoLibrary {
815    /// Returns the title of this library.
816    pub fn title(&self) -> &str {
817        &self.directory.title
818    }
819
820    /// Retrieves all of the albums in this library.
821    #[tracing::instrument(level = "debug", skip_all)]
822    pub async fn albums(&self) -> Result<Vec<PhotoAlbum>> {
823        pivot_items(&self.client, &self.directory, "content.library").await
824    }
825
826    /// Retrieves all of the playlists containing photos from this library.
827    #[tracing::instrument(level = "debug", skip_all)]
828    pub async fn playlists(&self) -> Result<Vec<Playlist<Photo>>> {
829        pivot_items(&self.client, &self.directory, "content.playlists").await
830    }
831}
832
833#[derive(Debug, Clone)]
834pub enum Library {
835    Movie(MovieLibrary),
836    TV(TVLibrary),
837    Music(MusicLibrary),
838    Video(MovieLibrary),
839    Photo(PhotoLibrary),
840}
841
842impl Library {
843    pub(super) fn new(client: HttpClient, directory: ServerLibrary) -> Self {
844        match directory.library_type {
845            LibraryType::Movie => {
846                if directory.subtype.as_deref() == Some("clip") {
847                    Library::Video(MovieLibrary { client, directory })
848                } else {
849                    Library::Movie(MovieLibrary { client, directory })
850                }
851            }
852            LibraryType::Show => Library::TV(TVLibrary { client, directory }),
853            LibraryType::Artist => Library::Music(MusicLibrary { client, directory }),
854            LibraryType::Photo => Library::Photo(PhotoLibrary { client, directory }),
855            LibraryType::Mixed => todo!("Mixed library type is not supported yet"),
856            LibraryType::Clip => todo!("Clip library type is not supported yet"),
857            #[cfg(not(feature = "tests_deny_unknown_fields"))]
858            LibraryType::Unknown => panic!("Unknown library type"),
859        }
860    }
861
862    fn directory(&self) -> &ServerLibrary {
863        match self {
864            Self::Movie(l) => &l.directory,
865            Self::TV(l) => &l.directory,
866            Self::Music(l) => &l.directory,
867            Self::Video(l) => &l.directory,
868            Self::Photo(l) => &l.directory,
869        }
870    }
871
872    /// Returns the unique ID of this library.
873    pub fn id(&self) -> &str {
874        &self.directory().id
875    }
876
877    /// Returns the title of this library.
878    pub fn title(&self) -> &str {
879        &self.directory().title
880    }
881
882    pub fn library_type(&self) -> &LibraryType {
883        &self.directory().library_type
884    }
885}