Merge pull request #426 from pkkid/conversion_actions

conversion_actions
This commit is contained in:
blacktwin 2020-04-12 22:34:51 -04:00 committed by GitHub
commit 0a77c74466
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 184 additions and 10 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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