mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-22 11:43:13 +00:00
Merge pull request #426 from pkkid/conversion_actions
conversion_actions
This commit is contained in:
commit
0a77c74466
5 changed files with 184 additions and 10 deletions
|
@ -25,9 +25,9 @@ except ImportError:
|
||||||
from urllib import quote
|
from urllib import quote
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus, quote
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib import quote_plus
|
from urllib import quote_plus, quote
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
|
@ -349,9 +349,30 @@ class TranscodeSession(PlexObject):
|
||||||
self.width = cast(int, data.attrib.get('width'))
|
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
|
@utils.registerPlexObject
|
||||||
class Optimized(PlexObject):
|
class Optimized(PlexObject):
|
||||||
""" Represents a Optimized item. """
|
""" Represents a Optimized item.
|
||||||
|
Optimized items are optimized and queued conversions items."""
|
||||||
TAG = 'Item'
|
TAG = 'Item'
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
|
@ -363,10 +384,26 @@ class Optimized(PlexObject):
|
||||||
self.target = data.attrib.get('target')
|
self.target = data.attrib.get('target')
|
||||||
self.targetTagID = data.attrib.get('targetTagID')
|
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
|
@utils.registerPlexObject
|
||||||
class Conversion(PlexObject):
|
class Conversion(PlexObject):
|
||||||
""" Represents a Conversion item. """
|
""" Represents a Conversion item.
|
||||||
|
Conversions are items queued for optimization or being actively optimized."""
|
||||||
TAG = 'Video'
|
TAG = 'Video'
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
|
@ -403,6 +440,29 @@ class Conversion(PlexObject):
|
||||||
self.viewOffset = data.attrib.get('viewOffset')
|
self.viewOffset = data.attrib.get('viewOffset')
|
||||||
self.year = data.attrib.get('year')
|
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):
|
class MediaTag(PlexObject):
|
||||||
""" Base class for media tags used for filtering and searching your library
|
""" Base class for media tags used for filtering and searching your library
|
||||||
|
|
|
@ -373,16 +373,35 @@ class PlexServer(PlexObject):
|
||||||
"""
|
"""
|
||||||
return self.fetchItem('/playlists', title=title)
|
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. """
|
""" 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')
|
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. """
|
""" 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):
|
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
|
""" Main method used to handle HTTPS requests to the Plex server. This method helps
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from plexapi import media, utils
|
from plexapi import media, utils
|
||||||
from plexapi.exceptions import BadRequest, NotFound
|
from plexapi.exceptions import BadRequest, NotFound
|
||||||
from plexapi.base import Playable, PlexPartialObject
|
from plexapi.base import Playable, PlexPartialObject
|
||||||
from plexapi.compat import quote_plus
|
from plexapi.compat import quote_plus, urlencode
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
@ -126,6 +126,82 @@ class Video(PlexPartialObject):
|
||||||
|
|
||||||
return self.fetchItems('%s/posters' % self.key, cls=media.Poster)
|
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):
|
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.
|
""" Add current video (movie, tv-show, season or episode) as sync item for specified device.
|
||||||
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import pytest
|
import pytest
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from time import sleep
|
||||||
from plexapi.exceptions import BadRequest, NotFound
|
from plexapi.exceptions import BadRequest, NotFound
|
||||||
from . import conftest as utils
|
from . import conftest as utils
|
||||||
|
|
||||||
|
@ -685,4 +686,22 @@ def test_video_exists_accessible(movie, episode):
|
||||||
episode.reload()
|
episode.reload()
|
||||||
assert episode.media[0].parts[0].exists is True
|
assert episode.media[0].parts[0].exists is True
|
||||||
assert episode.media[0].parts[0].accessible 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