Merge pull request #135 from Hellowlol/logs

Add download log/db and stop playback
This commit is contained in:
Michael Shepanski 2017-02-26 21:14:46 -05:00 committed by GitHub
commit 621d6fe667
8 changed files with 94 additions and 22 deletions

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
import json
import threading
import websocket
from plexapi import log
from plexapi.exceptions import Unsupported
class AlertListener(threading.Thread):
@ -12,9 +12,6 @@ class AlertListener(threading.Thread):
alerts you must call .start() on the object once it's created. When calling
`PlexServer.startAlertListener()`, the thread will be started for you.
In order to use this feature, you must have websocket-client installed in your Python path.
This can be installed vis pip `pip install websocket-client`.
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer this listener is connected to.
callback (func): Callback function to call on recieved messages. The callback function
@ -30,16 +27,10 @@ class AlertListener(threading.Thread):
super(AlertListener, self).__init__()
def run(self):
# try importing websocket-client package
try:
import websocket
except:
raise Unsupported('Websocket-client package is required to use this feature.')
# create the websocket connection
url = self._server.url(self.key).replace('http', 'ws')
log.info('Starting AlertListener: %s', url)
self._ws = websocket.WebSocketApp(url,
on_message=self._onMessage,
self._ws = websocket.WebSocketApp(url, on_message=self._onMessage,
on_error=self._onError)
self._ws.run_forever()

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import re
from plexapi import log, utils
from plexapi.compat import urlencode
from plexapi.compat import quote_plus, urlencode
from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
OPERATORS = {
@ -354,6 +354,7 @@ class Playable(object):
sessionKey (int): Active session key.
username (str): Username of the person playing this item (for active sessions).
players (:class:`~plexapi.client.PlexClient`): Client objects playing this item (for active sessions).
session (:class:`~plexapi.media.Session`): Session object, for a playing media file.
transcodeSession (:class:`~plexapi.media.TranscodeSession`): Transcode Session object
if item is being transcoded (None otherwise).
viewedAt (datetime): Datetime item was last viewed (history).
@ -364,6 +365,7 @@ class Playable(object):
self.usernames = self.listAttrs(data, 'title', etag='User') # session
self.players = self.findItems(data, etag='Player') # session
self.transcodeSessions = self.findItems(data, etag='TranscodeSession') # session
self.session = self.findItems(data, etag='Session') # session
self.viewedAt = utils.toDatetime(data.attrib.get('viewedAt')) # history
self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist
@ -444,3 +446,8 @@ class Playable(object):
if filepath:
filepaths.append(filepath)
return filepaths
def stop(self, reason=''):
""" Stop playback for a media item. """
key = '/status/sessions/terminate?sessionId=%s&reason=%s' % (self.session[0].id, quote_plus(reason))
return self._server.query(key)

View file

@ -17,6 +17,11 @@ try:
except ImportError:
from urllib import quote
try:
from urllib.parse import quote_plus
except ImportError:
from urllib import quote_plus
try:
from urllib.parse import unquote
except ImportError:
@ -31,4 +36,3 @@ try:
from xml.etree import cElementTree as ElementTree
except ImportError:
from xml.etree import ElementTree

View file

@ -253,6 +253,17 @@ class SubtitleStream(MediaPartStream):
self.title = data.attrib.get('title')
@utils.registerPlexObject
class Session(PlexObject):
""" Represents a current session. """
TAG = 'Session'
def _loadData(self, data):
self.id = data.attrib.get('id')
self.bandwidth = utils.cast(int, data.attrib.get('bandwidth'))
self.location = data.attrib.get('location')
@utils.registerPlexObject
class TranscodeSession(PlexObject):
""" Represents a current transcode session.

View file

@ -174,12 +174,28 @@ class PlexServer(PlexObject):
def clients(self):
""" Returns a list of all :class:`~plexapi.client.PlexClient` objects
connected to this server.
"""
connected to this server."""
items = []
cache_resource = None
from plexapi.myplex import MyPlexResource
for elem in self.query('/clients'):
baseurl = 'http://%s:%s' % (elem.attrib['host'], elem.attrib['port'])
items.append(PlexClient(baseurl, server=self, data=elem))
# Some shitty clients dont include a port..
port = elem.attrib.get('port')
if port is None:
log.debug("%s didn't provide a port. Checking https://plex.tv/devices.xml" % elem.attrib.get('name'))
data = cache_resource or self._server._session.get('https://plex.tv/devices.xml?X-Plex-Token=%s' % self.token) # noqa
cache_resource = data
resources = MyPlexResource(self, data)
for resource in resources:
if resource.clientIdentifier == elem.attrib.get('machineIdentifier'):
for conn in resource.connection:
if conn.local is True:
port = conn.port
break
baseurl = 'http://%s:%s' % (elem.attrib['host'], port)
items.append(PlexClient(baseurl=baseurl, server=self, data=elem))
return items
def client(self, name):
@ -326,6 +342,16 @@ class PlexServer(PlexObject):
return '%s%s%sX-Plex-Token=%s' % (self._baseurl, key, delim, self._token)
return '%s%s' % (self._baseurl, key)
def downloadLogs(self, savepath=None, unpack=False):
url = self.url('/diagnostics/databases')
fp = utils.download(url, filename=None, savepath=savepath, unpack=unpack, session=self._session)
return fp
def downloadDBS(self, savepath=None, unpack=False):
url = self.url('/diagnostics/logs')
fp = utils.download(url, filename=None, savepath=savepath, unpack=unpack, session=self._session)
return fp
class Account(PlexObject):
""" Contains the locally cached MyPlex account information. The properties provided don't

View file

@ -1,12 +1,15 @@
# -*- coding: utf-8 -*-
import logging
import os
import re
import requests
import time
import zipfile
from datetime import datetime
from threading import Thread
from plexapi.compat import quote, string_type
from plexapi.exceptions import NotFound
from threading import Thread
# Search Types - Plex uses these to filter specific media types when searching.
# Library Types - Populated at runtime
@ -224,7 +227,7 @@ def downloadSessionImages(server, filename=None, height=150, width=150, opacity=
return info
def download(url, filename=None, savepath=None, session=None, chunksize=4024, mocked=False):
def download(url, filename=None, savepath=None, session=None, chunksize=4024, mocked=False, unpack=False):
""" Helper to download a thumb, videofile or other media item. Returns the local
path to the downloaded file.
@ -234,6 +237,7 @@ def download(url, filename=None, savepath=None, session=None, chunksize=4024, mo
savepath (str): Defaults to current working dir.
chunksize (int): What chunksize read/write at the time.
mocked (bool): Helper to do evertything except write the file.
unpack (bool): Unpack the zip file
Example:
>>> download(a_episode.getStreamURL(), a_episode.location)
@ -251,10 +255,20 @@ def download(url, filename=None, savepath=None, session=None, chunksize=4024, mo
except OSError:
if not os.path.isdir(savepath): # pragma: no cover
raise
filename = os.path.basename(filename)
fullpath = os.path.join(savepath, filename)
try:
response = session.get(url, stream=True)
# Lets grab the name if we dont supply one.
# This will be used for downloading logs/db etc.
if filename is None and response.headers.get('Content-Disposition'):
filename = re.findall(ur'filename=\"(.+)\"', response.headers.get('Content-Disposition'))
if filename:
filename = filename[0]
filename = os.path.basename(filename)
fullpath = os.path.join(savepath, filename)
# images dont have a extention so we try
# to guess it from content-type
ext = os.path.splitext(fullpath)[-1]
@ -266,16 +280,24 @@ def download(url, filename=None, savepath=None, session=None, chunksize=4024, mo
if 'image' in cp:
ext = '.%s' % cp.split('/')[1]
fullpath = '%s%s' % (fullpath, ext)
if mocked:
log.debug('Mocked download %s', fullpath)
return fullpath
with open(fullpath, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunksize):
if chunk:
f.write(chunk)
if fullpath.endswith('zip') and unpack is True:
with zipfile.ZipFile(fullpath, 'r') as zp:
zp.extractall(savepath)
# log.debug('Downloaded %s to %s from %s' % (filename, fullpath, url))
return fullpath
except Exception as err: # pragma: no cover
log.error('Error downloading file: %s' % err)
log.exception('Error downloading file: %s' % err)
raise
# log.exception('Failed to download %s to %s %s' % (url, fullpath, e))

View file

@ -3,3 +3,4 @@
# pip install -r requirments.txt
#---------------------------------------------------------
requests
websocket

View file

@ -206,3 +206,13 @@ def test_server_account(pms):
assert acc.subscriptionFeatures == []
assert acc.subscriptionState == 'Unknown'
assert acc.username == 'testplexapi@gmail.com'
def test_server_downloadLogs(tmpdir, pms):
pms.downloadLogs(savepath=str(tmpdir), unpack=True)
assert len(tmpdir.listdir()) > 1
def test_server_downloadDB(tmpdir, pms):
pms.downloadDBS(savepath=str(tmpdir), unpack=True)
assert len(tmpdir.listdir()) > 1