Skip to content

Commit 0a77c74

Browse files
authored
Merge pull request #426 from pkkid/conversion_actions
conversion_actions
2 parents e9ecb59 + 839a9da commit 0a77c74

File tree

5 files changed

+184
-10
lines changed

5 files changed

+184
-10
lines changed

plexapi/compat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
from urllib import quote
2626

2727
try:
28-
from urllib.parse import quote_plus
28+
from urllib.parse import quote_plus, quote
2929
except ImportError:
30-
from urllib import quote_plus
30+
from urllib import quote_plus, quote
3131

3232
try:
3333
from urllib.parse import unquote

plexapi/media.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,30 @@ def _loadData(self, data):
349349
self.width = cast(int, data.attrib.get('width'))
350350

351351

352+
@utils.registerPlexObject
353+
class TranscodeJob(PlexObject):
354+
""" Represents an Optimizing job.
355+
TrancodeJobs are the process for optimizing conversions.
356+
Active or paused optimization items. Usually one item as a time"""
357+
TAG = 'TranscodeJob'
358+
359+
def _loadData(self, data):
360+
self._data = data
361+
self.generatorID = data.attrib.get('generatorID')
362+
self.key = data.attrib.get('key')
363+
self.progress = data.attrib.get('progress')
364+
self.ratingKey = data.attrib.get('ratingKey')
365+
self.size = data.attrib.get('size')
366+
self.targetTagID = data.attrib.get('targetTagID')
367+
self.thumb = data.attrib.get('thumb')
368+
self.title = data.attrib.get('title')
369+
self.type = data.attrib.get('type')
370+
371+
352372
@utils.registerPlexObject
353373
class Optimized(PlexObject):
354-
""" Represents a Optimized item. """
374+
""" Represents a Optimized item.
375+
Optimized items are optimized and queued conversions items."""
355376
TAG = 'Item'
356377

357378
def _loadData(self, data):
@@ -363,10 +384,26 @@ def _loadData(self, data):
363384
self.target = data.attrib.get('target')
364385
self.targetTagID = data.attrib.get('targetTagID')
365386

387+
def remove(self):
388+
""" Remove an Optimized item"""
389+
key = '%s/%s' % (self._initpath, self.id)
390+
self._server.query(key, method=self._server._session.delete)
391+
392+
def rename(self, title):
393+
""" Rename an Optimized item"""
394+
key = '%s/%s?Item[title]=%s' % (self._initpath, self.id, title)
395+
self._server.query(key, method=self._server._session.put)
396+
397+
def reprocess(self, ratingKey):
398+
""" Reprocess a removed Conversion item that is still a listed Optimize item"""
399+
key = '%s/%s/%s/enable' % (self._initpath, self.id, ratingKey)
400+
self._server.query(key, method=self._server._session.put)
401+
366402

367403
@utils.registerPlexObject
368404
class Conversion(PlexObject):
369-
""" Represents a Conversion item. """
405+
""" Represents a Conversion item.
406+
Conversions are items queued for optimization or being actively optimized."""
370407
TAG = 'Video'
371408

372409
def _loadData(self, data):
@@ -403,6 +440,29 @@ def _loadData(self, data):
403440
self.viewOffset = data.attrib.get('viewOffset')
404441
self.year = data.attrib.get('year')
405442

443+
def remove(self):
444+
""" Remove Conversion from queue """
445+
key = '/playlists/%s/items/%s/%s/disable' % (self.playlistID, self.generatorID, self.ratingKey)
446+
self._server.query(key, method=self._server._session.put)
447+
448+
def move(self, after):
449+
""" Move Conversion items position in queue
450+
after (int): Positional integer to move item
451+
-1 Active conversion
452+
OR
453+
Use another conversion items playQueueItemID to move in front of
454+
455+
Example:
456+
Move 5th conversion Item to active conversion
457+
conversions[4].move('-1')
458+
459+
Move 4th conversion Item to 2nd in conversion queue
460+
conversions[3].move(conversions[1].playQueueItemID)
461+
"""
462+
463+
key = '%s/items/%s/move?after=%s' % (self._initpath, self.playQueueItemID, after)
464+
self._server.query(key, method=self._server._session.put)
465+
406466

407467
class MediaTag(PlexObject):
408468
""" Base class for media tags used for filtering and searching your library

plexapi/server.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,16 +373,35 @@ def playlist(self, title):
373373
"""
374374
return self.fetchItem('/playlists', title=title)
375375

376-
def optimizedItems(self):
376+
def optimizedItems(self, removeAll=None):
377377
""" Returns list of all :class:`~plexapi.media.Optimized` objects connected to server. """
378+
if removeAll is True:
379+
key = '/playlists/generators?type=42'
380+
self.query(key, method=self._server._session.delete)
381+
else:
382+
backgroundProcessing = self.fetchItem('/playlists?type=42')
383+
return self.fetchItems('%s/items' % backgroundProcessing.key, cls=Optimized)
384+
385+
def optimizedItem(self, optimizedID):
386+
""" Returns single queued optimized item :class:`~plexapi.media.Video` object.
387+
Allows for using optimized item ID to connect back to source item.
388+
"""
378389

379390
backgroundProcessing = self.fetchItem('/playlists?type=42')
380-
return self.fetchItems('%s/items' % backgroundProcessing.key, cls=Optimized)
391+
return self.fetchItem('%s/items/%s/items' % (backgroundProcessing.key, optimizedID))
381392

382-
def conversions(self):
393+
def conversions(self, pause=None):
383394
""" Returns list of all :class:`~plexapi.media.Conversion` objects connected to server. """
395+
if pause is True:
396+
self.query('/:/prefs?BackgroundQueueIdlePaused=1', method=self._server._session.put)
397+
elif pause is False:
398+
self.query('/:/prefs?BackgroundQueueIdlePaused=0', method=self._server._session.put)
399+
else:
400+
return self.fetchItems('/playQueues/1', cls=Conversion)
384401

385-
return self.fetchItems('/playQueues/1', cls=Conversion)
402+
def currentBackgroundProcess(self):
403+
""" Returns list of all :class:`~plexapi.media.TranscodeJob` objects running or paused on server. """
404+
return self.fetchItems('/status/sessions/background')
386405

387406
def query(self, key, method=None, headers=None, timeout=None, **kwargs):
388407
""" Main method used to handle HTTPS requests to the Plex server. This method helps

plexapi/video.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from plexapi import media, utils
33
from plexapi.exceptions import BadRequest, NotFound
44
from plexapi.base import Playable, PlexPartialObject
5-
from plexapi.compat import quote_plus
5+
from plexapi.compat import quote_plus, urlencode
66
import os
77

88

@@ -126,6 +126,82 @@ def posters(self):
126126

127127
return self.fetchItems('%s/posters' % self.key, cls=media.Poster)
128128

129+
def optimize(self, title=None, target="", targetTagID=None, locationID=-1, policyScope='all',
130+
policyValue="", policyUnwatched=0, videoQuality=None, deviceProfile=None):
131+
""" Optimize item
132+
133+
locationID (int): -1 in folder with orginal items
134+
2 library path
135+
136+
target (str): custom quality name.
137+
if none provided use "Custom: {deviceProfile}"
138+
139+
targetTagID (int): Default quality settings
140+
1 Mobile
141+
2 TV
142+
3 Original Quality
143+
144+
deviceProfile (str): Android, IOS, Universal TV, Universal Mobile, Windows Phone,
145+
Windows, Xbox One
146+
147+
Example:
148+
Optimize for Mobile
149+
item.optimize(targetTagID="Mobile") or item.optimize(targetTagID=1")
150+
Optimize for Android 10 MBPS 1080p
151+
item.optimize(deviceProfile="Android", videoQuality=10)
152+
Optimize for IOS Original Quality
153+
item.optimize(deviceProfile="IOS", videoQuality=-1)
154+
155+
* see sync.py VIDEO_QUALITIES for additional information for using videoQuality
156+
"""
157+
tagValues = [1, 2, 3]
158+
tagKeys = ["Mobile", "TV", "Original Quality"]
159+
tagIDs = tagKeys + tagValues
160+
161+
if targetTagID not in tagIDs and (deviceProfile is None or videoQuality is None):
162+
raise BadRequest('Unexpected or missing quality profile.')
163+
164+
if isinstance(targetTagID, str):
165+
tagIndex = tagKeys.index(targetTagID)
166+
targetTagID = tagValues[tagIndex]
167+
168+
if title is None:
169+
title = self.title
170+
171+
backgroundProcessing = self.fetchItem('/playlists?type=42')
172+
key = '%s/items?' % backgroundProcessing.key
173+
params = {
174+
'Item[type]': 42,
175+
'Item[target]': target,
176+
'Item[targetTagID]': targetTagID if targetTagID else '',
177+
'Item[locationID]': locationID,
178+
'Item[Policy][scope]': policyScope,
179+
'Item[Policy][value]': policyValue,
180+
'Item[Policy][unwatched]': policyUnwatched
181+
}
182+
183+
if deviceProfile:
184+
params['Item[Device][profile]'] = deviceProfile
185+
186+
if videoQuality:
187+
from plexapi.sync import MediaSettings
188+
mediaSettings = MediaSettings.createVideo(videoQuality)
189+
params['Item[MediaSettings][videoQuality]'] = mediaSettings.videoQuality
190+
params['Item[MediaSettings][videoResolution]'] = mediaSettings.videoResolution
191+
params['Item[MediaSettings][maxVideoBitrate]'] = mediaSettings.maxVideoBitrate
192+
params['Item[MediaSettings][audioBoost]'] = ''
193+
params['Item[MediaSettings][subtitleSize]'] = ''
194+
params['Item[MediaSettings][musicBitrate]'] = ''
195+
params['Item[MediaSettings][photoQuality]'] = ''
196+
197+
titleParam = {'Item[title]': title}
198+
section = self._server.library.sectionByID(self.librarySectionID)
199+
params['Item[Location][uri]'] = 'library://' + section.uuid + '/item/' + \
200+
quote_plus(self.key + '?includeExternalMedia=1')
201+
202+
data = key + urlencode(params) + '&' + urlencode(titleParam)
203+
return self._server.query(data, method=self._server._session.put)
204+
129205
def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=False, title=None):
130206
""" Add current video (movie, tv-show, season or episode) as sync item for specified device.
131207
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.

tests/test_video.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
import pytest
33
from datetime import datetime
4+
from time import sleep
45
from plexapi.exceptions import BadRequest, NotFound
56
from . import conftest as utils
67

@@ -685,4 +686,22 @@ def test_video_exists_accessible(movie, episode):
685686
episode.reload()
686687
assert episode.media[0].parts[0].exists is True
687688
assert episode.media[0].parts[0].accessible is True
688-
689+
690+
691+
@pytest.mark.skip(reason='broken? assert len(plex.conversions()) == 1 may fail on some builds')
692+
def test_video_optimize(movie, plex):
693+
plex.optimizedItems(removeAll=True)
694+
movie.optimize(targetTagID=1)
695+
plex.conversions(pause=True)
696+
sleep(1)
697+
assert len(plex.optimizedItems()) == 1
698+
assert len(plex.conversions()) == 1
699+
conversion = plex.conversions()[0]
700+
conversion.remove()
701+
assert len(plex.conversions()) == 0
702+
assert len(plex.optimizedItems()) == 1
703+
optimized = plex.optimizedItems()[0]
704+
video = plex.optimizedItem(optimizedID=optimized.id)
705+
assert movie.key == video.key
706+
plex.optimizedItems(removeAll=True)
707+
assert len(plex.optimizedItems()) == 0

0 commit comments

Comments
 (0)