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