mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-10 14:14:19 +00:00
Merge branch 'master' into unmatch_match
This commit is contained in:
commit
bef40a74f5
12 changed files with 426 additions and 28 deletions
|
@ -10,8 +10,6 @@ services:
|
|||
- docker
|
||||
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.6
|
||||
|
||||
env:
|
||||
|
|
|
@ -132,6 +132,8 @@ class PlexObject(object):
|
|||
* __regex: Value matches the specified regular expression.
|
||||
* __startswith: Value starts with specified arg.
|
||||
"""
|
||||
if ekey is None:
|
||||
raise BadRequest('ekey was not provided')
|
||||
if isinstance(ekey, int):
|
||||
ekey = '/library/metadata/%s' % ekey
|
||||
for elem in self._server.query(ekey):
|
||||
|
@ -145,6 +147,8 @@ class PlexObject(object):
|
|||
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
|
||||
on how this is used.
|
||||
"""
|
||||
if ekey is None:
|
||||
raise BadRequest('ekey was not provided')
|
||||
data = self._server.query(ekey)
|
||||
items = self.findItems(data, cls, ekey, **kwargs)
|
||||
librarySectionID = data.attrib.get('librarySectionID')
|
||||
|
|
|
@ -7,7 +7,7 @@ from plexapi import BASE_HEADERS, CONFIG, TIMEOUT
|
|||
from plexapi import log, logfilter, utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.compat import ElementTree
|
||||
from plexapi.exceptions import BadRequest, Unsupported
|
||||
from plexapi.exceptions import BadRequest, Unauthorized, Unsupported
|
||||
from plexapi.playqueue import PlayQueue
|
||||
|
||||
|
||||
|
@ -162,8 +162,11 @@ class PlexClient(PlexObject):
|
|||
if response.status_code not in (200, 201):
|
||||
codename = codes.get(response.status_code)[0]
|
||||
errtext = response.text.replace('\n', ' ')
|
||||
log.warning('BadRequest (%s) %s %s; %s' % (response.status_code, codename, response.url, errtext))
|
||||
raise BadRequest('(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext))
|
||||
message = '(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext)
|
||||
if response.status_code == 401:
|
||||
raise Unauthorized(message)
|
||||
else:
|
||||
raise BadRequest(message)
|
||||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
|
||||
|
@ -204,10 +207,13 @@ class PlexClient(PlexObject):
|
|||
return query(key, headers=headers)
|
||||
except ElementTree.ParseError:
|
||||
# Workaround for players which don't return valid XML on successful commands
|
||||
# - Plexamp: `b'OK'`
|
||||
# - Plexamp, Plex for Android: `b'OK'`
|
||||
# - Plex for Samsung: `b'<?xml version="1.0"?><Response code="200" status="OK">'`
|
||||
if self.product in (
|
||||
'Plexamp',
|
||||
'Plex for Android (TV)',
|
||||
'Plex for Android (Mobile)',
|
||||
'Plex for Samsung',
|
||||
):
|
||||
return
|
||||
raise
|
||||
|
@ -469,10 +475,15 @@ class PlexClient(PlexObject):
|
|||
|
||||
if hasattr(media, "playlistType"):
|
||||
mediatype = media.playlistType
|
||||
elif media.listType == "audio":
|
||||
mediatype = "music"
|
||||
else:
|
||||
mediatype = "video"
|
||||
if isinstance(media, PlayQueue):
|
||||
mediatype = media.items[0].listType
|
||||
else:
|
||||
mediatype = media.listType
|
||||
|
||||
# mediatype must be in ["video", "music", "photo"]
|
||||
if mediatype == "audio":
|
||||
mediatype = "music"
|
||||
|
||||
if self.product != 'OpenPHT':
|
||||
try:
|
||||
|
|
|
@ -25,9 +25,9 @@ except ImportError:
|
|||
from urllib import quote
|
||||
|
||||
try:
|
||||
from urllib.parse import quote_plus
|
||||
from urllib.parse import quote_plus, quote
|
||||
except ImportError:
|
||||
from urllib import quote_plus
|
||||
from urllib import quote_plus, quote
|
||||
|
||||
try:
|
||||
from urllib.parse import unquote
|
||||
|
|
|
@ -26,6 +26,6 @@ class Unsupported(PlexApiException):
|
|||
pass
|
||||
|
||||
|
||||
class Unauthorized(PlexApiException):
|
||||
""" Invalid username or password. """
|
||||
class Unauthorized(BadRequest):
|
||||
""" Invalid username/password or token. """
|
||||
pass
|
||||
|
|
147
plexapi/gdm.py
Normal file
147
plexapi/gdm.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
"""
|
||||
Support for discovery using GDM (Good Day Mate), multicast protocol by Plex.
|
||||
|
||||
# Licensed Apache 2.0
|
||||
# From https://github.com/home-assistant/netdisco/netdisco/gdm.py
|
||||
|
||||
Inspired by
|
||||
hippojay's plexGDM:
|
||||
https://github.com/hippojay/script.plexbmc.helper/resources/lib/plexgdm.py
|
||||
iBaa's PlexConnect: https://github.com/iBaa/PlexConnect/PlexAPI.py
|
||||
"""
|
||||
import socket
|
||||
import struct
|
||||
|
||||
|
||||
class GDM:
|
||||
"""Base class to discover GDM services."""
|
||||
|
||||
def __init__(self):
|
||||
self.entries = []
|
||||
self.last_scan = None
|
||||
|
||||
def scan(self, scan_for_clients=False):
|
||||
"""Scan the network."""
|
||||
self.update(scan_for_clients)
|
||||
|
||||
def all(self):
|
||||
"""Return all found entries.
|
||||
|
||||
Will scan for entries if not scanned recently.
|
||||
"""
|
||||
self.scan()
|
||||
return list(self.entries)
|
||||
|
||||
def find_by_content_type(self, value):
|
||||
"""Return a list of entries that match the content_type."""
|
||||
self.scan()
|
||||
return [entry for entry in self.entries
|
||||
if value in entry['data']['Content_Type']]
|
||||
|
||||
def find_by_data(self, values):
|
||||
"""Return a list of entries that match the search parameters."""
|
||||
self.scan()
|
||||
return [entry for entry in self.entries
|
||||
if all(item in entry['data'].items()
|
||||
for item in values.items())]
|
||||
|
||||
def update(self, scan_for_clients):
|
||||
"""Scan for new GDM services.
|
||||
|
||||
Examples of the dict list assigned to self.entries by this function:
|
||||
|
||||
Server:
|
||||
[{'data': {
|
||||
'Content-Type': 'plex/media-server',
|
||||
'Host': '53f4b5b6023d41182fe88a99b0e714ba.plex.direct',
|
||||
'Name': 'myfirstplexserver',
|
||||
'Port': '32400',
|
||||
'Resource-Identifier': '646ab0aa8a01c543e94ba975f6fd6efadc36b7',
|
||||
'Updated-At': '1585769946',
|
||||
'Version': '1.18.8.2527-740d4c206',
|
||||
},
|
||||
'from': ('10.10.10.100', 32414)}]
|
||||
|
||||
Clients:
|
||||
[{'data': {'Content-Type': 'plex/media-player',
|
||||
'Device-Class': 'stb',
|
||||
'Name': 'plexamp',
|
||||
'Port': '36000',
|
||||
'Product': 'Plexamp',
|
||||
'Protocol': 'plex',
|
||||
'Protocol-Capabilities': 'timeline,playback,playqueues,playqueues-creation',
|
||||
'Protocol-Version': '1',
|
||||
'Resource-Identifier': 'b6e57a3f-e0f8-494f-8884-f4b58501467e',
|
||||
'Version': '1.1.0',
|
||||
},
|
||||
'from': ('10.10.10.101', 32412)}]
|
||||
"""
|
||||
|
||||
gdm_msg = 'M-SEARCH * HTTP/1.0'.encode('ascii')
|
||||
gdm_timeout = 1
|
||||
|
||||
self.entries = []
|
||||
known_responses = []
|
||||
|
||||
# setup socket for discovery -> multicast message
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(gdm_timeout)
|
||||
|
||||
# Set the time-to-live for messages for local network
|
||||
sock.setsockopt(socket.IPPROTO_IP,
|
||||
socket.IP_MULTICAST_TTL,
|
||||
struct.pack("B", gdm_timeout))
|
||||
|
||||
if scan_for_clients:
|
||||
# setup socket for broadcast to Plex clients
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
gdm_ip = '255.255.255.255'
|
||||
gdm_port = 32412
|
||||
else:
|
||||
# setup socket for multicast to Plex server(s)
|
||||
gdm_ip = '239.0.0.250'
|
||||
gdm_port = 32414
|
||||
|
||||
try:
|
||||
# Send data to the multicast group
|
||||
sock.sendto(gdm_msg, (gdm_ip, gdm_port))
|
||||
|
||||
# Look for responses from all recipients
|
||||
while True:
|
||||
try:
|
||||
bdata, host = sock.recvfrom(1024)
|
||||
data = bdata.decode('utf-8')
|
||||
if '200 OK' in data.splitlines()[0]:
|
||||
ddata = {k: v.strip() for (k, v) in (
|
||||
line.split(':') for line in
|
||||
data.splitlines() if ':' in line)}
|
||||
identifier = ddata.get('Resource-Identifier')
|
||||
if identifier and identifier in known_responses:
|
||||
continue
|
||||
known_responses.append(identifier)
|
||||
self.entries.append({'data': ddata,
|
||||
'from': host})
|
||||
except socket.timeout:
|
||||
break
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def main():
|
||||
"""Test GDM discovery."""
|
||||
from pprint import pprint
|
||||
|
||||
gdm = GDM()
|
||||
|
||||
pprint("Scanning GDM for servers...")
|
||||
gdm.scan()
|
||||
pprint(gdm.entries)
|
||||
|
||||
pprint("Scanning GDM for clients...")
|
||||
gdm.scan(scan_for_clients=True)
|
||||
pprint(gdm.entries)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -349,9 +349,30 @@ class TranscodeSession(PlexObject):
|
|||
self.width = cast(int, data.attrib.get('width'))
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class TranscodeJob(PlexObject):
|
||||
""" Represents an Optimizing job.
|
||||
TrancodeJobs are the process for optimizing conversions.
|
||||
Active or paused optimization items. Usually one item as a time"""
|
||||
TAG = 'TranscodeJob'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.generatorID = data.attrib.get('generatorID')
|
||||
self.key = data.attrib.get('key')
|
||||
self.progress = data.attrib.get('progress')
|
||||
self.ratingKey = data.attrib.get('ratingKey')
|
||||
self.size = data.attrib.get('size')
|
||||
self.targetTagID = data.attrib.get('targetTagID')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Optimized(PlexObject):
|
||||
""" Represents a Optimized item. """
|
||||
""" Represents a Optimized item.
|
||||
Optimized items are optimized and queued conversions items."""
|
||||
TAG = 'Item'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -363,10 +384,26 @@ class Optimized(PlexObject):
|
|||
self.target = data.attrib.get('target')
|
||||
self.targetTagID = data.attrib.get('targetTagID')
|
||||
|
||||
def remove(self):
|
||||
""" Remove an Optimized item"""
|
||||
key = '%s/%s' % (self._initpath, self.id)
|
||||
self._server.query(key, method=self._server._session.delete)
|
||||
|
||||
def rename(self, title):
|
||||
""" Rename an Optimized item"""
|
||||
key = '%s/%s?Item[title]=%s' % (self._initpath, self.id, title)
|
||||
self._server.query(key, method=self._server._session.put)
|
||||
|
||||
def reprocess(self, ratingKey):
|
||||
""" Reprocess a removed Conversion item that is still a listed Optimize item"""
|
||||
key = '%s/%s/%s/enable' % (self._initpath, self.id, ratingKey)
|
||||
self._server.query(key, method=self._server._session.put)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Conversion(PlexObject):
|
||||
""" Represents a Conversion item. """
|
||||
""" Represents a Conversion item.
|
||||
Conversions are items queued for optimization or being actively optimized."""
|
||||
TAG = 'Video'
|
||||
|
||||
def _loadData(self, data):
|
||||
|
@ -403,6 +440,29 @@ class Conversion(PlexObject):
|
|||
self.viewOffset = data.attrib.get('viewOffset')
|
||||
self.year = data.attrib.get('year')
|
||||
|
||||
def remove(self):
|
||||
""" Remove Conversion from queue """
|
||||
key = '/playlists/%s/items/%s/%s/disable' % (self.playlistID, self.generatorID, self.ratingKey)
|
||||
self._server.query(key, method=self._server._session.put)
|
||||
|
||||
def move(self, after):
|
||||
""" Move Conversion items position in queue
|
||||
after (int): Positional integer to move item
|
||||
-1 Active conversion
|
||||
OR
|
||||
Use another conversion items playQueueItemID to move in front of
|
||||
|
||||
Example:
|
||||
Move 5th conversion Item to active conversion
|
||||
conversions[4].move('-1')
|
||||
|
||||
Move 4th conversion Item to 2nd in conversion queue
|
||||
conversions[3].move(conversions[1].playQueueItemID)
|
||||
"""
|
||||
|
||||
key = '%s/items/%s/move?after=%s' % (self._initpath, self.playQueueItemID, after)
|
||||
self._server.query(key, method=self._server._session.put)
|
||||
|
||||
|
||||
class MediaTag(PlexObject):
|
||||
""" Base class for media tags used for filtering and searching your library
|
||||
|
|
|
@ -6,7 +6,7 @@ from requests.status_codes import _codes as codes
|
|||
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_IDENTIFIER, X_PLEX_ENABLE_FAST_CONNECT
|
||||
from plexapi import log, logfilter, utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.compat import ElementTree
|
||||
from plexapi.library import LibrarySection
|
||||
|
@ -175,7 +175,11 @@ class MyPlexAccount(PlexObject):
|
|||
if response.status_code not in (200, 201, 204): # pragma: no cover
|
||||
codename = codes.get(response.status_code)[0]
|
||||
errtext = response.text.replace('\n', ' ')
|
||||
raise BadRequest('(%s) %s %s; %s' % (response.status_code, codename, response.url, errtext))
|
||||
message = '(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext)
|
||||
if response.status_code == 401:
|
||||
raise Unauthorized(message)
|
||||
else:
|
||||
raise BadRequest(message)
|
||||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from plexapi.alert import AlertListener
|
|||
from plexapi.base import PlexObject
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.compat import ElementTree, urlencode
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
from plexapi.library import Library, Hub
|
||||
from plexapi.settings import Settings
|
||||
from plexapi.playlist import Playlist
|
||||
|
@ -380,16 +380,35 @@ class PlexServer(PlexObject):
|
|||
"""
|
||||
return self.fetchItem('/playlists', title=title)
|
||||
|
||||
def optimizedItems(self):
|
||||
def optimizedItems(self, removeAll=None):
|
||||
""" Returns list of all :class:`~plexapi.media.Optimized` objects connected to server. """
|
||||
if removeAll is True:
|
||||
key = '/playlists/generators?type=42'
|
||||
self.query(key, method=self._server._session.delete)
|
||||
else:
|
||||
backgroundProcessing = self.fetchItem('/playlists?type=42')
|
||||
return self.fetchItems('%s/items' % backgroundProcessing.key, cls=Optimized)
|
||||
|
||||
def optimizedItem(self, optimizedID):
|
||||
""" Returns single queued optimized item :class:`~plexapi.media.Video` object.
|
||||
Allows for using optimized item ID to connect back to source item.
|
||||
"""
|
||||
|
||||
backgroundProcessing = self.fetchItem('/playlists?type=42')
|
||||
return self.fetchItems('%s/items' % backgroundProcessing.key, cls=Optimized)
|
||||
return self.fetchItem('%s/items/%s/items' % (backgroundProcessing.key, optimizedID))
|
||||
|
||||
def conversions(self):
|
||||
def conversions(self, pause=None):
|
||||
""" Returns list of all :class:`~plexapi.media.Conversion` objects connected to server. """
|
||||
if pause is True:
|
||||
self.query('/:/prefs?BackgroundQueueIdlePaused=1', method=self._server._session.put)
|
||||
elif pause is False:
|
||||
self.query('/:/prefs?BackgroundQueueIdlePaused=0', method=self._server._session.put)
|
||||
else:
|
||||
return self.fetchItems('/playQueues/1', cls=Conversion)
|
||||
|
||||
return self.fetchItems('/playQueues/1', cls=Conversion)
|
||||
def currentBackgroundProcess(self):
|
||||
""" Returns list of all :class:`~plexapi.media.TranscodeJob` objects running or paused on server. """
|
||||
return self.fetchItems('/status/sessions/background')
|
||||
|
||||
def query(self, key, method=None, headers=None, timeout=None, **kwargs):
|
||||
""" Main method used to handle HTTPS requests to the Plex server. This method helps
|
||||
|
@ -405,8 +424,11 @@ class PlexServer(PlexObject):
|
|||
if response.status_code not in (200, 201):
|
||||
codename = codes.get(response.status_code)[0]
|
||||
errtext = response.text.replace('\n', ' ')
|
||||
log.warning('BadRequest (%s) %s %s; %s' % (response.status_code, codename, response.url, errtext))
|
||||
raise BadRequest('(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext))
|
||||
message = '(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext)
|
||||
if response.status_code == 401:
|
||||
raise Unauthorized(message)
|
||||
else:
|
||||
raise BadRequest(message)
|
||||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
|
||||
|
@ -500,6 +522,25 @@ class PlexServer(PlexObject):
|
|||
self.refreshSynclist()
|
||||
self.refreshContent()
|
||||
|
||||
def _allowMediaDeletion(self, toggle=False):
|
||||
""" Toggle allowMediaDeletion.
|
||||
Parameters:
|
||||
toggle (bool): True enables Media Deletion
|
||||
False or None disable Media Deletion (Default)
|
||||
"""
|
||||
if self.allowMediaDeletion and toggle is False:
|
||||
log.debug('Plex is currently allowed to delete media. Toggling off.')
|
||||
elif self.allowMediaDeletion and toggle is True:
|
||||
log.debug('Plex is currently allowed to delete media. Toggle set to allow, exiting.')
|
||||
raise BadRequest('Plex is currently allowed to delete media. Toggle set to allow, exiting.')
|
||||
elif self.allowMediaDeletion is None and toggle is True:
|
||||
log.debug('Plex is currently not allowed to delete media. Toggle set to allow.')
|
||||
else:
|
||||
log.debug('Plex is currently not allowed to delete media. Toggle set to not allow, exiting.')
|
||||
raise BadRequest('Plex is currently not allowed to delete media. Toggle set to not allow, exiting.')
|
||||
value = 1 if toggle is True else 0
|
||||
return self.query('/:/prefs?allowMediaDeletion=%s' % value, self._session.put)
|
||||
|
||||
|
||||
class Account(PlexObject):
|
||||
""" Contains the locally cached MyPlex account information. The properties provided don't
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from plexapi import media, utils
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.compat import quote_plus
|
||||
from plexapi.compat import quote_plus, urlencode
|
||||
import os
|
||||
|
||||
|
||||
|
@ -126,6 +126,82 @@ class Video(PlexPartialObject):
|
|||
|
||||
return self.fetchItems('%s/posters' % self.key, cls=media.Poster)
|
||||
|
||||
def optimize(self, title=None, target="", targetTagID=None, locationID=-1, policyScope='all',
|
||||
policyValue="", policyUnwatched=0, videoQuality=None, deviceProfile=None):
|
||||
""" Optimize item
|
||||
|
||||
locationID (int): -1 in folder with orginal items
|
||||
2 library path
|
||||
|
||||
target (str): custom quality name.
|
||||
if none provided use "Custom: {deviceProfile}"
|
||||
|
||||
targetTagID (int): Default quality settings
|
||||
1 Mobile
|
||||
2 TV
|
||||
3 Original Quality
|
||||
|
||||
deviceProfile (str): Android, IOS, Universal TV, Universal Mobile, Windows Phone,
|
||||
Windows, Xbox One
|
||||
|
||||
Example:
|
||||
Optimize for Mobile
|
||||
item.optimize(targetTagID="Mobile") or item.optimize(targetTagID=1")
|
||||
Optimize for Android 10 MBPS 1080p
|
||||
item.optimize(deviceProfile="Android", videoQuality=10)
|
||||
Optimize for IOS Original Quality
|
||||
item.optimize(deviceProfile="IOS", videoQuality=-1)
|
||||
|
||||
* see sync.py VIDEO_QUALITIES for additional information for using videoQuality
|
||||
"""
|
||||
tagValues = [1, 2, 3]
|
||||
tagKeys = ["Mobile", "TV", "Original Quality"]
|
||||
tagIDs = tagKeys + tagValues
|
||||
|
||||
if targetTagID not in tagIDs and (deviceProfile is None or videoQuality is None):
|
||||
raise BadRequest('Unexpected or missing quality profile.')
|
||||
|
||||
if isinstance(targetTagID, str):
|
||||
tagIndex = tagKeys.index(targetTagID)
|
||||
targetTagID = tagValues[tagIndex]
|
||||
|
||||
if title is None:
|
||||
title = self.title
|
||||
|
||||
backgroundProcessing = self.fetchItem('/playlists?type=42')
|
||||
key = '%s/items?' % backgroundProcessing.key
|
||||
params = {
|
||||
'Item[type]': 42,
|
||||
'Item[target]': target,
|
||||
'Item[targetTagID]': targetTagID if targetTagID else '',
|
||||
'Item[locationID]': locationID,
|
||||
'Item[Policy][scope]': policyScope,
|
||||
'Item[Policy][value]': policyValue,
|
||||
'Item[Policy][unwatched]': policyUnwatched
|
||||
}
|
||||
|
||||
if deviceProfile:
|
||||
params['Item[Device][profile]'] = deviceProfile
|
||||
|
||||
if videoQuality:
|
||||
from plexapi.sync import MediaSettings
|
||||
mediaSettings = MediaSettings.createVideo(videoQuality)
|
||||
params['Item[MediaSettings][videoQuality]'] = mediaSettings.videoQuality
|
||||
params['Item[MediaSettings][videoResolution]'] = mediaSettings.videoResolution
|
||||
params['Item[MediaSettings][maxVideoBitrate]'] = mediaSettings.maxVideoBitrate
|
||||
params['Item[MediaSettings][audioBoost]'] = ''
|
||||
params['Item[MediaSettings][subtitleSize]'] = ''
|
||||
params['Item[MediaSettings][musicBitrate]'] = ''
|
||||
params['Item[MediaSettings][photoQuality]'] = ''
|
||||
|
||||
titleParam = {'Item[title]': title}
|
||||
section = self._server.library.sectionByID(self.librarySectionID)
|
||||
params['Item[Location][uri]'] = 'library://' + section.uuid + '/item/' + \
|
||||
quote_plus(self.key + '?includeExternalMedia=1')
|
||||
|
||||
data = key + urlencode(params) + '&' + urlencode(titleParam)
|
||||
return self._server.query(data, method=self._server._session.put)
|
||||
|
||||
def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=False, title=None):
|
||||
""" Add current video (movie, tv-show, season or episode) as sync item for specified device.
|
||||
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
||||
|
@ -505,7 +581,7 @@ class Season(Video):
|
|||
|
||||
def show(self):
|
||||
""" Return this seasons :func:`~plexapi.video.Show`.. """
|
||||
return self.fetchItem(self.parentKey)
|
||||
return self.fetchItem(int(self.parentRatingKey))
|
||||
|
||||
def watched(self):
|
||||
""" Returns list of watched :class:`~plexapi.video.Episode` objects. """
|
||||
|
@ -646,7 +722,7 @@ class Episode(Playable, Video):
|
|||
|
||||
def show(self):
|
||||
"""" Return this episodes :func:`~plexapi.video.Show`.. """
|
||||
return self.fetchItem(self.grandparentKey)
|
||||
return self.fetchItem(int(self.grandparentRatingKey))
|
||||
|
||||
def _defaultSyncTitle(self):
|
||||
""" Returns str, default title for a new syncItem. """
|
||||
|
|
|
@ -269,3 +269,41 @@ def test_server_downloadLogs(tmpdir, plex):
|
|||
def test_server_downloadDatabases(tmpdir, plex):
|
||||
plex.downloadDatabases(savepath=str(tmpdir), unpack=True)
|
||||
assert len(tmpdir.listdir()) > 1
|
||||
|
||||
def test_server_allowMediaDeletion(account):
|
||||
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
|
||||
# Check server current allowMediaDeletion setting
|
||||
if plex.allowMediaDeletion:
|
||||
# If allowed then test disallowed
|
||||
plex._allowMediaDeletion(False)
|
||||
time.sleep(1)
|
||||
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
|
||||
assert plex.allowMediaDeletion is None
|
||||
# Test redundant toggle
|
||||
with pytest.raises(BadRequest):
|
||||
plex._allowMediaDeletion(False)
|
||||
|
||||
plex._allowMediaDeletion(True)
|
||||
time.sleep(1)
|
||||
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
|
||||
assert plex.allowMediaDeletion is True
|
||||
# Test redundant toggle
|
||||
with pytest.raises(BadRequest):
|
||||
plex._allowMediaDeletion(True)
|
||||
else:
|
||||
# If disallowed then test allowed
|
||||
plex._allowMediaDeletion(True)
|
||||
time.sleep(1)
|
||||
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
|
||||
assert plex.allowMediaDeletion is True
|
||||
# Test redundant toggle
|
||||
with pytest.raises(BadRequest):
|
||||
plex._allowMediaDeletion(True)
|
||||
|
||||
plex._allowMediaDeletion(False)
|
||||
time.sleep(1)
|
||||
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
|
||||
assert plex.allowMediaDeletion is None
|
||||
# Test redundant toggle
|
||||
with pytest.raises(BadRequest):
|
||||
plex._allowMediaDeletion(False)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from . import conftest as utils
|
||||
|
||||
|
@ -685,4 +686,22 @@ def test_video_exists_accessible(movie, episode):
|
|||
episode.reload()
|
||||
assert episode.media[0].parts[0].exists is True
|
||||
assert episode.media[0].parts[0].accessible is True
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason='broken? assert len(plex.conversions()) == 1 may fail on some builds')
|
||||
def test_video_optimize(movie, plex):
|
||||
plex.optimizedItems(removeAll=True)
|
||||
movie.optimize(targetTagID=1)
|
||||
plex.conversions(pause=True)
|
||||
sleep(1)
|
||||
assert len(plex.optimizedItems()) == 1
|
||||
assert len(plex.conversions()) == 1
|
||||
conversion = plex.conversions()[0]
|
||||
conversion.remove()
|
||||
assert len(plex.conversions()) == 0
|
||||
assert len(plex.optimizedItems()) == 1
|
||||
optimized = plex.optimizedItems()[0]
|
||||
video = plex.optimizedItem(optimizedID=optimized.id)
|
||||
assert movie.key == video.key
|
||||
plex.optimizedItems(removeAll=True)
|
||||
assert len(plex.optimizedItems()) == 0
|
Loading…
Reference in a new issue