You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
250 lines
6.9 KiB
250 lines
6.9 KiB
6 years ago
|
from plex import Plex
|
||
|
from plex_activity.sources.base import Source
|
||
|
from plex_activity.sources.s_logging.parsers import NowPlayingParser, ScrobbleParser
|
||
|
|
||
|
from asio import ASIO
|
||
|
from asio.file import SEEK_ORIGIN_CURRENT
|
||
|
from io import BufferedReader
|
||
|
import inspect
|
||
|
import logging
|
||
|
import os
|
||
|
import platform
|
||
|
import time
|
||
|
|
||
|
log = logging.getLogger(__name__)
|
||
|
|
||
|
PATH_HINTS = {
|
||
|
'Darwin': [
|
||
|
lambda: os.path.join(os.getenv('HOME'), 'Library/Logs/Plex Media Server.log')
|
||
|
],
|
||
|
'FreeBSD': [
|
||
|
# FreeBSD
|
||
|
'/usr/local/plexdata/Plex Media Server/Logs/Plex Media Server.log',
|
||
|
'/usr/local/plexdata-plexpass/Plex Media Server/Logs/Plex Media Server.log',
|
||
|
|
||
|
# FreeNAS
|
||
|
'/usr/pbi/plexmediaserver-amd64/plexdata/Plex Media Server/Logs/Plex Media Server.log',
|
||
|
'/var/db/plexdata/Plex Media Server/Logs/Plex Media Server.log',
|
||
|
'/var/db/plexdata-plexpass/Plex Media Server/Logs/Plex Media Server.log'
|
||
|
],
|
||
|
'Linux': [
|
||
|
# QNAP
|
||
|
'/share/HDA_DATA/.qpkg/PlexMediaServer/Library/Plex Media Server/Logs/Plex Media Server.log',
|
||
|
|
||
|
# Debian
|
||
|
'/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Logs/Plex Media Server.log'
|
||
|
],
|
||
|
'Windows': [
|
||
|
lambda: os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\\Logs\\Plex Media Server.log')
|
||
|
]
|
||
|
}
|
||
|
|
||
|
|
||
|
class Logging(Source):
|
||
|
name = 'logging'
|
||
|
events = [
|
||
|
'logging.playing',
|
||
|
'logging.action.played',
|
||
|
'logging.action.unplayed'
|
||
|
]
|
||
|
|
||
|
parsers = []
|
||
|
|
||
|
path = None
|
||
|
path_hints = PATH_HINTS
|
||
|
|
||
|
def __init__(self, activity):
|
||
|
super(Logging, self).__init__()
|
||
|
|
||
|
self.parsers = [p(self) for p in Logging.parsers]
|
||
|
|
||
|
self.file = None
|
||
|
self.reader = None
|
||
|
|
||
|
self.path = None
|
||
|
|
||
|
# Pipe events to the main activity instance
|
||
|
self.pipe(self.events, activity)
|
||
|
|
||
|
def run(self):
|
||
|
line = self.read_line_retry(ping=True, stale_sleep=0.5)
|
||
|
if not line:
|
||
|
log.info('Unable to read log file')
|
||
|
return
|
||
|
|
||
|
log.debug('Ready')
|
||
|
|
||
|
while True:
|
||
|
# Grab the next line of the log
|
||
|
line = self.read_line_retry(ping=True)
|
||
|
|
||
|
if line:
|
||
|
self.process(line)
|
||
|
else:
|
||
|
log.info('Unable to read log file')
|
||
|
|
||
|
def process(self, line):
|
||
|
for parser in self.parsers:
|
||
|
if parser.process(line):
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
def read_line(self):
|
||
|
if not self.file:
|
||
|
path = self.get_path()
|
||
|
if not path:
|
||
|
raise Exception('Unable to find the location of "Plex Media Server.log"')
|
||
|
|
||
|
# Open file
|
||
|
self.file = ASIO.open(path, opener=False)
|
||
|
self.file.seek(self.file.get_size(), SEEK_ORIGIN_CURRENT)
|
||
|
|
||
|
# Create buffered reader
|
||
|
self.reader = BufferedReader(self.file)
|
||
|
|
||
|
self.path = self.file.get_path()
|
||
|
log.info('Opened file path: "%s"' % self.path)
|
||
|
|
||
|
return self.reader.readline()
|
||
|
|
||
|
def read_line_retry(self, timeout=60, ping=False, stale_sleep=1.0):
|
||
|
line = None
|
||
|
stale_since = None
|
||
|
|
||
|
while not line:
|
||
|
line = self.read_line()
|
||
|
|
||
|
if line:
|
||
|
stale_since = None
|
||
|
time.sleep(0.05)
|
||
|
break
|
||
|
|
||
|
if stale_since is None:
|
||
|
stale_since = time.time()
|
||
|
time.sleep(stale_sleep)
|
||
|
continue
|
||
|
elif (time.time() - stale_since) > timeout:
|
||
|
return None
|
||
|
elif (time.time() - stale_since) > timeout / 2:
|
||
|
# Nothing returned for 5 seconds
|
||
|
if self.file.get_path() != self.path:
|
||
|
log.debug("Log file moved (probably rotated), closing")
|
||
|
self.close()
|
||
|
elif ping:
|
||
|
# Ping server to see if server is still active
|
||
|
Plex.detail()
|
||
|
ping = False
|
||
|
|
||
|
time.sleep(stale_sleep)
|
||
|
|
||
|
return line
|
||
|
|
||
|
def close(self):
|
||
|
if not self.file:
|
||
|
return
|
||
|
|
||
|
try:
|
||
|
# Close the buffered reader
|
||
|
self.reader.close()
|
||
|
except Exception as ex:
|
||
|
log.error('reader.close() - raised exception: %s', ex, exc_info=True)
|
||
|
finally:
|
||
|
self.reader = None
|
||
|
|
||
|
try:
|
||
|
# Close the file handle
|
||
|
self.file.close()
|
||
|
except OSError as ex:
|
||
|
if ex.errno == 9:
|
||
|
# Bad file descriptor, already closed?
|
||
|
log.info('file.close() - ignoring raised exception: %s (already closed)', ex)
|
||
|
else:
|
||
|
log.error('file.close() - raised exception: %s', ex, exc_info=True)
|
||
|
except Exception as ex:
|
||
|
log.error('file.close() - raised exception: %s', ex, exc_info=True)
|
||
|
finally:
|
||
|
self.file = None
|
||
|
|
||
|
@classmethod
|
||
|
def get_path(cls):
|
||
|
if cls.path:
|
||
|
return cls.path
|
||
|
|
||
|
hints = cls.get_hints()
|
||
|
|
||
|
log.debug('hints: %r', hints)
|
||
|
|
||
|
if not hints:
|
||
|
log.error('Unable to find any hints for "%s", operating system not supported', platform.system())
|
||
|
return None
|
||
|
|
||
|
for hint in hints:
|
||
|
log.debug('Testing if "%s" exists', hint)
|
||
|
|
||
|
if os.path.exists(hint):
|
||
|
cls.path = hint
|
||
|
break
|
||
|
|
||
|
if cls.path:
|
||
|
log.debug('Using the path: %r', cls.path)
|
||
|
else:
|
||
|
log.error('Unable to find a valid path for "Plex Media Server.log"', extra={
|
||
|
'data': {
|
||
|
'hints': hints
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return cls.path
|
||
|
|
||
|
@classmethod
|
||
|
def add_hint(cls, path, system=None):
|
||
|
if system not in cls.path_hints:
|
||
|
cls.path_hints[system] = []
|
||
|
|
||
|
cls.path_hints[system].append(path)
|
||
|
|
||
|
@classmethod
|
||
|
def get_hints(cls):
|
||
|
# Retrieve system hints
|
||
|
hints_system = PATH_HINTS.get(platform.system(), [])
|
||
|
|
||
|
# Retrieve global hints
|
||
|
hints_global = PATH_HINTS.get(None, [])
|
||
|
|
||
|
# Retrieve hint from server preferences (if available)
|
||
|
data_path = Plex[':/prefs'].get('LocalAppDataPath')
|
||
|
|
||
|
if data_path:
|
||
|
hints_global.append(os.path.join(data_path.value, "Plex Media Server", "Logs", "Plex Media Server.log"))
|
||
|
else:
|
||
|
log.info('Unable to retrieve "LocalAppDataPath" from server')
|
||
|
|
||
|
hints = []
|
||
|
|
||
|
for hint in (hints_global + hints_system):
|
||
|
# Resolve hint function
|
||
|
if inspect.isfunction(hint):
|
||
|
hint = hint()
|
||
|
|
||
|
# Check for duplicate
|
||
|
if hint in hints:
|
||
|
continue
|
||
|
|
||
|
hints.append(hint)
|
||
|
|
||
|
return hints
|
||
|
|
||
|
@classmethod
|
||
|
def test(cls):
|
||
|
# TODO "Logging" source testing
|
||
|
return True
|
||
|
|
||
|
@classmethod
|
||
|
def register(cls, parser):
|
||
|
cls.parsers.append(parser)
|
||
|
|
||
|
|
||
|
Logging.register(NowPlayingParser)
|
||
|
Logging.register(ScrobbleParser)
|