python-plexapi/plexapi/audio.py

407 lines
20 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2020-05-12 21:15:16 +00:00
from urllib.parse import quote_plus
from plexapi import media, utils
from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest
2017-01-02 21:06:40 +00:00
class Audio(PlexPartialObject):
""" Base class for audio :class:`~plexapi.audio.Artist`, :class:`~plexapi.audio.Album`
and :class:`~plexapi.audio.Track` objects.
Attributes:
2020-12-23 23:23:10 +00:00
addedAt (datetime): Datetime the item was added to the library.
art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>).
artBlurHash (str): BlurHash string for artwork image.
2020-12-23 23:23:10 +00:00
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the artist, album, or track (plex://artist/5d07bcb0403c64029053ac4c).
index (int): Plex index number (often the track number).
key (str): API URL (/library/metadata/<ratingkey>).
2020-12-23 23:23:10 +00:00
lastViewedAt (datetime): Datetime the item was last played.
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
2020-12-23 23:23:10 +00:00
librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key.
librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title.
listType (str): Hardcoded as 'audio' (useful for search filters).
2020-12-23 23:23:10 +00:00
moods (List<:class:`~plexapi.media.Mood`>): List of mood objects.
ratingKey (int): Unique key identifying the item.
summary (str): Summary of the artist, album, or track.
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
thumbBlurHash (str): BlurHash string for thumbnail image.
2020-12-23 23:23:10 +00:00
title (str): Name of the artist, album, or track (Jason Mraz, We Sing, Lucky, etc.).
titleSort (str): Title to use when sorting (defaults to title).
type (str): 'artist', 'album', or 'track'.
2020-12-23 23:23:10 +00:00
updatedAt (datatime): Datetime the item was updated.
userRating (float): Rating of the track (0.0 - 10.0) equaling (0 stars - 5 stars).
viewCount (int): Count of times the item was played.
2017-01-02 21:06:40 +00:00
"""
2018-09-08 15:25:16 +00:00
METADATA_TYPE = 'track'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
2017-02-04 17:43:50 +00:00
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
self.art = data.attrib.get('art')
self.artBlurHash = data.attrib.get('artBlurHash')
2020-12-24 04:39:15 +00:00
self.fields = self.findItems(data, media.Field)
2020-12-23 23:23:10 +00:00
self.guid = data.attrib.get('guid')
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '')
2017-02-04 17:43:50 +00:00
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
self.librarySectionID = data.attrib.get('librarySectionID')
2020-12-23 23:23:10 +00:00
self.librarySectionKey = data.attrib.get('librarySectionKey')
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
self.listType = 'audio'
self.moods = self.findItems(data, media.Mood)
2017-02-04 17:43:50 +00:00
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
self.summary = data.attrib.get('summary')
self.thumb = data.attrib.get('thumb')
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
2017-02-04 17:43:50 +00:00
self.title = data.attrib.get('title')
self.titleSort = data.attrib.get('titleSort', self.title)
2017-02-04 17:43:50 +00:00
self.type = data.attrib.get('type')
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
2020-12-23 23:23:10 +00:00
self.userRating = utils.cast(float, data.attrib.get('userRating', 0))
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
2017-01-02 21:06:40 +00:00
@property
def thumbUrl(self):
""" Return url to for the thumbnail image. """
key = self.firstAttr('thumb', 'parentThumb', 'granparentThumb')
return self._server.url(key, includeToken=True) if key else None
2017-01-02 21:06:40 +00:00
@property
def artUrl(self):
""" Return the first art url starting on the most specific for that item."""
art = self.firstAttr('art', 'grandparentArt')
return self._server.url(art, includeToken=True) if art else None
def url(self, part):
2020-12-23 23:23:10 +00:00
""" Returns the full URL for the audio item. Typically used for getting a specific track. """
return self._server.url(part, includeToken=True) if part else None
2018-09-08 15:25:16 +00:00
def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
return self.title
def sync(self, bitrate, client=None, clientId=None, limit=None, title=None):
""" Add current audio (artist, album or track) as sync item for specified device.
2020-11-23 03:06:30 +00:00
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
2018-09-08 15:25:16 +00:00
Parameters:
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
2020-11-23 03:06:30 +00:00
module :mod:`~plexapi.sync`.
client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see
:func:`~plexapi.myplex.MyPlexAccount.sync`.
clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`.
2018-09-08 15:25:16 +00:00
limit (int): maximum count of items to sync, unlimited if `None`.
2020-11-23 03:06:30 +00:00
title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be
2018-09-08 15:25:16 +00:00
generated from metadata of current media.
Returns:
2020-11-23 03:06:30 +00:00
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
2018-09-08 15:25:16 +00:00
"""
from plexapi.sync import SyncItem, Policy, MediaSettings
myplex = self._server.myPlexAccount()
sync_item = SyncItem(self._server, None)
sync_item.title = title if title else self._defaultSyncTitle()
sync_item.rootTitle = self.title
sync_item.contentType = self.listType
sync_item.metadataType = self.METADATA_TYPE
sync_item.machineIdentifier = self._server.machineIdentifier
section = self._server.library.sectionByID(self.librarySectionID)
sync_item.location = 'library://%s/item/%s' % (section.uuid, quote_plus(self.key))
sync_item.policy = Policy.create(limit)
sync_item.mediaSettings = MediaSettings.createMusic(bitrate)
return myplex.sync(sync_item, client=client, clientId=clientId)
@utils.registerPlexObject
class Artist(Audio):
""" Represents a single audio artist.
Attributes:
TAG (str): 'Directory'
TYPE (str): 'artist'
2020-12-23 23:23:10 +00:00
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
countries (List<:class:`~plexapi.media.Country`>): List country objects.
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
key (str): API URL (/library/metadata/<ratingkey>).
locations (List<str>): List of folder paths where the artist is found on disk.
2020-12-23 23:23:10 +00:00
similar (List<:class:`~plexapi.media.Similar`>): List of similar objects.
styles (List<:class:`~plexapi.media.Style`>): List of style objects.
2017-01-02 21:06:40 +00:00
"""
TAG = 'Directory'
TYPE = 'artist'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Audio._loadData(self, data)
2020-12-23 23:23:10 +00:00
self.collections = self.findItems(data, media.Collection)
self.countries = self.findItems(data, media.Country)
self.genres = self.findItems(data, media.Genre)
2020-12-23 23:23:10 +00:00
self.key = self.key.replace('/children', '') # FIX_BUG_50
self.locations = self.listAttrs(data, 'path', etag='Location')
self.similar = self.findItems(data, media.Similar)
2020-12-23 23:23:10 +00:00
self.styles = self.findItems(data, media.Style)
def __iter__(self):
for album in self.albums():
yield album
def album(self, title):
""" Returns the :class:`~plexapi.audio.Album` that matches the specified title.
Parameters:
title (str): Title of the album to return.
"""
key = '/library/metadata/%s/children' % self.ratingKey
return self.fetchItem(key, Album, title__iexact=title)
2017-02-09 04:08:25 +00:00
def albums(self, **kwargs):
2020-12-23 23:23:10 +00:00
""" Returns a list of :class:`~plexapi.audio.Album` objects by the artist. """
key = '/library/metadata/%s/children' % self.ratingKey
return self.fetchItems(key, Album, **kwargs)
def track(self, title=None, album=None, track=None):
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
2017-01-02 21:06:40 +00:00
Parameters:
title (str): Title of the track to return.
album (str): Album name (default: None; required if title not specified).
track (int): Track number (default: None; required if title not specified).
Raises:
:exc:`~plexapi.exceptions.BadRequest`: If title or album and track parameters are missing.
2017-01-02 21:06:40 +00:00
"""
key = '/library/metadata/%s/allLeaves' % self.ratingKey
if title:
return self.fetchItem(key, Track, title__iexact=title)
elif album is not None and track is not None:
return self.fetchItem(key, Track, parentTitle__iexact=album, index=track)
raise BadRequest('Missing argument: title or album and track are required')
2017-02-09 04:08:25 +00:00
def tracks(self, **kwargs):
2020-12-23 23:23:10 +00:00
""" Returns a list of :class:`~plexapi.audio.Track` objects by the artist. """
key = '/library/section/%s/allLeaves' % self.ratingKey
return self.fetchItems(key, Track, **kwargs)
def get(self, title=None, album=None, track=None):
""" Alias of :func:`~plexapi.audio.Artist.track`. """
return self.track(title, album, track)
2019-01-07 13:04:53 +00:00
def download(self, savepath=None, keep_original_name=False, **kwargs):
2020-12-23 23:23:10 +00:00
""" Downloads all tracks for the artist to the specified location.
2017-02-20 05:37:00 +00:00
2017-02-02 03:53:05 +00:00
Parameters:
savepath (str): Title of the track to return.
2019-01-07 13:04:53 +00:00
keep_original_name (bool): Set True to keep the original filename as stored in
2017-02-02 03:53:05 +00:00
the Plex server. False will create a new filename with the format
"<Atrist> - <Album> <Track>".
2020-11-23 03:06:30 +00:00
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
2017-02-02 03:53:05 +00:00
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
"""
filepaths = []
2017-01-09 14:21:54 +00:00
for album in self.albums():
for track in album.tracks():
2019-01-07 13:04:53 +00:00
filepaths += track.download(savepath, keep_original_name, **kwargs)
return filepaths
2017-01-09 14:21:54 +00:00
@utils.registerPlexObject
class Album(Audio):
""" Represents a single audio album.
2017-01-02 21:06:40 +00:00
Attributes:
TAG (str): 'Directory'
TYPE (str): 'album'
2020-12-23 23:23:10 +00:00
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
key (str): API URL (/library/metadata/<ratingkey>).
2020-12-23 23:23:10 +00:00
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
leafCount (int): Number of items in the album view.
loudnessAnalysisVersion (int): The Plex loudness analysis version level.
originallyAvailableAt (datetime): Datetime the album was released.
parentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
parentKey (str): API URL of the album artist (/library/metadata/<parentRatingKey>).
parentRatingKey (int): Unique key identifying the album artist.
parentThumb (str): URL to album artist thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
parentTitle (str): Name of the album artist.
rating (float): Album rating (7.9; 9.8; 8.1).
studio (str): Studio that released the album.
styles (List<:class:`~plexapi.media.Style`>): List of style objects.
viewedLeafCount (int): Number of items marked as played in the album view.
year (int): Year the album was released.
"""
TAG = 'Directory'
TYPE = 'album'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Audio._loadData(self, data)
2020-12-23 23:23:10 +00:00
self.collections = self.findItems(data, media.Collection)
self.genres = self.findItems(data, media.Genre)
self.key = self.key.replace('/children', '') # FIX_BUG_50
self.labels = self.findItems(data, media.Label)
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
self.loudnessAnalysisVersion = utils.cast(int, data.attrib.get('loudnessAnalysisVersion'))
2017-02-04 17:43:50 +00:00
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
2020-12-23 23:23:10 +00:00
self.parentGuid = data.attrib.get('parentGuid')
2017-02-04 17:43:50 +00:00
self.parentKey = data.attrib.get('parentKey')
2020-12-23 23:23:10 +00:00
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
2017-02-04 17:43:50 +00:00
self.parentThumb = data.attrib.get('parentThumb')
self.parentTitle = data.attrib.get('parentTitle')
2020-12-23 23:23:10 +00:00
self.rating = utils.cast(float, data.attrib.get('rating'))
2017-02-04 17:43:50 +00:00
self.studio = data.attrib.get('studio')
2020-12-23 23:23:10 +00:00
self.styles = self.findItems(data, media.Style)
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
2017-02-04 17:43:50 +00:00
self.year = utils.cast(int, data.attrib.get('year'))
2020-12-23 23:23:10 +00:00
def __iter__(self):
for track in self.tracks():
yield track
def track(self, title=None, track=None):
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
2017-01-02 21:06:40 +00:00
Parameters:
title (str): Title of the track to return.
track (int): Track number (default: None; required if title not specified).
Raises:
:exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing.
2017-01-02 21:06:40 +00:00
"""
key = '/library/metadata/%s/children' % self.ratingKey
if title:
return self.fetchItem(key, Track, title__iexact=title)
elif track:
return self.fetchItem(key, Track, parentTitle__iexact=self.title, index=track)
raise BadRequest('Missing argument: title or track is required')
2017-02-09 04:08:25 +00:00
def tracks(self, **kwargs):
2020-12-23 23:23:10 +00:00
""" Returns a list of :class:`~plexapi.audio.Track` objects in the album. """
key = '/library/metadata/%s/children' % self.ratingKey
return self.fetchItems(key, Track, **kwargs)
def get(self, title=None, track=None):
""" Alias of :func:`~plexapi.audio.Album.track`. """
return self.track(title, track)
def artist(self):
2020-12-23 23:23:10 +00:00
""" Return the album's :class:`~plexapi.audio.Artist`. """
return self.fetchItem(self.parentKey)
2019-01-07 13:04:53 +00:00
def download(self, savepath=None, keep_original_name=False, **kwargs):
2020-12-23 23:23:10 +00:00
""" Downloads all tracks for the artist to the specified location.
2017-02-20 05:37:00 +00:00
2017-02-02 03:53:05 +00:00
Parameters:
savepath (str): Title of the track to return.
2019-01-07 13:04:53 +00:00
keep_original_name (bool): Set True to keep the original filename as stored in
2017-02-02 03:53:05 +00:00
the Plex server. False will create a new filename with the format
"<Atrist> - <Album> <Track>".
2020-11-23 03:06:30 +00:00
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL` will
2017-02-02 03:53:05 +00:00
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
"""
filepaths = []
for track in self.tracks():
2019-01-07 13:04:53 +00:00
filepaths += track.download(savepath, keep_original_name, **kwargs)
return filepaths
2017-01-09 14:21:54 +00:00
2018-09-08 15:25:16 +00:00
def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
return '%s - %s' % (self.parentTitle, self.title)
@utils.registerPlexObject
class Track(Audio, Playable):
""" Represents a single audio track.
2017-01-02 21:06:40 +00:00
Attributes:
TAG (str): 'Directory'
TYPE (str): 'track'
2020-12-23 23:23:10 +00:00
chapterSource (str): Unknown
duration (int): Length of the track in milliseconds.
grandparentArt (str): URL to album artist artwork (/library/metadata/<grandparentRatingKey>/art/<artid>).
grandparentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
grandparentKey (str): API URL of the album artist (/library/metadata/<grandparentRatingKey>).
grandparentRatingKey (int): Unique key identifying the album artist.
grandparentThumb (str): URL to album artist thumbnail image
(/library/metadata/<grandparentRatingKey>/thumb/<thumbid>).
grandparentTitle (str): Name of the album artist for the track.
media (List<:class:`~plexapi.media.Media`>): List of media objects.
originalTitle (str): The original title of the track (eg. a different language).
parentGuid (str): Plex GUID for the album (plex://album/5d07cd8e403c640290f180f9).
parentIndex (int): Album index.
2020-12-23 23:23:10 +00:00
parentKey (str): API URL of the album (/library/metadata/<parentRatingKey>).
parentRatingKey (int): Unique key identifying the album.
parentThumb (str): URL to album thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
parentTitle (str): Name of the album for the track.
primaryExtraKey (str) API URL for the primary extra for the track.
ratingCount (int): Number of ratings contributing to the rating score.
viewOffset (int): View offset in milliseconds.
year (int): Year the track was released.
"""
TAG = 'Track'
TYPE = 'track'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Audio._loadData(self, data)
Playable._loadData(self, data)
2017-02-04 17:43:50 +00:00
self.chapterSource = data.attrib.get('chapterSource')
self.duration = utils.cast(int, data.attrib.get('duration'))
self.grandparentArt = data.attrib.get('grandparentArt')
2020-12-23 23:23:10 +00:00
self.grandparentGuid = data.attrib.get('grandparentGuid')
2017-02-04 17:43:50 +00:00
self.grandparentKey = data.attrib.get('grandparentKey')
2020-12-23 23:23:10 +00:00
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
2017-02-04 17:43:50 +00:00
self.grandparentThumb = data.attrib.get('grandparentThumb')
self.grandparentTitle = data.attrib.get('grandparentTitle')
2020-12-23 23:23:10 +00:00
self.media = self.findItems(data, media.Media)
2017-02-04 17:43:50 +00:00
self.originalTitle = data.attrib.get('originalTitle')
2020-12-23 23:23:10 +00:00
self.parentGuid = data.attrib.get('parentGuid')
2017-02-04 17:43:50 +00:00
self.parentIndex = data.attrib.get('parentIndex')
self.parentKey = data.attrib.get('parentKey')
2020-12-23 23:23:10 +00:00
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
2017-02-04 17:43:50 +00:00
self.parentThumb = data.attrib.get('parentThumb')
self.parentTitle = data.attrib.get('parentTitle')
self.primaryExtraKey = data.attrib.get('primaryExtraKey')
self.ratingCount = utils.cast(int, data.attrib.get('ratingCount'))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
2017-02-04 17:43:50 +00:00
self.year = utils.cast(int, data.attrib.get('year'))
def _prettyfilename(self):
""" Returns a filename for use in download. """
return '%s - %s %s' % (self.grandparentTitle, self.parentTitle, self.title)
def album(self):
2020-12-23 23:23:10 +00:00
""" Return the track's :class:`~plexapi.audio.Album`. """
return self.fetchItem(self.parentKey)
def artist(self):
2020-12-23 23:23:10 +00:00
""" Return the track's :class:`~plexapi.audio.Artist`. """
return self.fetchItem(self.grandparentKey)
2018-09-08 15:25:16 +00:00
2020-12-23 23:23:10 +00:00
@property
def locations(self):
""" This does not exist in plex xml response but is added to have a common
interface to get the locations of the track.
Retruns:
List<str> of file paths where the track is found on disk.
2020-12-23 23:23:10 +00:00
"""
return [part.file for part in self.iterParts() if part]
2018-09-08 15:25:16 +00:00
def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
2020-06-06 18:14:03 +00:00
return '%s - %s - %s' % (self.grandparentTitle, self.parentTitle, self.title)