mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-24 20:53:09 +00:00
remove backup files, add .orgin to gitignore
- delete .travis....
This commit is contained in:
parent
d19d602455
commit
e2efc5f3f2
7 changed files with 3 additions and 1912 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -19,4 +19,5 @@ pip-selfcheck.json
|
|||
pyvenv.cfg
|
||||
|
||||
htmlcov
|
||||
.coverage
|
||||
.coverage
|
||||
*.orig
|
16
.travis
16
.travis
|
@ -1,16 +0,0 @@
|
|||
language: python
|
||||
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.5"
|
||||
|
||||
|
||||
install:
|
||||
- pip install .
|
||||
- pip install -r requirements_dev.txt
|
||||
|
||||
|
||||
script: py.test tests/tests_pytest --cov-config .coveragerc --cov=plexapi --cov-report=html
|
||||
|
||||
after_success:
|
||||
- coveralls
|
|
@ -8,8 +8,7 @@ python:
|
|||
- "3.6"
|
||||
|
||||
before_install:
|
||||
- pip install -U pytest pytest-cov
|
||||
- pip install -U coveralls
|
||||
- pip install -U pytest pytest-cov coveralls
|
||||
|
||||
install:
|
||||
- pip install -r requirements_dev.txt
|
||||
|
|
|
@ -1,340 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from plexapi import media, utils
|
||||
from plexapi.utils import Playable, PlexPartialObject
|
||||
|
||||
NA = utils.NA
|
||||
|
||||
|
||||
class Audio(PlexPartialObject):
|
||||
""" Base class for audio :class:`~plexapi.audio.Artist`, :class:`~plexapi.audio.Album`
|
||||
and :class:`~plexapi.audio.Track` objects.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
|
||||
data (ElementTree): Response from PlexServer used to build this object (optional).
|
||||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
|
||||
Attributes:
|
||||
addedAt (datetime): Datetime this item was added to the library.
|
||||
index (sting): Index Number (often the track number).
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
lastViewedAt (datetime): Datetime item was last accessed.
|
||||
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
|
||||
listType (str): Hardcoded as 'audio' (useful for search filters).
|
||||
ratingKey (int): Unique key identifying this item.
|
||||
summary (str): Summary of the artist, track, or album.
|
||||
thumb (str): URL to thumbnail image.
|
||||
title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.)
|
||||
titleSort (str): Title to use when sorting (defaults to title).
|
||||
type (str): 'artist', 'album', or 'track'.
|
||||
updatedAt (datatime): Datetime this item was updated.
|
||||
viewCount (int): Count of times this item was accessed.
|
||||
"""
|
||||
TYPE = None
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
super(Audio, self).__init__(data, initpath, server)
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self.listType = 'audio'
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
|
||||
self.index = data.attrib.get('index', NA)
|
||||
self.key = data.attrib.get('key', NA)
|
||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt', NA))
|
||||
self.librarySectionID = data.attrib.get('librarySectionID', NA)
|
||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey', NA))
|
||||
self.summary = data.attrib.get('summary', NA)
|
||||
self.thumb = data.attrib.get('thumb', NA)
|
||||
self.title = data.attrib.get('title', NA)
|
||||
self.titleSort = data.attrib.get('titleSort', self.title)
|
||||
self.type = data.attrib.get('type', NA)
|
||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt', NA))
|
||||
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
|
||||
|
||||
@property
|
||||
def thumbUrl(self):
|
||||
""" Returns the URL to this items thumbnail image. """
|
||||
if self.thumb:
|
||||
return self.server.url(self.thumb)
|
||||
|
||||
def refresh(self):
|
||||
""" Tells Plex to refresh the metadata for this and all subitems. """
|
||||
self.server.query('%s/refresh' % self.key, method=self.server.session.put)
|
||||
|
||||
def section(self):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
|
||||
return self.server.library.sectionByID(self.librarySectionID)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Artist(Audio):
|
||||
""" Represents a single audio artist.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
|
||||
data (ElementTree): Response from PlexServer used to build this object (optional).
|
||||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
|
||||
Attributes:
|
||||
art (str): Artist artwork (/library/metadata/<ratingkey>/art/<artid>)
|
||||
countries (list): List of :class:`~plexapi.media.Country` objects this artist respresents.
|
||||
genres (list): List of :class:`~plexapi.media.Genre` objects this artist respresents.
|
||||
guid (str): Unknown (unique ID; com.plexapp.agents.plexmusic://gracenote/artist/05517B8701668D28?lang=en)
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
location (str): Filepath this artist is found on disk.
|
||||
similar (list): List of :class:`~plexapi.media.Similar` artists.
|
||||
"""
|
||||
TYPE = 'artist'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Audio._loadData(self, data)
|
||||
self.art = data.attrib.get('art', NA)
|
||||
self.guid = data.attrib.get('guid', NA)
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.location = utils.findLocations(data, single=True)
|
||||
if self.isFullObject(): # check if this is needed
|
||||
self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE]
|
||||
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
|
||||
self.similar = [media.Similar(self.server, e) for e in data if e.tag == media.Similar.TYPE]
|
||||
|
||||
def albums(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Album` objects by this artist. """
|
||||
path = '%s/children' % self.key
|
||||
return utils.listItems(self.server, path, Album.TYPE)
|
||||
|
||||
def album(self, title):
|
||||
""" Returns the :class:`~plexapi.audio.Album` that matches the specified title.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the album to return.
|
||||
"""
|
||||
path = '%s/children' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def tracks(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Track` objects by this artist. """
|
||||
path = '%s/allLeaves' % self.key
|
||||
return utils.listItems(self.server, path)
|
||||
|
||||
def track(self, title):
|
||||
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the track to return.
|
||||
"""
|
||||
path = '%s/allLeaves' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def get(self, title):
|
||||
""" Alias of :func:`~plexapi.audio.Artist.track`. """
|
||||
return self.track(title)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
downloaded = []
|
||||
for album in self.albums():
|
||||
for track in album.tracks():
|
||||
dl = track.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
|
||||
if dl:
|
||||
downloaded.extend(dl)
|
||||
|
||||
return downloaded
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Album(Audio):
|
||||
""" Represents a single audio album.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
|
||||
data (ElementTree): Response from PlexServer used to build this object (optional).
|
||||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
|
||||
Attributes:
|
||||
art (str): Album artwork (/library/metadata/<ratingkey>/art/<artid>)
|
||||
genres (list): List of :class:`~plexapi.media.Genre` objects this album respresents.
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
originallyAvailableAt (datetime): Datetime this album was released.
|
||||
parentKey (str): API URL of this artist.
|
||||
parentRatingKey (int): Unique key identifying artist.
|
||||
parentThumb (str): URL to artist thumbnail image.
|
||||
parentTitle (str): Name of the artist for this album.
|
||||
studio (str): Studio that released this album.
|
||||
year (int): Year this album was released.
|
||||
"""
|
||||
TYPE = 'album'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Audio._loadData(self, data)
|
||||
self.art = data.attrib.get('art', NA)
|
||||
self.key = self.key.replace('/children', '') # fixes bug #50
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
|
||||
self.parentKey = data.attrib.get('parentKey', NA)
|
||||
self.parentRatingKey = data.attrib.get('parentRatingKey', NA)
|
||||
self.parentThumb = data.attrib.get('parentThumb', NA)
|
||||
self.parentTitle = data.attrib.get('parentTitle', NA)
|
||||
self.studio = data.attrib.get('studio', NA)
|
||||
self.year = utils.cast(int, data.attrib.get('year', NA))
|
||||
if self.isFullObject():
|
||||
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
|
||||
|
||||
def tracks(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Track` objects in this album. """
|
||||
path = '%s/children' % self.key
|
||||
return utils.listItems(self.server, path)
|
||||
|
||||
def track(self, title):
|
||||
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the track to return.
|
||||
"""
|
||||
path = '%s/children' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def get(self, title):
|
||||
""" Alias of :func:`~plexapi.audio.Album.track`. """
|
||||
return self.track(title)
|
||||
|
||||
def artist(self):
|
||||
""" Return :func:`~plexapi.audio.Artist` of this album. """
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
downloaded = []
|
||||
for ep in self.tracks():
|
||||
dl = ep.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
|
||||
if dl:
|
||||
downloaded.extend(dl)
|
||||
|
||||
return downloaded
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Track(Audio, Playable):
|
||||
""" Represents a single audio track.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
|
||||
data (ElementTree): XML response from PlexServer used to build this object (optional).
|
||||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
|
||||
Attributes:
|
||||
art (str): Track artwork (/library/metadata/<ratingkey>/art/<artid>)
|
||||
chapterSource (TYPE): Unknown
|
||||
duration (int): Length of this album in seconds.
|
||||
grandparentArt (str): Artist artowrk.
|
||||
grandparentKey (str): Artist API URL.
|
||||
grandparentRatingKey (str): Unique key identifying artist.
|
||||
grandparentThumb (str): URL to artist thumbnail image.
|
||||
grandparentTitle (str): Name of the artist for this track.
|
||||
guid (str): Unknown (unique ID).
|
||||
media (list): List of :class:`~plexapi.media.Media` objects for this track.
|
||||
moods (list): List of :class:`~plexapi.media.Mood` objects for this track.
|
||||
originalTitle (str): Original track title (if translated).
|
||||
parentIndex (int): Album index.
|
||||
parentKey (str): Album API URL.
|
||||
parentRatingKey (int): Unique key identifying album.
|
||||
parentThumb (str): URL to album thumbnail image.
|
||||
parentTitle (str): Name of the album for this track.
|
||||
primaryExtraKey (str): Unknown
|
||||
ratingCount (int): Rating of this track (1-10?)
|
||||
viewOffset (int): Unknown
|
||||
year (int): Year this track was released.
|
||||
sessionKey (int): Session Key (active sessions only).
|
||||
username (str): Username of person playing this track (active sessions only).
|
||||
player (str): :class:`~plexapi.client.PlexClient` for playing track (active sessions only).
|
||||
transcodeSession (None): :class:`~plexapi.media.TranscodeSession` for playing
|
||||
track (active sessions only).
|
||||
"""
|
||||
TYPE = 'track'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Audio._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
self.art = data.attrib.get('art', NA)
|
||||
self.chapterSource = data.attrib.get('chapterSource', NA)
|
||||
self.duration = utils.cast(int, data.attrib.get('duration', NA))
|
||||
self.grandparentArt = data.attrib.get('grandparentArt', NA)
|
||||
self.grandparentKey = data.attrib.get('grandparentKey', NA)
|
||||
self.grandparentRatingKey = data.attrib.get('grandparentRatingKey', NA)
|
||||
self.grandparentThumb = data.attrib.get('grandparentThumb', NA)
|
||||
self.grandparentTitle = data.attrib.get('grandparentTitle', NA)
|
||||
self.guid = data.attrib.get('guid', NA)
|
||||
self.originalTitle = data.attrib.get('originalTitle', NA)
|
||||
self.parentIndex = data.attrib.get('parentIndex', NA)
|
||||
self.parentKey = data.attrib.get('parentKey', NA)
|
||||
self.parentRatingKey = data.attrib.get('parentRatingKey', NA)
|
||||
self.parentThumb = data.attrib.get('parentThumb', NA)
|
||||
self.parentTitle = data.attrib.get('parentTitle', NA)
|
||||
self.primaryExtraKey = data.attrib.get('primaryExtraKey', NA)
|
||||
self.ratingCount = utils.cast(int, data.attrib.get('ratingCount', NA))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.year = utils.cast(int, data.attrib.get('year', NA))
|
||||
# media is included in /children
|
||||
self.media = [media.Media(self.server, e, self.initpath, self)
|
||||
for e in data if e.tag == media.Media.TYPE]
|
||||
if self.isFullObject(): # check me
|
||||
self.moods = [media.Mood(self.server, e) for e in data if e.tag == media.Mood.TYPE]
|
||||
self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
|
||||
|
||||
#self.media = [media.Media(self.server, e, self.initpath, self)
|
||||
# for e in data if e.tag == media.Media.TYPE]
|
||||
# data for active sessions and history
|
||||
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
|
||||
self.username = utils.findUsername(data)
|
||||
self.player = utils.findPlayer(self.server, data)
|
||||
self.transcodeSession = utils.findTranscodeSession(self.server, data)
|
||||
|
||||
@property
|
||||
def thumbUrl(self):
|
||||
""" Returns the URL thumbnail image for this track's album. """
|
||||
if self.parentThumb:
|
||||
return self.server.url(self.parentThumb)
|
||||
|
||||
def album(self):
|
||||
""" Return this track's :class:`~plexapi.audio.Album`. """
|
||||
return utils.listItems(self.server, self.parentKey)[0]
|
||||
|
||||
def artist(self):
|
||||
""" Return this track's :class:`~plexapi.audio.Artist`. """
|
||||
return utils.listItems(self.server, self.grandparentKey)[0]
|
||||
|
||||
def _prettyfilename(self):
|
||||
return '%s - %s %s' % (self.grandparentTitle, self.parentTitle, self.title)
|
||||
|
||||
'''
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
"""Download a episode. If kwargs are passed your can download a trancoded file.
|
||||
|
||||
Args:
|
||||
savepath (str): Abs path to savefolder
|
||||
keep_orginal_name (bool): Use the mediafiles orginal name
|
||||
|
||||
kwargs:
|
||||
See getStreamURL docs.
|
||||
|
||||
"""
|
||||
downloaded = []
|
||||
locs = [i for i in self.iterParts() if i]
|
||||
for loc in locs:
|
||||
if keep_orginal_name is False:
|
||||
name = '%s.%s' % (self._prettyfilename(), loc.container)
|
||||
else:
|
||||
name = loc.file
|
||||
|
||||
# So this seems to be a alot slower but allows transcode.
|
||||
if kwargs:
|
||||
download_url = self.getStreamURL(**kwargs)
|
||||
else:
|
||||
download_url = self.server.url('%s?download=1' % loc.key)
|
||||
|
||||
dl = utils.download(download_url, filename=name, savepath=savepath, session=self.server.session)
|
||||
if dl:
|
||||
downloaded.append(dl)
|
||||
|
||||
return downloaded
|
||||
'''
|
|
@ -1,500 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from plexapi import log, utils
|
||||
from plexapi import X_PLEX_CONTAINER_SIZE
|
||||
from plexapi.compat import unquote
|
||||
from plexapi.media import MediaTag
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
||||
|
||||
class Library(object):
|
||||
""" Represents a PlexServer library. This contains all sections of media defined
|
||||
in your Plex server including video, shows and audio.
|
||||
|
||||
Attributes:
|
||||
identifier (str): Unknown ('com.plexapp.plugins.library').
|
||||
mediaTagVersion (str): Unknown (/system/bundle/media/flags/)
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to.
|
||||
title1 (str): 'Plex Library' (not sure how useful this is).
|
||||
title2 (str): Second title (this is blank on my setup).
|
||||
"""
|
||||
|
||||
def __init__(self, server, data):
|
||||
self.identifier = data.attrib.get('identifier')
|
||||
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
|
||||
self.server = server
|
||||
self.title1 = data.attrib.get('title1')
|
||||
self.title2 = data.attrib.get('title2')
|
||||
self._sectionsByID = {} # cached section UUIDs
|
||||
|
||||
def __repr__(self):
|
||||
return '<Library:%s>' % self.title1.encode('utf8')
|
||||
|
||||
def sections(self):
|
||||
""" Returns a list of all media sections in this library. Library sections may be any of
|
||||
:class:`~plexapi.library.MovieSection`, :class:`~plexapi.library.ShowSection`,
|
||||
:class:`~plexapi.library.MusicSection`, :class:`~plexapi.library.PhotoSection`.
|
||||
"""
|
||||
items = []
|
||||
SECTION_TYPES = {
|
||||
MovieSection.TYPE: MovieSection,
|
||||
ShowSection.TYPE: ShowSection,
|
||||
MusicSection.TYPE: MusicSection,
|
||||
PhotoSection.TYPE: PhotoSection,
|
||||
}
|
||||
path = '/library/sections'
|
||||
for elem in self.server.query(path):
|
||||
stype = elem.attrib['type']
|
||||
if stype in SECTION_TYPES:
|
||||
cls = SECTION_TYPES[stype]
|
||||
section = cls(self.server, elem, path)
|
||||
self._sectionsByID[section.key] = section
|
||||
items.append(section)
|
||||
return items
|
||||
|
||||
def section(self, title=None):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified title.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the section to return.
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.NotFound`: Invalid library section title.
|
||||
"""
|
||||
for item in self.sections():
|
||||
if item.title == title:
|
||||
return item
|
||||
raise NotFound('Invalid library section: %s' % title)
|
||||
|
||||
def sectionByID(self, sectionID):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified sectionID.
|
||||
|
||||
Parameters:
|
||||
sectionID (int): ID of the section to return.
|
||||
"""
|
||||
self.sections()
|
||||
return self._sectionsByID[sectionID]
|
||||
|
||||
def all(self):
|
||||
""" Returns a list of all media from all library sections.
|
||||
This may be a very large dataset to retrieve.
|
||||
"""
|
||||
return [item for section in self.sections() for item in section.all()]
|
||||
|
||||
def onDeck(self):
|
||||
""" Returns a list of all media items on deck. """
|
||||
return utils.listItems(self.server, '/library/onDeck')
|
||||
|
||||
def recentlyAdded(self):
|
||||
""" Returns a list of all media items recently added. """
|
||||
return utils.listItems(self.server, '/library/recentlyAdded')
|
||||
|
||||
def get(self, title): # this should use hub search when its merged
|
||||
""" Return the first item from all items with the specified title.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the item to return.
|
||||
"""
|
||||
for i in self.all():
|
||||
if i.title.lower() == tite.lower():
|
||||
reutrn i
|
||||
|
||||
def getByKey(self, key):
|
||||
""" Return the first item from all items with the specified key.
|
||||
|
||||
Parameters:
|
||||
key (str): Key of the item to return.
|
||||
"""
|
||||
return utils.findKey(self.server, key)
|
||||
|
||||
def search(self, title=None, libtype=None, **kwargs):
|
||||
""" Searching within a library section is much more powerful. It seems certain
|
||||
attributes on the media objects can be targeted to filter this search down
|
||||
a bit, but I havent found the documentation for it.
|
||||
|
||||
Example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items
|
||||
such as actor=<id> seem to work, but require you already know the id of the actor.
|
||||
TLDR: This is untested but seems to work. Use library section search when you can.
|
||||
"""
|
||||
args = {}
|
||||
if title:
|
||||
args['title'] = title
|
||||
if libtype:
|
||||
args['type'] = utils.searchType(libtype)
|
||||
for attr, value in kwargs.items():
|
||||
args[attr] = value
|
||||
query = '/library/all%s' % utils.joinArgs(args)
|
||||
return utils.listItems(self.server, query)
|
||||
|
||||
def cleanBundles(self):
|
||||
""" Poster images and other metadata for items in your library are kept in "bundle"
|
||||
packages. When you remove items from your library, these bundles aren't immediately
|
||||
removed. Removing these old bundles can reduce the size of your install. By default, your
|
||||
server will automatically clean up old bundles once a week as part of Scheduled Tasks.
|
||||
"""
|
||||
self.server.query('/library/clean/bundles')
|
||||
|
||||
def emptyTrash(self):
|
||||
""" If a library has items in the Library Trash, use this option to empty the Trash. """
|
||||
for section in self.sections():
|
||||
section.emptyTrash()
|
||||
|
||||
def optimize(self):
|
||||
""" The Optimize option cleans up the server database from unused or fragmented data.
|
||||
For example, if you have deleted or added an entire library or many items in a
|
||||
library, you may like to optimize the database.
|
||||
"""
|
||||
self.server.query('/library/optimize')
|
||||
|
||||
def refresh(self):
|
||||
""" Refresh the metadata for the entire library. This will fetch fresh metadata for
|
||||
all contents in the library, including items that already have metadata.
|
||||
"""
|
||||
self.server.query('/library/sections/all/refresh')
|
||||
|
||||
|
||||
class LibrarySection(object):
|
||||
""" Base class for a single library section.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this library section is from.
|
||||
data (ElementTree): Response from PlexServer used to build this object (optional).
|
||||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
|
||||
Attributes:
|
||||
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
|
||||
initpath (str): Path requested when building this object.
|
||||
agent (str): Unknown (com.plexapp.agents.imdb, etc)
|
||||
allowSync (bool): True if you allow syncing content from this section.
|
||||
art (str): Wallpaper artwork used to respresent this section.
|
||||
composite (str): Composit image used to represent this section.
|
||||
createdAt (datetime): Datetime this library section was created.
|
||||
filters (str): Unknown
|
||||
key (str): Key (or ID) of this library section.
|
||||
language (str): Language represented in this section (en, xn, etc).
|
||||
locations (str): Paths on disk where section content is stored.
|
||||
refreshing (str): True if this section is currently being refreshed.
|
||||
scanner (str): Internal scanner used to find media (Plex Movie Scanner, Plex Premium Music Scanner, etc.)
|
||||
thumb (str): Thumbnail image used to represent this section.
|
||||
title (str): Title of this section.
|
||||
type (str): Type of content section represents (movie, artist, photo, show).
|
||||
updatedAt (datetime): Datetime this library section was last updated.
|
||||
uuid (str): Unique id for this section (32258d7c-3e6c-4ac5-98ad-bad7a3b78c63)
|
||||
"""
|
||||
ALLOWED_FILTERS = ()
|
||||
ALLOWED_SORT = ()
|
||||
BOOLEAN_FILTERS = ('unwatched', 'duplicate')
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
self.agent = data.attrib.get('agent')
|
||||
self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
|
||||
self.art = data.attrib.get('art')
|
||||
self.composite = data.attrib.get('composite')
|
||||
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
|
||||
self.filters = data.attrib.get('filters')
|
||||
self.key = data.attrib.get('key')
|
||||
self.language = data.attrib.get('language')
|
||||
self.locations = utils.findLocations(data)
|
||||
self.refreshing = utils.cast(bool, data.attrib.get('refreshing'))
|
||||
self.scanner = data.attrib.get('scanner')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self.uuid = data.attrib.get('uuid')
|
||||
|
||||
def __repr__(self):
|
||||
title = self.title.replace(' ', '.')[0:20]
|
||||
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
|
||||
|
||||
def get(self, title):
|
||||
""" Returns the media item with the specified title.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the item to return.
|
||||
"""
|
||||
path = '/library/sections/%s/all' % self.key
|
||||
return utils.findItem(self.server, path, title)
|
||||
|
||||
def all(self):
|
||||
""" Returns a list of media from this library section. """
|
||||
return utils.listItems(self.server, '/library/sections/%s/all' % self.key)
|
||||
|
||||
def onDeck(self):
|
||||
""" Returns a list of media items on deck from this library section. """
|
||||
return utils.listItems(self.server, '/library/sections/%s/onDeck' % self.key)
|
||||
|
||||
def recentlyAdded(self, maxresults=50):
|
||||
""" Returns a list of media items recently added from this library section.
|
||||
|
||||
Parameters:
|
||||
maxresults (int): Max number of items to return (default 50).
|
||||
"""
|
||||
return self.search(sort='addedAt:desc', maxresults=maxresults)
|
||||
|
||||
def analyze(self):
|
||||
""" Run an analysis on all of the items in this library section. """
|
||||
self.server.query('/library/sections/%s/analyze' % self.key)
|
||||
|
||||
def emptyTrash(self):
|
||||
""" If a section has items in the Trash, use this option to empty the Trash. """
|
||||
self.server.query('/library/sections/%s/emptyTrash' % self.key)
|
||||
|
||||
def refresh(self):
|
||||
""" Refresh the metadata for this library section. This will fetch fresh metadata for
|
||||
all contents in the section, including items that already have metadata.
|
||||
"""
|
||||
self.server.query('/library/sections/%s/refresh' % self.key)
|
||||
|
||||
def listChoices(self, category, libtype=None, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.library.FilterChoice` objects for the
|
||||
specified category and libtype. kwargs can be any of the same kwargs in
|
||||
:func:`plexapi.library.LibraySection.search()` to help narrow down the choices
|
||||
to only those that matter in your current context.
|
||||
|
||||
Parameters:
|
||||
category (str): Category to list choices for (genre, contentRating, etc).
|
||||
libtype (int): Library type of item filter.
|
||||
**kwargs (dict): Additional kwargs to narrow down the choices.
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.BadRequest`: Cannot include kwarg equal to specified category.
|
||||
"""
|
||||
if category in kwargs:
|
||||
raise BadRequest('Cannot include kwarg equal to specified category: %s' % category)
|
||||
args = {}
|
||||
for subcategory, value in kwargs.items():
|
||||
args[category] = self._cleanSearchFilter(subcategory, value)
|
||||
if libtype is not None:
|
||||
args['type'] = utils.searchType(libtype)
|
||||
query = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
|
||||
return utils.listItems(self.server, query, bytag=True)
|
||||
|
||||
def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs):
|
||||
""" Search the library. If there are many results, they will be fetched from the server
|
||||
in batches of X_PLEX_CONTAINER_SIZE amounts. If you're only looking for the first <num>
|
||||
results, it would be wise to set the maxresults option to that amount so this functions
|
||||
doesn't iterate over all results on the server.
|
||||
|
||||
Parameters:
|
||||
title (str): General string query to search for (optional).
|
||||
sort (str): column:dir; column can be any of {addedAt, originallyAvailableAt, lastViewedAt,
|
||||
titleSort, rating, mediaHeight, duration}. dir can be asc or desc (optional).
|
||||
maxresults (int): Only return the specified number of results (optional).
|
||||
libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist, album, track; optional).
|
||||
**kwargs (dict): Any of the available filters for the current library section. Partial string
|
||||
matches allowed. Multiple matches OR together. All inputs will be compared with the
|
||||
available options and a warning logged if the option does not appear valid.
|
||||
|
||||
* unwatched: Display or hide unwatched content (True, False). [all]
|
||||
* duplicate: Display or hide duplicate items (True, False). [movie]
|
||||
* actor: List of actors to search ([actor_or_id, ...]). [movie]
|
||||
* collection: List of collections to search within ([collection_or_id, ...]). [all]
|
||||
* contentRating: List of content ratings to search within ([rating_or_key, ...]). [movie,tv]
|
||||
* country: List of countries to search within ([country_or_key, ...]). [movie,music]
|
||||
* decade: List of decades to search within ([yyy0, ...]). [movie]
|
||||
* director: List of directors to search ([director_or_id, ...]). [movie]
|
||||
* genre: List Genres to search within ([genere_or_id, ...]). [all]
|
||||
* network: List of TV networks to search within ([resolution_or_key, ...]). [tv]
|
||||
* resolution: List of video resolutions to search within ([resolution_or_key, ...]). [movie]
|
||||
* studio: List of studios to search within ([studio_or_key, ...]). [music]
|
||||
* year: List of years to search within ([yyyy, ...]). [all]
|
||||
"""
|
||||
# Cleanup the core arguments
|
||||
args = {}
|
||||
for category, value in kwargs.items():
|
||||
args[category] = self._cleanSearchFilter(category, value, libtype)
|
||||
if title is not None:
|
||||
args['title'] = title
|
||||
if sort is not None:
|
||||
args['sort'] = self._cleanSearchSort(sort)
|
||||
if libtype is not None:
|
||||
args['type'] = utils.searchType(libtype)
|
||||
# Iterate over the results
|
||||
results, subresults = [], '_init'
|
||||
args['X-Plex-Container-Start'] = 0
|
||||
args['X-Plex-Container-Size'] = min(X_PLEX_CONTAINER_SIZE, maxresults)
|
||||
while subresults and maxresults > len(results):
|
||||
query = '/library/sections/%s/all%s' % (
|
||||
self.key, utils.joinArgs(args))
|
||||
subresults = utils.listItems(self.server, query)
|
||||
results += subresults[:maxresults - len(results)]
|
||||
args['X-Plex-Container-Start'] += args['X-Plex-Container-Size']
|
||||
return results
|
||||
|
||||
def _cleanSearchFilter(self, category, value, libtype=None):
|
||||
# check a few things before we begin
|
||||
if category not in self.ALLOWED_FILTERS:
|
||||
raise BadRequest('Unknown filter category: %s' % category)
|
||||
if category in self.BOOLEAN_FILTERS:
|
||||
return '1' if value else '0'
|
||||
if not isinstance(value, (list, tuple)):
|
||||
value = [value]
|
||||
# convert list of values to list of keys or ids
|
||||
result = set()
|
||||
choices = self.listChoices(category, libtype)
|
||||
lookup = {c.title.lower(): unquote(unquote(c.key)) for c in choices}
|
||||
allowed = set(c.key for c in choices)
|
||||
for item in value:
|
||||
item = str(item.id if isinstance(item, MediaTag) else item).lower()
|
||||
# find most logical choice(s) to use in url
|
||||
if item in allowed:
|
||||
result.add(item)
|
||||
continue
|
||||
if item in lookup:
|
||||
result.add(lookup[item])
|
||||
continue
|
||||
matches = [k for t, k in lookup.items() if item in t]
|
||||
if matches:
|
||||
map(result.add, matches)
|
||||
continue
|
||||
# nothing matched; use raw item value
|
||||
log.warning('Filter value not listed, using raw item value: %s' % item)
|
||||
result.add(item)
|
||||
return ','.join(result)
|
||||
|
||||
def _cleanSearchSort(self, sort):
|
||||
sort = '%s:asc' % sort if ':' not in sort else sort
|
||||
scol, sdir = sort.lower().split(':')
|
||||
lookup = {s.lower(): s for s in self.ALLOWED_SORT}
|
||||
if scol not in lookup:
|
||||
raise BadRequest('Unknown sort column: %s' % scol)
|
||||
if sdir not in ('asc', 'desc'):
|
||||
raise BadRequest('Unknown sort dir: %s' % sdir)
|
||||
return '%s:%s' % (lookup[scol], sdir)
|
||||
|
||||
|
||||
class MovieSection(LibrarySection):
|
||||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
|
||||
|
||||
Attributes:
|
||||
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('unwatched',
|
||||
'duplicate', 'year', 'decade', 'genre', 'contentRating', 'collection',
|
||||
'director', 'actor', 'country', 'studio', 'resolution')
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
|
||||
'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
|
||||
'mediaHeight', 'duration')
|
||||
TYPE (str): 'movie'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('unwatched', 'duplicate', 'year', 'decade', 'genre', 'contentRating',
|
||||
'collection', 'director', 'actor', 'country', 'studio', 'resolution')
|
||||
ALLOWED_SORT = ('addedAt', 'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
|
||||
'mediaHeight', 'duration')
|
||||
TYPE = 'movie'
|
||||
|
||||
|
||||
class ShowSection(LibrarySection):
|
||||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing tv shows.
|
||||
|
||||
Attributes:
|
||||
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('unwatched',
|
||||
'year', 'genre', 'contentRating', 'network', 'collection')
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt', 'lastViewedAt',
|
||||
'originallyAvailableAt', 'titleSort', 'rating', 'unwatched')
|
||||
TYPE (str): 'show'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('unwatched', 'year', 'genre', 'contentRating', 'network', 'collection')
|
||||
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'originallyAvailableAt', 'titleSort',
|
||||
'rating', 'unwatched')
|
||||
TYPE = 'show'
|
||||
|
||||
def searchShows(self, **kwargs):
|
||||
""" Search for a show. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
return self.search(libtype='show', **kwargs)
|
||||
|
||||
def searchEpisodes(self, **kwargs):
|
||||
""" Search for an episode. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
return self.search(libtype='episode', **kwargs)
|
||||
|
||||
def recentlyAdded(self, libtype='episode', maxresults=50):
|
||||
""" Returns a list of recently added episodes from this library section.
|
||||
|
||||
Parameters:
|
||||
maxresults (int): Max number of items to return (default 50).
|
||||
"""
|
||||
return self.search(sort='addedAt:desc', libtype=libtype, maxresults=maxresults)
|
||||
|
||||
|
||||
class MusicSection(LibrarySection):
|
||||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing music artists.
|
||||
|
||||
Attributes:
|
||||
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('genre',
|
||||
'country', 'collection')
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
|
||||
'lastViewedAt', 'viewCount', 'titleSort')
|
||||
TYPE (str): 'artist'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('genre', 'country', 'collection')
|
||||
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort')
|
||||
TYPE = 'artist'
|
||||
|
||||
def albums(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
|
||||
return utils.listItems(self.server, '/library/sections/%s/albums' % self.key)
|
||||
|
||||
def searchArtists(self, **kwargs):
|
||||
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
return self.search(libtype='artist', **kwargs)
|
||||
|
||||
def searchAlbums(self, **kwargs):
|
||||
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
return self.search(libtype='album', **kwargs)
|
||||
|
||||
def searchTracks(self, **kwargs):
|
||||
""" Search for a track. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
return self.search(libtype='track', **kwargs)
|
||||
|
||||
|
||||
class PhotoSection(LibrarySection):
|
||||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing photos.
|
||||
|
||||
Attributes:
|
||||
ALLOWED_FILTERS (list<str>): List of allowed search filters. <NONE>
|
||||
ALLOWED_SORT (list<str>): List of allowed sorting keys. <NONE>
|
||||
TYPE (str): 'photo'
|
||||
"""
|
||||
ALLOWED_FILTERS = ()
|
||||
ALLOWED_SORT = ()
|
||||
TYPE = 'photo'
|
||||
|
||||
def searchAlbums(self, **kwargs):
|
||||
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
return self.search(libtype='photo', **kwargs)
|
||||
|
||||
def searchPhotos(self, **kwargs):
|
||||
""" Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
return self.search(libtype='photo', **kwargs)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class FilterChoice(object):
|
||||
""" Represents a single filter choice. These objects are gathered when using filters
|
||||
while searching for library items and is the object returned in the result set of
|
||||
:func:`~plexapi.library.LibrarySection.listChoices()`.
|
||||
|
||||
Attributes:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to.
|
||||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
fastKey (str): API path to quickly list all items in this filter
|
||||
(/library/sections/<section>/all?genre=<key>)
|
||||
key (str): Short key (id) of this filter option (used ad <key> in fastKey above).
|
||||
thumb (str): Thumbnail used to represent this filter option.
|
||||
title (str): Human readable name for this filter option.
|
||||
type (str): Filter type (genre, contentRating, etc).
|
||||
"""
|
||||
TYPE = 'Directory'
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
self.fastKey = data.attrib.get('fastKey')
|
||||
self.key = data.attrib.get('key')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
|
||||
def __repr__(self):
|
||||
title = self.title.replace(' ', '.')[0:20]
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.key, title)
|
|
@ -1,459 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import plexapi, requests
|
||||
from plexapi import TIMEOUT, log, logfilter, utils
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.compat import ElementTree
|
||||
from plexapi.server import PlexServer
|
||||
from requests.status_codes import _codes as codes
|
||||
|
||||
|
||||
class MyPlexAccount(object):
|
||||
""" MyPlex account and profile information. The easiest way to build
|
||||
this object is by calling the staticmethod :func:`~plexapi.myplex.MyPlexAccount.signin`
|
||||
with your username and password. This object represents the data found Account on
|
||||
the myplex.tv servers at the url https://plex.tv/users/account.
|
||||
|
||||
Attributes:
|
||||
authenticationToken (str): <Unknown>
|
||||
certificateVersion (str): <Unknown>
|
||||
cloudSyncDevice (str):
|
||||
email (str): Your current Plex email address.
|
||||
entitlements (List<str>): List of devices your allowed to use with this account.
|
||||
guest (bool): <Unknown>
|
||||
home (bool): <Unknown>
|
||||
homeSize (int): <Unknown>
|
||||
id (str): Your Plex account ID.
|
||||
locale (str): Your Plex locale
|
||||
mailing_list_status (str): Your current mailing list status.
|
||||
maxHomeSize (int): <Unknown>
|
||||
queueEmail (str): Email address to add items to your `Watch Later` queue.
|
||||
queueUid (str): <Unknown>
|
||||
restricted (bool): <Unknown>
|
||||
roles: (List<str>) Lit of account roles. Plexpass membership listed here.
|
||||
scrobbleTypes (str): Description
|
||||
secure (bool): Description
|
||||
subscriptionActive (bool): True if your subsctiption is active.
|
||||
subscriptionFeatures: (List<str>) List of features allowed on your subscription.
|
||||
subscriptionPlan (str): Name of subscription plan.
|
||||
subscriptionStatus (str): String representation of `subscriptionActive`.
|
||||
thumb (str): URL of your account thumbnail.
|
||||
title (str): <Unknown> - Looks like an alias for `username`.
|
||||
username (str): Your account username.
|
||||
uuid (str): <Unknown>
|
||||
"""
|
||||
BASEURL = 'https://plex.tv/users/account'
|
||||
SIGNIN = 'https://my.plexapp.com/users/sign_in.xml'
|
||||
|
||||
<<<<<<< HEAD
|
||||
def __init__(self, data, initpath=None):
|
||||
=======
|
||||
def __init__(self, data=None, initpath=None, username=None, password=None, session=None):
|
||||
"""Sets the attrs.
|
||||
|
||||
Args:
|
||||
data (Element): XML response from PMS as a Element
|
||||
initpath (string, optional): relative path.
|
||||
"""
|
||||
if data is None and username and password:
|
||||
self.Signin(username, password)#
|
||||
|
||||
self._session = session or requests.Session()
|
||||
>>>>>>> testing testing, 1, 2, 3.
|
||||
self.authenticationToken = data.attrib.get('authenticationToken')
|
||||
if self.authenticationToken:
|
||||
logfilter.add_secret(self.authenticationToken)
|
||||
self.certificateVersion = data.attrib.get('certificateVersion')
|
||||
self.cloudSyncDevice = data.attrib.get('cloudSyncDevice')
|
||||
self.email = data.attrib.get('email')
|
||||
self.guest = utils.cast(bool, data.attrib.get('guest'))
|
||||
self.home = utils.cast(bool, data.attrib.get('home'))
|
||||
self.homeSize = utils.cast(int, data.attrib.get('homeSize'))
|
||||
self.id = data.attrib.get('id')
|
||||
self.locale = data.attrib.get('locale')
|
||||
self.mailing_list_status = data.attrib.get('mailing_list_status')
|
||||
self.maxHomeSize = utils.cast(int, data.attrib.get('maxHomeSize'))
|
||||
self.queueEmail = data.attrib.get('queueEmail')
|
||||
self.queueUid = data.attrib.get('queueUid')
|
||||
self.restricted = utils.cast(bool, data.attrib.get('restricted'))
|
||||
self.scrobbleTypes = data.attrib.get('scrobbleTypes')
|
||||
self.secure = utils.cast(bool, data.attrib.get('secure'))
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.title = data.attrib.get('title')
|
||||
self.username = data.attrib.get('username')
|
||||
self.uuid = data.attrib.get('uuid')
|
||||
# TODO: Complete these items!
|
||||
self.subscriptionActive = None # renamed on server
|
||||
self.subscriptionStatus = None # renamed on server
|
||||
self.subscriptionPlan = None # renmaed on server
|
||||
self.subscriptionFeatures = None # renamed on server
|
||||
self.roles = None
|
||||
self.entitlements = None
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username.encode('utf8'))
|
||||
|
||||
def device(self, name):
|
||||
""" Returns the :class:`~plexapi.myplex.MyPlexDevice` that matches the name specified.
|
||||
|
||||
Parameters:
|
||||
name (str): Name to match against.
|
||||
"""
|
||||
return _findItem(self.devices(), name)
|
||||
|
||||
def devices(self):
|
||||
""" Returns a list of all :class:`~plexapi.myplex.MyPlexDevice` objects connected to the server. """
|
||||
return _listItems(MyPlexDevice.BASEURL, self.authenticationToken, MyPlexDevice)
|
||||
|
||||
def resources(self):
|
||||
""" Returns a list of all :class:`~plexapi.myplex.MyPlexResource` objects connected to the server. """
|
||||
return _listItems(MyPlexResource.BASEURL, self.authenticationToken, MyPlexResource)
|
||||
|
||||
def resource(self, name):
|
||||
""" Returns the :class:`~plexapi.myplex.MyPlexResource` that matches the name specified.
|
||||
|
||||
Parameters:
|
||||
name (str): Name to match against.
|
||||
"""
|
||||
return _findItem(self.resources(), name)
|
||||
|
||||
def users(self):
|
||||
""" Returns a list of all :class:`~plexapi.myplex.MyPlexUser` objects connected to your account. """
|
||||
return _listItems(MyPlexUser.BASEURL, self.authenticationToken, MyPlexUser)
|
||||
|
||||
def user(self, email):
|
||||
""" Returns the :class:`~myplex.MyPlexUser` that matches the email or username specified.
|
||||
|
||||
Parameters:
|
||||
email (str): Username or email to match against.
|
||||
"""
|
||||
return _findItem(self.users(), email, ['username', 'email'])
|
||||
|
||||
@classmethod
|
||||
<<<<<<< HEAD
|
||||
def signin(cls, username, password):
|
||||
""" Returns a new :class:`~myplex.MyPlexAccount` object by connecting to MyPlex with the
|
||||
specified username and password. This is essentially logging into MyPlex and often
|
||||
the very first entry point to using this API.
|
||||
=======
|
||||
def signin(cls, username, password, session=None):
|
||||
"""Summary
|
||||
>>>>>>> testing testing, 1, 2, 3.
|
||||
|
||||
Parameters:
|
||||
username (str): Your MyPlex.tv username.
|
||||
password (str): Your MyPlex.tv password.
|
||||
|
||||
<<<<<<< HEAD
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.Unauthorized`: (401) If the username or password are invalid.
|
||||
:class:`~plexapi.exceptions.BadRequest`: If any other errors occured not allowing us to log into MyPlex.tv.
|
||||
=======
|
||||
Returns:
|
||||
class: MyPlexAccount
|
||||
|
||||
Raises:
|
||||
BadRequest: (HTTPCODE) http codename
|
||||
Unauthorized: (401) http codename
|
||||
>>>>>>> testing testing, 1, 2, 3.
|
||||
"""
|
||||
if 'X-Plex-Token' in plexapi.BASE_HEADERS:
|
||||
del plexapi.BASE_HEADERS['X-Plex-Token']
|
||||
auth = (username, password)
|
||||
log.info('POST %s', cls.SIGNIN)
|
||||
sess = session or requests.Session()
|
||||
response = sess.post(
|
||||
cls.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT)
|
||||
if response.status_code != requests.codes.created:
|
||||
codename = codes.get(response.status_code)[0]
|
||||
if response.status_code == 401:
|
||||
raise Unauthorized('(%s) %s' %
|
||||
(response.status_code, codename))
|
||||
raise BadRequest('(%s) %s' % (response.status_code, codename))
|
||||
data = ElementTree.fromstring(response.text.encode('utf8'))
|
||||
return MyPlexAccount(data, cls.SIGNIN, session=sess)
|
||||
|
||||
|
||||
class MyPlexUser(object):
|
||||
""" This object represents non-signed in users such as friends and linked
|
||||
accounts. NOTE: This should not be confused with the :class:`~myplex.MyPlexAccount`
|
||||
which is your specific account. The raw xml for the data presented here
|
||||
can be found at: https://plex.tv/api/users/
|
||||
|
||||
Attributes:
|
||||
allowCameraUpload (bool): True if this user can upload images
|
||||
allowChannels (bool): True if this user has access to channels
|
||||
allowSync (bool): True if this user can sync
|
||||
email (str): User's email address (user@gmail.com)
|
||||
filterAll (str): Unknown
|
||||
filterMovies (str): Unknown
|
||||
filterMusic (str): Unknown
|
||||
filterPhotos (str): Unknown
|
||||
filterTelevision (str): Unknown
|
||||
home (bool): Unknown
|
||||
id (int): User's Plex account ID.
|
||||
protected (False): Unknown (possibly SSL enabled?)
|
||||
recommendationsPlaylistId (str): Unknown
|
||||
restricted (str): Unknown
|
||||
thumb (str): Link to the users avatar
|
||||
title (str): Seems to be an aliad for username
|
||||
username (str): User's username
|
||||
"""
|
||||
BASEURL = 'https://plex.tv/api/users/'
|
||||
|
||||
def __init__(self, data, initpath=None):
|
||||
self.allowCameraUpload = utils.cast(bool, data.attrib.get('allowCameraUpload'))
|
||||
self.allowChannels = utils.cast(bool, data.attrib.get('allowChannels'))
|
||||
self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
|
||||
self.email = data.attrib.get('email')
|
||||
self.filterAll = data.attrib.get('filterAll')
|
||||
self.filterMovies = data.attrib.get('filterMovies')
|
||||
self.filterMusic = data.attrib.get('filterMusic')
|
||||
self.filterPhotos = data.attrib.get('filterPhotos')
|
||||
self.filterTelevision = data.attrib.get('filterTelevision')
|
||||
self.home = utils.cast(bool, data.attrib.get('home'))
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.protected = utils.cast(bool, data.attrib.get('protected'))
|
||||
self.recommendationsPlaylistId = data.attrib.get('recommendationsPlaylistId')
|
||||
self.restricted = data.attrib.get('restricted')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.title = data.attrib.get('title')
|
||||
self.username = data.attrib.get('username')
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username)
|
||||
|
||||
|
||||
class MyPlexResource(object):
|
||||
""" This object represents resources connected to your Plex server that can provide
|
||||
content such as Plex Media Servers, iPhone or Android clients, etc. The raw xml
|
||||
for the data presented here can be found at: https://plex.tv/api/resources?includeHttps=1
|
||||
|
||||
Attributes:
|
||||
accessToken (str): This resources accesstoken.
|
||||
clientIdentifier (str): Unique ID for this resource.
|
||||
connections (list): List of :class:`~myplex.ResourceConnection` objects
|
||||
for this resource.
|
||||
createdAt (datetime): Timestamp this resource first connected to your server.
|
||||
device (str): Best guess on the type of device this is (PS, iPhone, Linux, etc).
|
||||
home (bool): Unknown
|
||||
lastSeenAt (datetime): Timestamp this resource last connected.
|
||||
name (str): Descriptive name of this resource.
|
||||
owned (bool): True if this resource is one of your own (you logged into it).
|
||||
platform (str): OS the resource is running (Linux, Windows, Chrome, etc.)
|
||||
platformVersion (str): Version of the platform.
|
||||
presence (bool): True if the resource is online
|
||||
product (str): Plex product (Plex Media Server, Plex for iOS, Plex Web, etc.)
|
||||
productVersion (str): Version of the product.
|
||||
provides (str): List of services this resource provides (client, server,
|
||||
player, pubsub-player, etc.)
|
||||
synced (bool): Unknown (possibly True if the resource has synced content?)
|
||||
"""
|
||||
BASEURL = 'https://plex.tv/api/resources?includeHttps=1'
|
||||
|
||||
def __init__(self, data):
|
||||
self.name = data.attrib.get('name')
|
||||
self.accessToken = data.attrib.get('accessToken')
|
||||
if self.accessToken:
|
||||
logfilter.add_secret(self.accessToken)
|
||||
self.product = data.attrib.get('product')
|
||||
self.productVersion = data.attrib.get('productVersion')
|
||||
self.platform = data.attrib.get('platform')
|
||||
self.platformVersion = data.attrib.get('platformVersion')
|
||||
self.device = data.attrib.get('device')
|
||||
self.clientIdentifier = data.attrib.get('clientIdentifier')
|
||||
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
|
||||
self.lastSeenAt = utils.toDatetime(data.attrib.get('lastSeenAt'))
|
||||
self.provides = data.attrib.get('provides')
|
||||
self.owned = utils.cast(bool, data.attrib.get('owned'))
|
||||
self.home = utils.cast(bool, data.attrib.get('home'))
|
||||
self.synced = utils.cast(bool, data.attrib.get('synced'))
|
||||
self.presence = utils.cast(bool, data.attrib.get('presence'))
|
||||
self.connections = [ResourceConnection(elem) for elem in data if elem.tag == 'Connection']
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'))
|
||||
|
||||
def connect(self, ssl=None):
|
||||
""" Returns a new :class:`~server.PlexServer` object. Often times there is more than
|
||||
one address specified for a server or client. This function will prioritize local
|
||||
connections before remote and HTTPS before HTTP. After trying to connect to all
|
||||
available addresses for this resource and assuming at least one connection was
|
||||
successful, the PlexServer object is built and returned.
|
||||
|
||||
Parameters:
|
||||
ssl (optional): Set True to only connect to HTTPS connections. Set False to
|
||||
only connect to HTTP connections. Set None (default) to connect to any
|
||||
HTTP or HTTPS connection.
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
|
||||
"""
|
||||
# Sort connections from (https, local) to (http, remote)
|
||||
# Only check non-local connections unless we own the resource
|
||||
forcelocal = lambda c: self.owned or c.local
|
||||
connections = sorted(self.connections, key=lambda c: c.local, reverse=True)
|
||||
https = [c.uri for c in self.connections if forcelocal(c)]
|
||||
http = [c.httpuri for c in self.connections if forcelocal(c)]
|
||||
# Force ssl, no ssl, or any (default)
|
||||
if ssl is True: connections = https
|
||||
elif ssl is False: connections = http
|
||||
else: connections = https + http
|
||||
# Try connecting to all known resource connections in parellel, but
|
||||
# only return the first server (in order) that provides a response.
|
||||
listargs = [[c] for c in connections]
|
||||
results = utils.threaded(self._connect, listargs)
|
||||
# At this point we have a list of result tuples containing (url, token, PlexServer)
|
||||
# or (url, token, None) in the case a connection could not be
|
||||
# established.
|
||||
for url, token, result in results:
|
||||
okerr = 'OK' if result else 'ERR'
|
||||
<<<<<<< HEAD
|
||||
log.info('Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr)
|
||||
=======
|
||||
log.info(
|
||||
'Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr)
|
||||
|
||||
>>>>>>> testing testing, 1, 2, 3.
|
||||
results = [r[2] for r in results if r and r[2] is not None]
|
||||
if not results:
|
||||
raise NotFound('Unable to connect to resource: %s' % self.name)
|
||||
log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token)
|
||||
return results[0]
|
||||
|
||||
def _connect(self, url, results, i):
|
||||
try:
|
||||
results[i] = (url, self.accessToken, PlexServer(url, self.accessToken))
|
||||
except NotFound:
|
||||
results[i] = (url, self.accessToken, None)
|
||||
|
||||
|
||||
class ResourceConnection(object):
|
||||
""" Represents a Resource Connection object found within the
|
||||
:class:`~myplex.MyPlexResource` objects.
|
||||
|
||||
Attributes:
|
||||
address (str): Local IP address
|
||||
httpuri (str): Full local address
|
||||
local (bool): True if local
|
||||
port (int): 32400
|
||||
protocol (str): HTTP or HTTPS
|
||||
uri (str): External address
|
||||
"""
|
||||
def __init__(self, data):
|
||||
self.protocol = data.attrib.get('protocol')
|
||||
self.address = data.attrib.get('address')
|
||||
self.port = utils.cast(int, data.attrib.get('port'))
|
||||
self.uri = data.attrib.get('uri')
|
||||
self.local = utils.cast(bool, data.attrib.get('local'))
|
||||
self.httpuri = 'http://%s:%s' % (self.address, self.port)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8'))
|
||||
|
||||
|
||||
class MyPlexDevice(object):
|
||||
""" This object represents resources connected to your Plex server that provide
|
||||
playback ability from your Plex Server, iPhone or Android clients, Plex Web,
|
||||
this API, etc. The raw xml for the data presented here can be found at:
|
||||
https://plex.tv/devices.xml
|
||||
|
||||
Attributes:
|
||||
clientIdentifier (str): Unique ID for this resource.
|
||||
connections (list): List of connection URIs for the device.
|
||||
device (str): Best guess on the type of device this is (Linux, iPad, AFTB, etc).
|
||||
id (str): MyPlex ID of the device.
|
||||
model (str): Model of the device (bueller, Linux, x86_64, etc.)
|
||||
name (str): Hostname of the device.
|
||||
platform (str): OS the resource is running (Linux, Windows, Chrome, etc.)
|
||||
platformVersion (str): Version of the platform.
|
||||
product (str): Plex product (Plex Media Server, Plex for iOS, Plex Web, etc.)
|
||||
productVersion (string): Version of the product.
|
||||
provides (str): List of services this resource provides (client, controller,
|
||||
sync-target, player, pubsub-player).
|
||||
publicAddress (str): Public IP address.
|
||||
screenDensity (str): Unknown
|
||||
screenResolution (str): Screen resolution (750x1334, 1242x2208, etc.)
|
||||
token (str): Plex authentication token for the device.
|
||||
vendor (str): Device vendor (ubuntu, etc).
|
||||
version (str): Unknown (1, 2, 1.3.3.3148-b38628e, 1.3.15, etc.)
|
||||
"""
|
||||
|
||||
BASEURL = 'https://plex.tv/devices.xml'
|
||||
|
||||
def __init__(self, data):
|
||||
self.name = data.attrib.get('name')
|
||||
self.publicAddress = data.attrib.get('publicAddress')
|
||||
self.product = data.attrib.get('product')
|
||||
self.productVersion = data.attrib.get('productVersion')
|
||||
self.platform = data.attrib.get('platform')
|
||||
self.platformVersion = data.attrib.get('platformVersion')
|
||||
self.device = data.attrib.get('device')
|
||||
self.model = data.attrib.get('model')
|
||||
self.vendor = data.attrib.get('vendor')
|
||||
self.provides = data.attrib.get('provides')
|
||||
self.clientIdentifier = data.attrib.get('clientIdentifier')
|
||||
self.version = data.attrib.get('version')
|
||||
self.id = data.attrib.get('id')
|
||||
self.token = data.attrib.get('token')
|
||||
if self.token:
|
||||
logfilter.add_secret(self.token)
|
||||
self.screenResolution = data.attrib.get('screenResolution')
|
||||
self.screenDensity = data.attrib.get('screenDensity')
|
||||
self.connections = [connection.attrib.get('uri') for connection in data.iter('Connection')]
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'), self.product.encode('utf8'))
|
||||
|
||||
def connect(self):
|
||||
""" Returns a new :class:`~plexapi.client.PlexClient` object. Sometimes there is more than
|
||||
one address specified for a server or client. After trying to connect to all
|
||||
available addresses for this resource and assuming at least one connection was
|
||||
successful, the PlexClient object is built and returned.
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device.
|
||||
"""
|
||||
# Try connecting to all known resource connections in parellel, but
|
||||
# only return the first server (in order) that provides a response.
|
||||
listargs = [[c] for c in self.connections]
|
||||
results = utils.threaded(self._connect, listargs)
|
||||
# At this point we have a list of result tuples containing (url, token, PlexServer)
|
||||
# or (url, token, None) in the case a connection could not be
|
||||
# established.
|
||||
for url, token, result in results:
|
||||
okerr = 'OK' if result else 'ERR'
|
||||
log.info('Testing device connection: %s?X-Plex-Token=%s %s', url, token, okerr)
|
||||
results = [r[2] for r in results if r and r[2] is not None]
|
||||
if not results:
|
||||
raise NotFound('Unable to connect to resource: %s' % self.name)
|
||||
log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token)
|
||||
return results[0]
|
||||
|
||||
def _connect(self, url, results, i):
|
||||
try:
|
||||
results[i] = (url, self.token, PlexClient(url, self.token))
|
||||
except NotFound:
|
||||
results[i] = (url, self.token, None)
|
||||
|
||||
|
||||
def _findItem(items, value, attrs=None):
|
||||
""" This will return the first item in the list of items where value is
|
||||
found in any of the specified attributes.
|
||||
"""
|
||||
attrs = attrs or ['name']
|
||||
for item in items:
|
||||
for attr in attrs:
|
||||
if value.lower() == getattr(item, attr).lower():
|
||||
return item
|
||||
raise NotFound('Unable to find item %s' % value)
|
||||
|
||||
|
||||
def _listItems(url, token, cls):
|
||||
""" Builds list of classes from a XML response. """
|
||||
headers = plexapi.BASE_HEADERS
|
||||
headers['X-Plex-Token'] = token
|
||||
log.info('GET %s?X-Plex-Token=%s', url, token)
|
||||
response = requests.get(url, headers=headers, timeout=TIMEOUT)
|
||||
data = ElementTree.fromstring(response.text.encode('utf8'))
|
||||
return [cls(elem) for elem in data]
|
|
@ -1,594 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging, re
|
||||
import os
|
||||
from datetime import datetime
|
||||
from plexapi.compat import quote, urlencode, string_type
|
||||
import requests
|
||||
|
||||
from plexapi.exceptions import NotFound, UnknownType, Unsupported
|
||||
from threading import Thread
|
||||
from plexapi import log
|
||||
|
||||
|
||||
# Search Types - Plex uses these to filter specific media types when searching.
|
||||
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3,
|
||||
'episode': 4, 'artist': 8, 'album': 9, 'track': 10}
|
||||
|
||||
LIBRARY_TYPES = {}
|
||||
|
||||
|
||||
def register_libtype(cls):
|
||||
""" Registry of library types we may come across when parsing XML. This allows us to
|
||||
define a few helper functions to dynamically convery the XML into objects. See
|
||||
buildItem() below for an example.
|
||||
"""
|
||||
LIBRARY_TYPES[cls.TYPE] = cls
|
||||
return cls
|
||||
|
||||
|
||||
class NA(object):
|
||||
""" This used to be a simple variable equal to '__NA__'. There has been need to
|
||||
compare NA against None in some use cases. This object allows the internals
|
||||
of PlexAPI to distinguish between unfetched values and fetched, but non-existent
|
||||
values. (NA == None results to True; NA is None results to False)
|
||||
"""
|
||||
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, NA) or other in [None, '__NA__']
|
||||
|
||||
def __nonzero__(self):
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return '__NA__'
|
||||
|
||||
|
||||
class SecretsFilter(logging.Filter):
|
||||
""" Logging filter to hide secrets. """
|
||||
def __init__(self, secrets=None):
|
||||
self.secrets = secrets or set()
|
||||
|
||||
def add_secret(self, secret):
|
||||
self.secrets.add(secret)
|
||||
|
||||
def filter(self, record):
|
||||
cleanargs = list(record.args)
|
||||
for i in range(len(cleanargs)):
|
||||
if isinstance(cleanargs[i], string_type):
|
||||
for secret in self.secrets:
|
||||
cleanargs[i] = cleanargs[i].replace(secret, '<hidden>')
|
||||
record.args = tuple(cleanargs)
|
||||
return True
|
||||
|
||||
|
||||
class PlexPartialObject(object):
|
||||
""" Not all objects in the Plex listings return the complete list of elements
|
||||
for the object. This object will allow you to assume each object is complete,
|
||||
and if the specified value you request is None it will fetch the full object
|
||||
automatically and update itself.
|
||||
|
||||
Attributes:
|
||||
data (ElementTree): Response from PlexServer used to build this object (optional).
|
||||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
"""
|
||||
def __init__(self, data, initpath, server=None):
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
self._loadData(data)
|
||||
self._reloaded = False
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.key == other.key
|
||||
|
||||
def __repr__(self):
|
||||
clsname = self.__class__.__name__
|
||||
key = self.key.replace('/library/metadata/', '') if self.key else 'NA'
|
||||
title = self.title.replace(' ', '.')[0:20].encode('utf8')
|
||||
return '<%s:%s:%s>' % (clsname, key, title)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# Auto reload self, from the full key (path) when needed.
|
||||
if attr == 'key' or self.__dict__.get(attr) or self.isFullObject():
|
||||
return self.__dict__.get(attr, NA)
|
||||
print('reload because of %s' % attr)
|
||||
self.reload()
|
||||
return self.__dict__.get(attr, NA)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if value != NA or self.isFullObject():
|
||||
self.__dict__[attr] = value
|
||||
|
||||
def _loadData(self, data):
|
||||
raise Exception('Abstract method not implemented.')
|
||||
|
||||
def isFullObject(self):
|
||||
""" Retruns True if this is already a full object. A full object means all attributes
|
||||
were populated from the api path representing only this item. For example, the
|
||||
search result for a movie often only contain a portion of the attributes a full
|
||||
object (main url) for that movie contain.
|
||||
"""
|
||||
return not self.key or self.key == self.initpath
|
||||
|
||||
def isPartialObject(self):
|
||||
""" Returns True if this is NOT a full object. """
|
||||
return not self.isFullObject()
|
||||
|
||||
def reload(self):
|
||||
""" Reload the data for this object from PlexServer XML. """
|
||||
data = self.server.query(self.key)
|
||||
self.initpath = self.key
|
||||
self._loadData(data[0])
|
||||
self._reloaded = True
|
||||
|
||||
|
||||
class Playable(object):
|
||||
""" This is a general place to store functions specific to media that is Playable.
|
||||
Things were getting mixed up a bit when dealing with Shows, Season, Artists,
|
||||
Albums which are all not playable.
|
||||
|
||||
Attributes:
|
||||
player (:class:`~plexapi.client.PlexClient`): Client object playing this item (for active sessions).
|
||||
playlistItemID (int): Playlist item ID (only populated for :class:`~plexapi.playlist.Playlist` items).
|
||||
sessionKey (int): Active session key.
|
||||
transcodeSession (:class:`~plexapi.media.TranscodeSession`): Transcode Session object
|
||||
if item is being transcoded (None otherwise).
|
||||
username (str): Username of the person playing this item (for active sessions).
|
||||
viewedAt (datetime): Datetime item was last viewed (history).
|
||||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
# Load data for active sessions (/status/sessions)
|
||||
self.sessionKey = cast(int, data.attrib.get('sessionKey', NA))
|
||||
self.username = findUsername(data)
|
||||
self.player = findPlayer(self.server, data)
|
||||
self.transcodeSession = findTranscodeSession(self.server, data)
|
||||
# Load data for history details (/status/sessions/history/all)
|
||||
self.viewedAt = toDatetime(data.attrib.get('viewedAt', NA))
|
||||
# Load data for playlist items
|
||||
self.playlistItemID = cast(int, data.attrib.get('playlistItemID', NA))
|
||||
|
||||
def getStreamURL(self, **params):
|
||||
""" Returns a stream url that may be used by external applications such as VLC.
|
||||
|
||||
Parameters:
|
||||
**params (dict): optional parameters to manipulate the playback when accessing
|
||||
the stream. A few known parameters include: maxVideoBitrate, videoResolution
|
||||
offset, copyts, protocol, mediaIndex, platform.
|
||||
|
||||
Raises:
|
||||
Unsupported: When the item doesn't support fetching a stream URL.
|
||||
"""
|
||||
if self.TYPE not in ('movie', 'episode', 'track'):
|
||||
raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE)
|
||||
mvb = params.get('maxVideoBitrate')
|
||||
vr = params.get('videoResolution', '')
|
||||
params = {
|
||||
'path': self.key,
|
||||
'offset': params.get('offset', 0),
|
||||
'copyts': params.get('copyts', 1),
|
||||
'protocol': params.get('protocol'),
|
||||
'mediaIndex': params.get('mediaIndex', 0),
|
||||
'X-Plex-Platform': params.get('platform', 'Chrome'),
|
||||
'maxVideoBitrate': max(mvb, 64) if mvb else None,
|
||||
'videoResolution': vr if re.match('^\d+x\d+$', vr) else None
|
||||
}
|
||||
# remove None values
|
||||
params = {k: v for k, v in params.items() if v is not None}
|
||||
streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video'
|
||||
# sort the keys since the randomness fucks with my tests..
|
||||
sorted_params = sorted(params.items(), key=lambda val: val[0])
|
||||
return self.server.url('/%s/:/transcode/universal/start.m3u8?%s' % (streamtype, urlencode(sorted_params)))
|
||||
|
||||
def iterParts(self):
|
||||
""" Iterates over the parts of this media item. """
|
||||
for item in self.media:
|
||||
for part in item.parts:
|
||||
yield part
|
||||
|
||||
def play(self, client):
|
||||
""" Start playback on the specified client.
|
||||
|
||||
Parameters:
|
||||
client (:class:`~plexapi.client.PlexClient`): Client to start playing on.
|
||||
"""
|
||||
client.playMedia(self)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
"""Download a episode. If kwargs are passed your can download a trancoded file.
|
||||
|
||||
Args:
|
||||
savepath (str): Abs path to savefolder
|
||||
keep_orginal_name (bool): Use the mediafiles orginal name
|
||||
|
||||
kwargs:
|
||||
See getStreamURL docs.
|
||||
|
||||
"""
|
||||
downloaded = []
|
||||
locs = [i for i in self.iterParts() if i]
|
||||
for loc in locs:
|
||||
if keep_orginal_name is False:
|
||||
name = '%s.%s' % (self._prettyfilename(), loc.container)
|
||||
else:
|
||||
name = loc.file
|
||||
|
||||
# So this seems to be a alot slower but allows transcode.
|
||||
if kwargs:
|
||||
download_url = self.getStreamURL(**kwargs)
|
||||
else:
|
||||
download_url = self.server.url('%s?download=1' % loc.key)
|
||||
|
||||
dl = download(download_url, filename=name, savepath=savepath, session=self.server.session)
|
||||
if dl:
|
||||
downloaded.append(dl)
|
||||
|
||||
return downloaded
|
||||
|
||||
|
||||
def buildItem(server, elem, initpath, bytag=False):
|
||||
""" Factory function to build the objects used within the PlexAPI.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
elem (ElementTree): XML data needed to build the object.
|
||||
initpath (str): Relative path requested when retrieving specified `data` (optional).
|
||||
bytag (bool): Creates the object from the name specified by the tag instead of the
|
||||
default which builds the object specified by the type attribute. <tag type='foo' />
|
||||
|
||||
Raises:
|
||||
UnknownType: Unknown library type.
|
||||
"""
|
||||
libtype = elem.tag if bytag else elem.attrib.get('type')
|
||||
if libtype == 'photo' and elem.tag == 'Directory':
|
||||
libtype = 'photoalbum'
|
||||
if libtype in LIBRARY_TYPES:
|
||||
cls = LIBRARY_TYPES[libtype]
|
||||
return cls(server, elem, initpath)
|
||||
raise UnknownType('Unknown library type: %s' % libtype)
|
||||
|
||||
|
||||
def cast(func, value):
|
||||
""" Cast the specified value to the specified type (returned by func).
|
||||
|
||||
Parameters:
|
||||
func (func): Calback function to used cast to type (int, bool, float, etc).
|
||||
value (any): value to be cast and returned.
|
||||
"""
|
||||
if value not in [None, NA]:
|
||||
if func == bool:
|
||||
return bool(int(value))
|
||||
elif func in [int, float]:
|
||||
try:
|
||||
return func(value)
|
||||
except ValueError:
|
||||
return float('nan')
|
||||
return func(value)
|
||||
return value
|
||||
|
||||
|
||||
def findKey(server, key):
|
||||
""" Finds and builds a object based on ratingKey.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
key (int): ratingKey to find and return.
|
||||
|
||||
Raises:
|
||||
NotFound: Unable to find key
|
||||
"""
|
||||
path = '/library/metadata/{0}'.format(key)
|
||||
try:
|
||||
# Item seems to be the first sub element
|
||||
elem = server.query(path)[0]
|
||||
return buildItem(server, elem, path)
|
||||
except:
|
||||
raise NotFound('Unable to find key: %s' % key)
|
||||
|
||||
|
||||
def findItem(server, path, title):
|
||||
""" Finds and builds a object based on title.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
path (str): API path that returns item to search title for.
|
||||
title (str): Title of the item to find and return.
|
||||
|
||||
Raises:
|
||||
NotFound: Unable to find item.
|
||||
"""
|
||||
for elem in server.query(path):
|
||||
if elem.attrib.get('title').lower() == title.lower():
|
||||
return buildItem(server, elem, path)
|
||||
raise NotFound('Unable to find item: %s' % title)
|
||||
|
||||
|
||||
def findLocations(data, single=False):
|
||||
""" Returns a list of filepaths from a location tag.
|
||||
|
||||
Parameters:
|
||||
data (ElementTree): XML object to search for locations in.
|
||||
single (bool): Set True to only return the first location found.
|
||||
Return type will be a string if this is set to True.
|
||||
"""
|
||||
locations = []
|
||||
for elem in data:
|
||||
if elem.tag == 'Location':
|
||||
locations.append(elem.attrib.get('path'))
|
||||
if single:
|
||||
return locations[0] if locations else None
|
||||
return locations
|
||||
|
||||
|
||||
def findPlayer(server, data):
|
||||
""" Returns the :class:`~plexapi.client.PlexClient` object found in the specified data.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
data (ElementTree): XML data to find Player in.
|
||||
"""
|
||||
elem = data.find('Player')
|
||||
if elem is not None:
|
||||
from plexapi.client import PlexClient
|
||||
baseurl = 'http://%s:%s' % (elem.attrib.get('address'), elem.attrib.get('port'))
|
||||
return PlexClient(baseurl, server=server, data=elem)
|
||||
return None
|
||||
|
||||
|
||||
def findStreams(media, streamtype):
|
||||
""" Returns a list of streams (str) found in media that match the specified streamtype.
|
||||
|
||||
Parameters:
|
||||
media (:class:`~plexapi.utils.Playable`): Item to search for streams (show, movie, episode).
|
||||
streamtype (str): Streamtype to return (videostream, audiostream, subtitlestream).
|
||||
"""
|
||||
streams = []
|
||||
for mediaitem in media:
|
||||
for part in mediaitem.parts:
|
||||
for stream in part.streams:
|
||||
if stream.TYPE == streamtype:
|
||||
streams.append(stream)
|
||||
return streams
|
||||
|
||||
|
||||
def findTranscodeSession(server, data):
|
||||
""" Returns a :class:`~plexapi.media.TranscodeSession` object if found within the specified
|
||||
XML data.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
data (ElementTree): XML data to find TranscodeSession in.
|
||||
"""
|
||||
|
||||
elem = data.find('TranscodeSession')
|
||||
if elem is not None:
|
||||
from plexapi import media
|
||||
return media.TranscodeSession(server, elem)
|
||||
return None
|
||||
|
||||
|
||||
def findUsername(data):
|
||||
""" Returns the username if found in the specified XML data. Returns None if not found.
|
||||
|
||||
Parameters:
|
||||
data (ElementTree): XML data to find username in.
|
||||
"""
|
||||
elem = data.find('User')
|
||||
if elem is not None:
|
||||
return elem.attrib.get('title')
|
||||
return None
|
||||
|
||||
|
||||
def isInt(str):
|
||||
""" Returns True if the specified string passes as an int. """
|
||||
try:
|
||||
int(str)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def joinArgs(args):
|
||||
""" Returns a query string (uses for HTTP URLs) where only the value is URL encoded.
|
||||
Example return value: '?genre=action&type=1337'.
|
||||
|
||||
Parameters:
|
||||
args (dict): Arguments to include in query string.
|
||||
"""
|
||||
if not args:
|
||||
return ''
|
||||
arglist = []
|
||||
for key in sorted(args, key=lambda x: x.lower()):
|
||||
value = str(args[key])
|
||||
arglist.append('%s=%s' % (key, quote(value)))
|
||||
return '?%s' % '&'.join(arglist)
|
||||
|
||||
|
||||
def listChoices(server, path):
|
||||
""" Returns a dict of {title:key} for all simple choices in a search filter.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
path (str): Relative path to request XML data from.
|
||||
"""
|
||||
return {c.attrib['title']: c.attrib['key'] for c in server.query(path)}
|
||||
|
||||
|
||||
def listItems(server, path, libtype=None, watched=None, bytag=False):
|
||||
""" Returns a list of object built from :func:`~plexapi.utils.buildItem()` found
|
||||
within the specified path.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
path (str): Relative path to request XML data from.
|
||||
libtype (str): Optionally return only the specified library type.
|
||||
watched (bool): Optionally return only watched or unwatched items.
|
||||
bytag (bool): Set true if libtype is found in the XML tag (and not the 'type' attribute).
|
||||
"""
|
||||
items = []
|
||||
for elem in server.query(path):
|
||||
if libtype and elem.attrib.get('type') != libtype:
|
||||
continue
|
||||
if watched is True and int(elem.attrib.get('viewCount', 0)) == 0:
|
||||
continue
|
||||
if watched is False and int(elem.attrib.get('viewCount', 0)) >= 1:
|
||||
continue
|
||||
try:
|
||||
items.append(buildItem(server, elem, path, bytag))
|
||||
except UnknownType:
|
||||
pass
|
||||
return items
|
||||
|
||||
|
||||
def rget(obj, attrstr, default=None, delim='.'):
|
||||
""" Returns the value at the specified attrstr location within a nexted tree of
|
||||
dicts, lists, tuples, functions, classes, etc. The lookup is done recursivley
|
||||
for each key in attrstr (split by by the delimiter) This function is heavily
|
||||
influenced by the lookups used in Django templates.
|
||||
|
||||
Parameters:
|
||||
obj (any): Object to start the lookup in (dict, obj, list, tuple, etc).
|
||||
attrstr (str): String to lookup (ex: 'foo.bar.baz.value')
|
||||
default (any): Default value to return if not found.
|
||||
delim (str): Delimiter separating keys in attrstr.
|
||||
"""
|
||||
try:
|
||||
parts = attrstr.split(delim, 1)
|
||||
attr = parts[0]
|
||||
attrstr = parts[1] if len(parts) == 2 else None
|
||||
if isinstance(obj, dict):
|
||||
value = obj[attr]
|
||||
elif isinstance(obj, list):
|
||||
value = obj[int(attr)]
|
||||
elif isinstance(obj, tuple):
|
||||
value = obj[int(attr)]
|
||||
elif isinstance(obj, object):
|
||||
value = getattr(obj, attr)
|
||||
if attrstr:
|
||||
return rget(value, attrstr, default, delim)
|
||||
return value
|
||||
except:
|
||||
return default
|
||||
|
||||
|
||||
def searchType(libtype):
|
||||
""" Returns the integer value of the library string type.
|
||||
|
||||
Parameters:
|
||||
libtype (str): Library type to lookup (movie, show, season, episode,
|
||||
artist, album, track)
|
||||
|
||||
Raises:
|
||||
NotFound: Unknown libtype
|
||||
"""
|
||||
libtype = str(libtype)
|
||||
if libtype in [str(v) for v in SEARCHTYPES.values()]:
|
||||
return libtype
|
||||
if SEARCHTYPES.get(libtype) is not None:
|
||||
return SEARCHTYPES[libtype]
|
||||
raise NotFound('Unknown libtype: %s' % libtype)
|
||||
|
||||
|
||||
def threaded(callback, listargs):
|
||||
""" Returns the result of <callback> for each set of \*args in listargs. Each call
|
||||
to <callback. is called concurrently in their own separate threads.
|
||||
|
||||
Parameters:
|
||||
callback (func): Callback function to apply to each set of \*args.
|
||||
listargs (list): List of lists; \*args to pass each thread.
|
||||
"""
|
||||
threads, results = [], []
|
||||
for args in listargs:
|
||||
args += [results, len(results)]
|
||||
results.append(None)
|
||||
threads.append(Thread(target=callback, args=args))
|
||||
threads[-1].start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def toDatetime(value, format=None):
|
||||
""" Returns a datetime object from the specified value.
|
||||
|
||||
Parameters:
|
||||
value (str): value to return as a datetime
|
||||
format (str): Format to pass strftime (optional; if value is a str).
|
||||
"""
|
||||
if value and value != NA:
|
||||
if format:
|
||||
value = datetime.strptime(value, format)
|
||||
else:
|
||||
value = datetime.fromtimestamp(int(value))
|
||||
return value
|
||||
|
||||
|
||||
def download(url, filename=None, savepath=None, session=None, chunksize=4024, mocked=False):
|
||||
"""Helper to download a thumb, videofile or something.
|
||||
|
||||
Args:
|
||||
url (str): url where the content be reached
|
||||
filename (str): Filename of the downloaded file, default None
|
||||
savepath (str): Defaults to current working dir
|
||||
chunksize (int): What chunksize read/write at the time
|
||||
mocked (bool): Helper to do evertything except write the file.
|
||||
|
||||
Example:
|
||||
|
||||
>>> download(a_episode.getStreamURL(), a_episode.location)
|
||||
/path/to/file
|
||||
|
||||
Returns:
|
||||
/path/to/file or None
|
||||
|
||||
"""
|
||||
session = session or requests.Session()
|
||||
|
||||
if savepath is None:
|
||||
savepath = os.getcwd()
|
||||
else:
|
||||
# Make sure the user supplied path exists
|
||||
try:
|
||||
os.makedirs(savepath)
|
||||
except OSError:
|
||||
if not os.path.isdir(savepath):
|
||||
raise
|
||||
|
||||
filename = os.path.basename(filename)
|
||||
fullpath = os.path.join(savepath, filename)
|
||||
|
||||
try:
|
||||
response = session.get(url, stream=True)
|
||||
|
||||
# images dont have a extention so we try
|
||||
# to guess it from content-type
|
||||
ext = os.path.splitext(fullpath)[-1]
|
||||
if ext:
|
||||
ext = ''
|
||||
else:
|
||||
cp = response.headers.get('content-type')
|
||||
|
||||
if cp:
|
||||
if 'image' in cp:
|
||||
ext = '.%s' % cp.split('/')[1]
|
||||
|
||||
fullpath = '%s%s' % (fullpath, ext)
|
||||
|
||||
if mocked:
|
||||
return fullpath
|
||||
|
||||
with open(fullpath, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=chunksize):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
|
||||
log.debug('Downloaded %s to %s from %s' % (filename, fullpath, url))
|
||||
|
||||
return fullpath
|
||||
|
||||
except Exception as e:
|
||||
log.exception('Failed to download %s to %s %s' % (url, fullpath, e))
|
Loading…
Reference in a new issue