Skip to content

Instantly share code, notes, and snippets.

@blacktwin
Last active October 17, 2019 04:13
Show Gist options
  • Save blacktwin/2148bb0b2f8d67b8a08c50ace62ad39f to your computer and use it in GitHub Desktop.
Save blacktwin/2148bb0b2f8d67b8a08c50ace62ad39f to your computer and use it in GitHub Desktop.
Receive session_key from PlexPy when paused. Use session_id to create sub-script to wait for X time then check if transcoding still paused. If so, kill.
'''
kill_transcode function from https://gist.github.com/Hellowlol/ee47b6534410b1880e19
PlexPy > Settings > Notification Agents > Scripts > Bell icon:
[X] Notify on pause
PlexPy > Settings > Notification Agents > Scripts > Gear icon:
Playback Pause: create_wait_kill_trans.py
PlexPy > Settings > Notifications > Script > Script Arguments:
{session_key}
create_wait_kill_trans.py creates a new file with the session_id (sub_script) as it's name.
PlexPy will timeout create_wait_kill_trans.py after 30 seconds (default) but sub_script.py will continue.
sub_script will check if the transcoding and stream's session_id is still pause or if playing as restarted.
If playback is restarted then sub_script will stop and delete itself.
If stream remains paused then it will be killed and sub_script will stop and delete itself.
Set TIMEOUT to max time before killing stream
Set INTERVAL to how often you want to check the stream status
'''
import os
import platform
import subprocess
import sys
from uuid import getnode
import unicodedata
import requests
## EDIT THESE SETTINGS ##
PLEX_HOST = ''
PLEX_PORT = 32400
PLEX_SSL = '' # s or ''
PLEX_TOKEN = 'xxx'
TIMEOUT = 30
INTERVAL = 10
REASON = 'Because....'
ignore_lst = ('test')
def fetch(path, t='GET'):
url = 'http%s://%s:%s/' % (PLEX_SSL, PLEX_HOST, PLEX_PORT)
headers = {'X-Plex-Token': PLEX_TOKEN,
'Accept': 'application/json',
'X-Plex-Provides': 'controller',
'X-Plex-Platform': platform.uname()[0],
'X-Plex-Platform-Version': platform.uname()[2],
'X-Plex-Product': 'Plexpy script',
'X-Plex-Version': '0.9.5',
'X-Plex-Device': platform.platform(),
'X-Plex-Client-Identifier': str(hex(getnode()))
}
try:
if t == 'GET':
r = requests.get(url + path, headers=headers, verify=False)
elif t == 'POST':
r = requests.post(url + path, headers=headers, verify=False)
elif t == 'DELETE':
r = requests.delete(url + path, headers=headers, verify=False)
if r and len(r.content): # incase it dont return anything
return r.json()
else:
return r.content
except Exception as e:
print e
def kill_stream(sessionId, message, xtime, ntime, user, title, sessionKey):
headers = {'X-Plex-Token': PLEX_TOKEN}
params = {'sessionId': sessionId,
'reason': message}
response = fetch('status/sessions')
if response['MediaContainer']['Video']:
for a in response['MediaContainer']['Video']:
if a['sessionKey'] == sessionKey:
if xtime == ntime and a['Player']['state'] == 'paused' and a['Media']['Part']['decision'] == 'transcode':
sys.stdout.write("Killing {user}'s paused stream of {title}".format(user=user, title=title))
requests.get('http://{}:{}/status/sessions/terminate'.format(PLEX_HOST, PLEX_PORT),
headers=headers, params=params)
return ntime
elif a['Player']['state'] in ('playing', 'buffering'):
sys.stdout.write("{user}'s stream of {title} is now {state}".
format(user=user, title=title, state=a['Player']['state']))
return None
else:
return xtime
else:
return None
def find_sessionID(response):
sessions = []
for s in response['MediaContainer']['Video']:
if s['sessionKey'] == sys.argv[1] and s['Player']['state'] == 'paused' \
and s['Media']['Part']['decision'] == 'transcode':
sess_id = s['Session']['id']
user = s['User']['title']
sess_key = sys.argv[1]
title = (s['grandparentTitle'] + ' - ' if s['type'] == 'episode' else '') + s['title']
title = unicodedata.normalize('NFKD', title).encode('ascii','ignore')
sessions.append((sess_id, user, title, sess_key))
else:
pass
for session in sessions:
if session[1] not in ignore_lst:
return session
else:
print("{}'s stream of {} is ignored.".format(session[1], session[2]))
return None
if __name__ == '__main__':
startupinfo = None
if os.name == 'nt':
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
response = fetch('status/sessions')
try:
if find_sessionID(response):
stream_info = find_sessionID(response)
file_name = "{}.py".format(stream_info[0])
file = "from time import sleep\n" \
"import sys, os\n" \
"from {script} import kill_stream \n" \
"message = '{REASON}'\n" \
"sessionID = os.path.basename(sys.argv[0])[:-3]\n" \
"x = 0\n" \
"n = {ntime}\n" \
"try:\n" \
" while x < n and x is not None:\n" \
" sleep({xtime})\n" \
" x += kill_stream(sessionID, message, {xtime}, n, '{user}', '{title}', '{sess_key}')\n" \
" kill_stream(sessionID, message, {ntime}, n, '{user}', '{title}', '{sess_key}')\n" \
" os.remove(sys.argv[0])\n" \
"except TypeError as e:\n" \
" os.remove(sys.argv[0])".format(script=os.path.basename(__file__)[:-3],
ntime=TIMEOUT, xtime=INTERVAL, REASON=REASON,
user=stream_info[1], title=stream_info[2],
sess_key=stream_info[3])
with open(file_name, "w+") as output:
output.write(file)
subprocess.Popen([sys.executable, file_name], startupinfo=startupinfo)
exit(0)
except TypeError as e:
print(e)
pass
@blacktwin
Copy link
Author

blacktwin commented Jun 13, 2017

@csy7550 Make sure your settings are correct

PLEX_HOST = '127.0.0.1'
PLEX_PORT = 32400
PLEX_SSL = ''  # s or '' # 's' if you access Plex from https://127.0.0.1 and '' if http://127.0.0.1
PLEX_TOKEN = 'xxx'

@csy7550
Copy link

csy7550 commented Jun 14, 2017

@blacktwin Ah, stupid me.. Forgot I downgraded to PMS 1.5.5 due to another issue. Works fine when upgrading again, thanks!

@csy7550
Copy link

csy7550 commented Jun 14, 2017

@blacktwin Well, I spoke too soon. I found a bug.

Script only works when there is only one active stream on PMS.

Steps to reproduce:

  • Setup and activate script
  • Start a transcoded stream
  • Start another transcoded stream
  • Pause playback on one of the streams
  • Script won't create sub_script.

If there is only one active stream everything works as expected.

@blacktwin
Copy link
Author

blacktwin commented Jun 14, 2017

@csy7550 Updated the script. thanks for the troubleshooting. The exit(0) in line 107 was causing the problem.

@csy7550
Copy link

csy7550 commented Jun 15, 2017

@blacktwin That fixed the problem with not creating sub_script with multiple streams, but I found another set of bugs.

First:

Steps to reproduce:

  • Start transcoded stream 1 from user 1
  • Start transcoded stream 2 from user 1
  • Pause transcoded stream 1 from user 1
  • Pause transcoded stream 2 from user 1

sub_script is created for both sessions, and both are killed, but output from both says:

PlexPy Notifiers :: Script returned: Killing user 1's stream of stream 2

Second:

Steps to reproduce:

  • Start stream 1 from user 1
  • Start stream 2 from user 1
  • Pause stream 1 from user 1
  • Let the script run while stream 2 from user 1 is still running

If you don't get an error, resume/restart stream 1 from user 1 and pause stream 2 from user 1.
On one of the streams you'll get this error and stream won't get killed:

2017-06-15 08:21:05 ERROR PlexPy Notifiers :: Script error:
Traceback (most recent call last):
File "pfj6h65p5j6lqu2hisznpw2l.py", line 10, in
x += kill_stream(sessionID, message, 10, n)
File "C:\drzoidberg33-plexpy-786a374\scripts\create_wait_kill_trans.py", line 91, in kill_stream
sys.stdout.write("{a.user}'s stream of {a.title} restarted".format(a=a))
AttributeError: 'dict' object has no attribute 'user'

@blacktwin
Copy link
Author

blacktwin commented Jun 15, 2017

@csy7550 Thanks again. Looks like I didn't test this as thoroughly as I should have. The last error message should be fixed now. I'll need more time to look at the other issue.

@blacktwin
Copy link
Author

@csy7550 Script is updated. Please try to reproduce.

@csy7550
Copy link

csy7550 commented Jun 16, 2017

@blacktwin tried again with a new error.

Steps to reproduce:

  • Start stream 1 user 1
  • Start stream 2 user 1
  • Pause stream 1 user 1
  • Pause stream 2 user 1

Result:

  • First stream killed

  • Second stream output this error:

    PlexPy Notifiers :: Script error:
    Traceback (most recent call last):
    File "b8nshb6sk46o6lg04kwcbkme.py", line 11, in
    kill_stream(sessionID, message, 60, n, 'REDACTED', 'REDACTED ')
    File "C:\drzoidberg33-plexpy-786a374\scripts\create_wait_kill_trans.py", line 85, in kill_stream
    if a['Player']['state'] == 'paused' and a['Media']['Part']['decision'] == 'transcode' and xtime == ntime:
    TypeError: list indices must be integers, not str

@blacktwin
Copy link
Author

@csy7550 i was getting this error too, but only sometimes and when the pausing was relatively close in time. can you confirm? wait 30 seconds before pausing the second stream.

@csy7550
Copy link

csy7550 commented Jun 16, 2017

@blacktwin will try as fast as possible with the house full of kids :p

@csy7550
Copy link

csy7550 commented Jun 16, 2017

@blacktwin I tried to reproduce, but I never got that far before random weirdness start happening.

Every time I've tested the script I've been alone on the server, but now there are 4 remote users and it's like
the more streams, the more jibberish plex api returns (?)

For instance, when trying to reproduce multiple streams from one user, I didn't even have to pause the second stream
before getting this error on the first stream:

PlexPy Notifiers :: Script error:
    File "oaxwa5ca4rhvw4pooqs17dgz.py", line 10
        x += kill_stream(sessionID, message, 10, n, 'REDACTED', 'REDACTED')
                                                                                                                                                      ^
SyntaxError: invalid syntax 

The ^ isn't a typo from me, it's the actual output.

So I thought I'd try with only one stream.

I pause stream and script runs.
After 10sec plexpy log tells me stream has resumed, even though it's still paused. Not only does it tell me
stream has resumed, but the information in the output is double.

PlexPy Notifiers :: Script returned:
REDACTED's stream of REDACTED restartedREDACTED's stream of REDACTED restarted

So I'm not sure what to think..

@blacktwin
Copy link
Author

@csy7550 Updated the script. Please try again.

@csy7550
Copy link

csy7550 commented Jun 28, 2017

@blacktwin Sorry, been busy at work.. Works much better now, thank you!

I've tested everything I could think of and the only error I can produce now is if the client exits the stream
while paused and script is running. This will produce an error and subscript won't delete itself.

PlexPy Notifiers :: Script error:
Traceback (most recent call last):
File "ntzd8cew3oyg1ymbvjdgf9su.py", line 11, in
x += kill_stream(sessionID, message, 10, n, 'REDACTED', 'REDACTED', '64')
File "C:\drzoidberg33-plexpy-786a374\scripts\create_wait_kill_trans.py", line 83, in kill_stream
for a in response['MediaContainer']['Video']:
KeyError: 'Video'

Steps to reproduce:

  • Start stream 1 user 1
  • Start stream 2 user 1
  • Pause stream 1 user 1
  • Pause stream 2 user 1
  • Wait until scripts launches
  • Stop/exit playback from client side before script reaches timeout value

@blacktwin
Copy link
Author

@csy7550 Great thanks. Updated script to catch no current streams and delete the sub-script.

@csy7550
Copy link

csy7550 commented Jun 29, 2017

@blacktwin Fantastic! Everything works as expected now. Great work! I'll unleash the script on my 20 users and report back if any issues, never know what scenarios might unfold when users actually start using things..

@csy7550
Copy link

csy7550 commented Jun 29, 2017

@blacktwin Well, that didn't take long.. and I'm not sure if this is Plex's fault or the script.

  • Pausing Direct Play works as expected. Script runs, no subscript is created.
  • Pausing Transcoded stream works as expected. Script runs, creates subscript and so on..
  • Pausing Direct Stream (Both Video and Audio) does not work as expected.

Even though both video and audio is direct streaming subscript is created with a different looking filename than transcoded streams:

e.g: C92380AF-653B-45B0-BA0D-2BA31207D2A3.py

(Transcoded streams get e.g oaxwa5ca4rhvw4pooqs17dgz.py)

I've only had one user with this scenario yet, but user paused/resumed a few times and quit the stream before timeout and I got these errors:

PlexPy Notifiers :: Script error:
Traceback (most recent call last):
File "C92380AF-653B-45B0-BA0D-2BA31207D2A3.py", line 15, in
WindowsError: [Error 2] Systemet finner ikke angitt fil: 'C92380AF-653B-45B0-BA0D-2BA31207D2A3.py'

(Norwegian for did not find file).

..and finally

PlexPy Notifiers :: Script error:
Traceback (most recent call last):
    File "C92380AF-653B-45B0-BA0D-2BA31207D2A3.py", line 11, in
        x += kill_stream(sessionID, message, 600, n, 'REDACTED', 'REDACTED', '10')
    File "C:\drzoidberg33-plexpy-786a374\scripts\create_wait_kill_trans.py", line 83, in kill_stream
        if response['MediaContainer']['Video']:
KeyError: 'Video' 

Subscript did not delete itself after timeout.

Could this be related to a change within Plex 1.7.5? The reason I ask is because when I checked the
Status -> Now Playing within Plex it says Transcoding (!) and for both Video and Audio it says Direct Stream (!).

PlexPy says Direct Stream all over at the same time.

https://www.dropbox.com/s/1b3xtid7uj7ynj9/tc_ds.jpg?dl=0

Will let the script run and watch what happens.

@blacktwin
Copy link
Author

@csy7550 thanks for the report. It could be that the subtitles are being transcoded? Maybe? I'll look into the other issues.

@csy7550
Copy link

csy7550 commented Jun 29, 2017

@blacktwin this episode in particular had no subtitles at the moment so that can't be it.. and I know plex correctly states that video is being transcoded if subtitles are burned in the stream.. I can't be 100% sure, but I think something has changed within Plex, because I'm positive Plex "now playing" said "direct stream" before when both video and audio where direct stream.. now it says transcoding like in the picture I posted even though both video and audio is clearly direct streaming..

@csy7550
Copy link

csy7550 commented Jun 29, 2017

@blacktwin I can now confirm that script also kills direct streams. No subtitles involved. Did not get any errors this time like before,
but Plex still states video is direct streaming and audio is direct streaming, but "overall" transcoding like in the picture I posted.

Direct play, no problem.

edit: posted on plex pass forum, maybe it's a bug within plex.

@csy7550
Copy link

csy7550 commented Jun 30, 2017

@blacktwin no bug within Plex, but I think I know why direct streams also get killed. It's because Plex uses the transcoder to remux the file since most of the time direct stream is when container isn't supported by the client. Had a look at the sessions xml while direct streaming. Maybe you could change what "decision" to look for with the script.

<Part audioProfile="lc" id="90193" videoProfile="high" bitrate="1492" container="mkv" duration="1316449" height="480" width="720" decision="transcode" selected="1"> <Stream bitrate="1492" codec="h264" default="1" height="480" id="292626" language="English" languageCode="eng" streamType="1" width="720" decision="copy"/> <Stream bitrateMode="cbr" channels="2" codec="aac" default="1" id="292627" language="English" languageCode="eng" selected="1" streamType="2" decision="copy"/> </Part>

and

<TranscodeSession key="/transcode/sessions/868ek8sb2yvfjrvsowbhwkmj" throttled="1" complete="0" progress="14.699999809265137" speed="0" duration="1316449" remaining="11211" context="streaming" sourceVideoCodec="h264" sourceAudioCodec="aac" videoDecision="copy" audioDecision="copy" protocol="http" container="mkv" videoCodec="h264" audioCodec="aac" audioChannels="2" transcodeHwRequested="0" transcodeHwFullPipeline="1"/>

I think "videoDecision" is the way to go. Let me know what you think.

edit: I can see that technically this is a bit outside the box as the script clearly states it is for killing transcoded sessions, and direct stream is sort of a transcoded stream, but there is little to none cpu usage when direct streaming so why kill it.. and direct streamed video does not count when using "max transcode" setting in Plex, only transcoded video does. This is the main reason I like this script so much, as I have many users and I've set max transcodes to 6 within Plex. Sometimes you could end up with 4 out of 6 streams paused for a long time, while server still got free resources, but a new transcoded stream wont start since max is set to 6. This is where the script comes in VERY handy.

edit2: "videoDecision" may be a bad idea, just saw a session xml for direct play, and the tag is not available then.

@blacktwin
Copy link
Author

@csy7550 so you're good? :)

@csy7550
Copy link

csy7550 commented Jun 30, 2017

@blacktwin Well, it works :-) but an exception for direct streams would be great.. no reason for killing direct streams :-)

@blacktwin
Copy link
Author

@csy7550 If you want, you can try this script. It is kinda the same as this one but set to kill all paused streams and uses PlexPy's Activity to check the stream instead of the xml. Just change line 111 from:
if a.state == 'paused' and xtime == ntime: to if a.state == 'paused' and a.video_decision == 'transcode' and xtime == ntime: to include the check for transcoding. Again, that way you're relying on PlexPy definition instead of my definition based on the xml.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment