mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-11 15:56:07 -07:00
Initial Commit
This commit is contained in:
commit
88daa3fb91
1311 changed files with 256240 additions and 0 deletions
336
plexpy/__init__.py
Normal file
336
plexpy/__init__.py
Normal file
|
@ -0,0 +1,336 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of
|
||||
# XBian - XBMC on the Raspberry Pi
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import threading
|
||||
import webbrowser
|
||||
import sqlite3
|
||||
import cherrypy
|
||||
import datetime
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
|
||||
from plexpy import versioncheck, logger
|
||||
import plexpy.config
|
||||
|
||||
PROG_DIR = None
|
||||
FULL_PATH = None
|
||||
|
||||
ARGS = None
|
||||
SIGNAL = None
|
||||
|
||||
SYS_PLATFORM = None
|
||||
SYS_ENCODING = None
|
||||
|
||||
QUIET = False
|
||||
VERBOSE = True
|
||||
DAEMON = False
|
||||
CREATEPID = False
|
||||
PIDFILE = None
|
||||
|
||||
SCHED = BackgroundScheduler()
|
||||
SCHED_LOCK = threading.Lock()
|
||||
|
||||
INIT_LOCK = threading.Lock()
|
||||
_INITIALIZED = False
|
||||
started = False
|
||||
|
||||
DATA_DIR = None
|
||||
|
||||
CONFIG = None
|
||||
|
||||
DB_FILE = None
|
||||
|
||||
LOG_LIST = []
|
||||
|
||||
INSTALL_TYPE = None
|
||||
CURRENT_VERSION = None
|
||||
LATEST_VERSION = None
|
||||
COMMITS_BEHIND = None
|
||||
|
||||
UMASK = None
|
||||
|
||||
|
||||
def initialize(config_file):
|
||||
with INIT_LOCK:
|
||||
|
||||
global CONFIG
|
||||
global _INITIALIZED
|
||||
global CURRENT_VERSION
|
||||
global LATEST_VERSION
|
||||
global UMASK
|
||||
|
||||
CONFIG = plexpy.config.Config(config_file)
|
||||
|
||||
assert CONFIG is not None
|
||||
|
||||
if _INITIALIZED:
|
||||
return False
|
||||
|
||||
if CONFIG.HTTP_PORT < 21 or CONFIG.HTTP_PORT > 65535:
|
||||
plexpy.logger.warn(
|
||||
'HTTP_PORT out of bounds: 21 < %s < 65535', CONFIG.HTTP_PORT)
|
||||
CONFIG.HTTP_PORT = 8181
|
||||
|
||||
if CONFIG.HTTPS_CERT == '':
|
||||
CONFIG.HTTPS_CERT = os.path.join(DATA_DIR, 'server.crt')
|
||||
if CONFIG.HTTPS_KEY == '':
|
||||
CONFIG.HTTPS_KEY = os.path.join(DATA_DIR, 'server.key')
|
||||
|
||||
if not CONFIG.LOG_DIR:
|
||||
CONFIG.LOG_DIR = os.path.join(DATA_DIR, 'logs')
|
||||
|
||||
if not os.path.exists(CONFIG.LOG_DIR):
|
||||
try:
|
||||
os.makedirs(CONFIG.LOG_DIR)
|
||||
except OSError:
|
||||
CONFIG.LOG_DIR = None
|
||||
|
||||
if not QUIET:
|
||||
sys.stderr.write("Unable to create the log directory. " \
|
||||
"Logging to screen only.\n")
|
||||
|
||||
# Start the logger, disable console if needed
|
||||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
||||
verbose=VERBOSE)
|
||||
|
||||
if not CONFIG.CACHE_DIR:
|
||||
# Put the cache dir in the data dir for now
|
||||
CONFIG.CACHE_DIR = os.path.join(DATA_DIR, 'cache')
|
||||
if not os.path.exists(CONFIG.CACHE_DIR):
|
||||
try:
|
||||
os.makedirs(CONFIG.CACHE_DIR)
|
||||
except OSError as e:
|
||||
logger.error("Could not create cache dir '%s': %s", DATA_DIR, e)
|
||||
|
||||
# Initialize the database
|
||||
logger.info('Checking to see if the database has all tables....')
|
||||
try:
|
||||
dbcheck()
|
||||
except Exception as e:
|
||||
logger.error("Can't connect to the database: %s", e)
|
||||
|
||||
# Get the currently installed version. Returns None, 'win32' or the git
|
||||
# hash.
|
||||
CURRENT_VERSION, CONFIG.GIT_BRANCH = versioncheck.getVersion()
|
||||
|
||||
# Write current version to a file, so we know which version did work.
|
||||
# This allowes one to restore to that version. The idea is that if we
|
||||
# arrive here, most parts of PlexPy seem to work.
|
||||
if CURRENT_VERSION:
|
||||
version_lock_file = os.path.join(DATA_DIR, "version.lock")
|
||||
|
||||
try:
|
||||
with open(version_lock_file, "w") as fp:
|
||||
fp.write(CURRENT_VERSION)
|
||||
except IOError as e:
|
||||
logger.error("Unable to write current version to file '%s': %s",
|
||||
version_lock_file, e)
|
||||
|
||||
# Check for new versions
|
||||
if CONFIG.CHECK_GITHUB_ON_STARTUP:
|
||||
try:
|
||||
LATEST_VERSION = versioncheck.checkGithub()
|
||||
except:
|
||||
logger.exception("Unhandled exception")
|
||||
LATEST_VERSION = CURRENT_VERSION
|
||||
else:
|
||||
LATEST_VERSION = CURRENT_VERSION
|
||||
|
||||
# Store the original umask
|
||||
UMASK = os.umask(0)
|
||||
os.umask(UMASK)
|
||||
|
||||
_INITIALIZED = True
|
||||
return True
|
||||
|
||||
|
||||
def daemonize():
|
||||
if threading.activeCount() != 1:
|
||||
logger.warn(
|
||||
'There are %r active threads. Daemonizing may cause'
|
||||
' strange behavior.',
|
||||
threading.enumerate())
|
||||
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
# Do first fork
|
||||
try:
|
||||
pid = os.fork() # @UndefinedVariable - only available in UNIX
|
||||
if pid != 0:
|
||||
sys.exit(0)
|
||||
except OSError, e:
|
||||
raise RuntimeError("1st fork failed: %s [%d]", e.strerror, e.errno)
|
||||
|
||||
os.setsid()
|
||||
|
||||
# Make sure I can read my own files and shut out others
|
||||
prev = os.umask(0) # @UndefinedVariable - only available in UNIX
|
||||
os.umask(prev and int('077', 8))
|
||||
|
||||
# Make the child a session-leader by detaching from the terminal
|
||||
try:
|
||||
pid = os.fork() # @UndefinedVariable - only available in UNIX
|
||||
if pid != 0:
|
||||
sys.exit(0)
|
||||
except OSError, e:
|
||||
raise RuntimeError("2nd fork failed: %s [%d]", e.strerror, e.errno)
|
||||
|
||||
dev_null = file('/dev/null', 'r')
|
||||
os.dup2(dev_null.fileno(), sys.stdin.fileno())
|
||||
|
||||
si = open('/dev/null', "r")
|
||||
so = open('/dev/null', "a+")
|
||||
se = open('/dev/null', "a+")
|
||||
|
||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||
|
||||
pid = os.getpid()
|
||||
logger.info('Daemonized to PID: %d', pid)
|
||||
|
||||
if CREATEPID:
|
||||
logger.info("Writing PID %d to %s", pid, PIDFILE)
|
||||
with file(PIDFILE, 'w') as fp:
|
||||
fp.write("%s\n" % pid)
|
||||
|
||||
|
||||
def launch_browser(host, port, root):
|
||||
if host == '0.0.0.0':
|
||||
host = 'localhost'
|
||||
|
||||
if CONFIG.ENABLE_HTTPS:
|
||||
protocol = 'https'
|
||||
else:
|
||||
protocol = 'http'
|
||||
|
||||
try:
|
||||
webbrowser.open('%s://%s:%i%s' % (protocol, host, port, root))
|
||||
except Exception as e:
|
||||
logger.error('Could not launch browser: %s', e)
|
||||
|
||||
|
||||
def initialize_scheduler():
|
||||
"""
|
||||
Start the scheduled background tasks. Re-schedule if interval settings changed.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
with SCHED_LOCK:
|
||||
|
||||
# Check if scheduler should be started
|
||||
start_jobs = not len(SCHED.get_jobs())
|
||||
|
||||
#Update check
|
||||
if CONFIG.CHECK_GITHUB_INTERVAL:
|
||||
minutes = CONFIG.CHECK_GITHUB_INTERVAL
|
||||
else:
|
||||
minutes = 0
|
||||
schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0, minutes=minutes)
|
||||
|
||||
# Start scheduler
|
||||
if start_jobs and len(SCHED.get_jobs()):
|
||||
try:
|
||||
SCHED.start()
|
||||
except Exception as e:
|
||||
logger.info(e)
|
||||
|
||||
# Debug
|
||||
#SCHED.print_jobs()
|
||||
|
||||
|
||||
def schedule_job(function, name, hours=0, minutes=0):
|
||||
"""
|
||||
Start scheduled job if starting or restarting plexpy.
|
||||
Reschedule job if Interval Settings have changed.
|
||||
Remove job if if Interval Settings changed to 0
|
||||
|
||||
"""
|
||||
|
||||
job = SCHED.get_job(name)
|
||||
if job:
|
||||
if hours == 0 and minutes == 0:
|
||||
SCHED.remove_job(name)
|
||||
logger.info("Removed background task: %s", name)
|
||||
elif job.trigger.interval != datetime.timedelta(hours=hours, minutes=minutes):
|
||||
SCHED.reschedule_job(name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes))
|
||||
logger.info("Re-scheduled background task: %s", name)
|
||||
elif hours > 0 or minutes > 0:
|
||||
SCHED.add_job(function, id=name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes))
|
||||
logger.info("Scheduled background task: %s", name)
|
||||
|
||||
|
||||
def start():
|
||||
global started
|
||||
|
||||
if _INITIALIZED:
|
||||
initialize_scheduler()
|
||||
started = True
|
||||
|
||||
|
||||
def sig_handler(signum=None, frame=None):
|
||||
if signum is not None:
|
||||
logger.info("Signal %i caught, saving and exiting...", signum)
|
||||
shutdown()
|
||||
|
||||
|
||||
def dbcheck():
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
c = conn.cursor()
|
||||
conn.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
def shutdown(restart=False, update=False):
|
||||
cherrypy.engine.exit()
|
||||
SCHED.shutdown(wait=False)
|
||||
|
||||
CONFIG.write()
|
||||
|
||||
if not restart and not update:
|
||||
logger.info('PlexPy is shutting down...')
|
||||
|
||||
if update:
|
||||
logger.info('PlexPy is updating...')
|
||||
try:
|
||||
versioncheck.update()
|
||||
except Exception as e:
|
||||
logger.warn('PlexPy failed to update: %s. Restarting.', e)
|
||||
|
||||
if CREATEPID:
|
||||
logger.info('Removing pidfile %s', PIDFILE)
|
||||
os.remove(PIDFILE)
|
||||
|
||||
if restart:
|
||||
logger.info('PlexPy is restarting...')
|
||||
popen_list = [sys.executable, FULL_PATH]
|
||||
popen_list += ARGS
|
||||
if '--nolaunch' not in popen_list:
|
||||
popen_list += ['--nolaunch']
|
||||
logger.info('Restarting PlexPy with %s', popen_list)
|
||||
subprocess.Popen(popen_list, cwd=os.getcwd())
|
||||
|
||||
os._exit(0)
|
128
plexpy/api.py
Normal file
128
plexpy/api.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from plexpy import db, updater, cache, versioncheck, logger
|
||||
|
||||
import plexpy
|
||||
import json
|
||||
|
||||
cmd_list = ['getLogs', 'getVersion', 'checkGithub', 'shutdown', 'restart', 'update']
|
||||
|
||||
|
||||
class Api(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.apikey = None
|
||||
self.cmd = None
|
||||
self.id = None
|
||||
|
||||
self.kwargs = None
|
||||
|
||||
self.data = None
|
||||
|
||||
self.callback = None
|
||||
|
||||
def checkParams(self, *args, **kwargs):
|
||||
|
||||
if not plexpy.CONFIG.API_ENABLED:
|
||||
self.data = 'API not enabled'
|
||||
return
|
||||
if not plexpy.CONFIG.API_KEY:
|
||||
self.data = 'API key not generated'
|
||||
return
|
||||
if len(plexpy.CONFIG.API_KEY) != 32:
|
||||
self.data = 'API key not generated correctly'
|
||||
return
|
||||
|
||||
if 'apikey' not in kwargs:
|
||||
self.data = 'Missing api key'
|
||||
return
|
||||
|
||||
if kwargs['apikey'] != plexpy.CONFIG.API_KEY:
|
||||
self.data = 'Incorrect API key'
|
||||
return
|
||||
else:
|
||||
self.apikey = kwargs.pop('apikey')
|
||||
|
||||
if 'cmd' not in kwargs:
|
||||
self.data = 'Missing parameter: cmd'
|
||||
return
|
||||
|
||||
if kwargs['cmd'] not in cmd_list:
|
||||
self.data = 'Unknown command: %s' % kwargs['cmd']
|
||||
return
|
||||
else:
|
||||
self.cmd = kwargs.pop('cmd')
|
||||
|
||||
self.kwargs = kwargs
|
||||
self.data = 'OK'
|
||||
|
||||
def fetchData(self):
|
||||
|
||||
if self.data == 'OK':
|
||||
logger.info('Recieved API command: %s', self.cmd)
|
||||
methodToCall = getattr(self, "_" + self.cmd)
|
||||
methodToCall(**self.kwargs)
|
||||
if 'callback' not in self.kwargs:
|
||||
if isinstance(self.data, basestring):
|
||||
return self.data
|
||||
else:
|
||||
return json.dumps(self.data)
|
||||
else:
|
||||
self.callback = self.kwargs['callback']
|
||||
self.data = json.dumps(self.data)
|
||||
self.data = self.callback + '(' + self.data + ');'
|
||||
return self.data
|
||||
else:
|
||||
return self.data
|
||||
|
||||
def _dic_from_query(self, query):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
rows = myDB.select(query)
|
||||
|
||||
rows_as_dic = []
|
||||
|
||||
for row in rows:
|
||||
row_as_dic = dict(zip(row.keys(), row))
|
||||
rows_as_dic.append(row_as_dic)
|
||||
|
||||
return rows_as_dic
|
||||
|
||||
def _getLogs(self, **kwargs):
|
||||
pass
|
||||
|
||||
def _getVersion(self, **kwargs):
|
||||
self.data = {
|
||||
'git_path': plexpy.CONFIG.GIT_PATH,
|
||||
'install_type': plexpy.INSTALL_TYPE,
|
||||
'current_version': plexpy.CURRENT_VERSION,
|
||||
'latest_version': plexpy.LATEST_VERSION,
|
||||
'commits_behind': plexpy.COMMITS_BEHIND,
|
||||
}
|
||||
|
||||
def _checkGithub(self, **kwargs):
|
||||
versioncheck.checkGithub()
|
||||
self._getVersion()
|
||||
|
||||
def _shutdown(self, **kwargs):
|
||||
plexpy.SIGNAL = 'shutdown'
|
||||
|
||||
def _restart(self, **kwargs):
|
||||
plexpy.SIGNAL = 'restart'
|
||||
|
||||
def _update(self, **kwargs):
|
||||
plexpy.SIGNAL = 'update'
|
483
plexpy/cache.py
Normal file
483
plexpy/cache.py
Normal file
|
@ -0,0 +1,483 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import plexpy
|
||||
|
||||
from plexpy import db, helpers, logger, request
|
||||
|
||||
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
|
||||
|
||||
|
||||
class Cache(object):
|
||||
"""
|
||||
This class deals with getting, storing and serving up artwork (album art,
|
||||
artist images, etc) and info/descriptions (album info, artist descrptions)
|
||||
to and from the cache folder. This can be called from within a web
|
||||
interface. For example, using the helper functions `getInfo(id)` and
|
||||
`getArtwork(id)`, to utilize the cached images rather than having to
|
||||
retrieve them every time the page is reloaded.
|
||||
|
||||
You can call `getArtwork(id)` which will return an absolute path to the
|
||||
image file on the local machine, or if the cache directory doesn't exist,
|
||||
or can not be written to, it will return a url to the image.
|
||||
|
||||
Call `getInfo(id)` to grab the artist/album info. This will return the
|
||||
text description.
|
||||
|
||||
The basic format for art in the cache is `<musicbrainzid>.<date>.<ext>`
|
||||
and for info it is `<musicbrainzid>.<date>.txt`
|
||||
"""
|
||||
|
||||
path_to_art_cache = os.path.join(plexpy.CONFIG.CACHE_DIR, 'artwork')
|
||||
|
||||
def __init__(self):
|
||||
self.id = None
|
||||
self.id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed
|
||||
self.query_type = None # 'artwork','thumb' or 'info' - set automatically
|
||||
|
||||
self.artwork_files = []
|
||||
self.thumb_files = []
|
||||
|
||||
self.artwork_errors = False
|
||||
self.artwork_url = None
|
||||
|
||||
self.thumb_errors = False
|
||||
self.thumb_url = None
|
||||
|
||||
self.info_summary = None
|
||||
self.info_content = None
|
||||
|
||||
def _findfilesstartingwith(self, pattern, folder):
|
||||
files = []
|
||||
if os.path.exists(folder):
|
||||
for fname in os.listdir(folder):
|
||||
if fname.startswith(pattern):
|
||||
files.append(os.path.join(folder, fname))
|
||||
return files
|
||||
|
||||
def _exists(self, type):
|
||||
self.artwork_files = []
|
||||
self.thumb_files = []
|
||||
|
||||
if type == 'artwork':
|
||||
self.artwork_files = self._findfilesstartingwith(self.id, self.path_to_art_cache)
|
||||
if self.artwork_files:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
elif type == 'thumb':
|
||||
self.thumb_files = self._findfilesstartingwith("T_" + self.id, self.path_to_art_cache)
|
||||
if self.thumb_files:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _get_age(self, date):
|
||||
# There's probably a better way to do this
|
||||
split_date = date.split('-')
|
||||
days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2])
|
||||
|
||||
return days_old
|
||||
|
||||
def _is_current(self, filename=None, date=None):
|
||||
|
||||
if filename:
|
||||
base_filename = os.path.basename(filename)
|
||||
date = base_filename.split('.')[1]
|
||||
|
||||
# Calculate how old the cached file is based on todays date & file date stamp
|
||||
# helpers.today() returns todays date in yyyy-mm-dd format
|
||||
if self._get_age(helpers.today()) - self._get_age(date) < 30:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _get_thumb_url(self, data):
|
||||
|
||||
thumb_url = None
|
||||
|
||||
try:
|
||||
images = data[self.id_type]['image']
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
for image in images:
|
||||
if image['size'] == 'medium':
|
||||
thumb_url = image['#text']
|
||||
break
|
||||
|
||||
return thumb_url
|
||||
|
||||
def get_artwork_from_cache(self, ArtistID=None, AlbumID=None):
|
||||
"""
|
||||
Pass a musicbrainz id to this function (either ArtistID or AlbumID)
|
||||
"""
|
||||
|
||||
self.query_type = 'artwork'
|
||||
|
||||
if ArtistID:
|
||||
self.id = ArtistID
|
||||
self.id_type = 'artist'
|
||||
else:
|
||||
self.id = AlbumID
|
||||
self.id_type = 'album'
|
||||
|
||||
if self._exists('artwork') and self._is_current(filename=self.artwork_files[0]):
|
||||
return self.artwork_files[0]
|
||||
else:
|
||||
self._update_cache()
|
||||
# If we failed to get artwork, either return the url or the older file
|
||||
if self.artwork_errors and self.artwork_url:
|
||||
return self.artwork_url
|
||||
elif self._exists('artwork'):
|
||||
return self.artwork_files[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_thumb_from_cache(self, ArtistID=None, AlbumID=None):
|
||||
"""
|
||||
Pass a musicbrainz id to this function (either ArtistID or AlbumID)
|
||||
"""
|
||||
|
||||
self.query_type = 'thumb'
|
||||
|
||||
if ArtistID:
|
||||
self.id = ArtistID
|
||||
self.id_type = 'artist'
|
||||
else:
|
||||
self.id = AlbumID
|
||||
self.id_type = 'album'
|
||||
|
||||
if self._exists('thumb') and self._is_current(filename=self.thumb_files[0]):
|
||||
return self.thumb_files[0]
|
||||
else:
|
||||
self._update_cache()
|
||||
# If we failed to get artwork, either return the url or the older file
|
||||
if self.thumb_errors and self.thumb_url:
|
||||
return self.thumb_url
|
||||
elif self._exists('thumb'):
|
||||
return self.thumb_files[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_info_from_cache(self, ArtistID=None, AlbumID=None):
|
||||
|
||||
self.query_type = 'info'
|
||||
myDB = db.DBConnection()
|
||||
|
||||
if ArtistID:
|
||||
self.id = ArtistID
|
||||
self.id_type = 'artist'
|
||||
db_info = myDB.action('SELECT Summary, Content, LastUpdated FROM descriptions WHERE ArtistID=?', [self.id]).fetchone()
|
||||
else:
|
||||
self.id = AlbumID
|
||||
self.id_type = 'album'
|
||||
db_info = myDB.action('SELECT Summary, Content, LastUpdated FROM descriptions WHERE ReleaseGroupID=?', [self.id]).fetchone()
|
||||
|
||||
if not db_info or not db_info['LastUpdated'] or not self._is_current(date=db_info['LastUpdated']):
|
||||
|
||||
self._update_cache()
|
||||
info_dict = {'Summary': self.info_summary, 'Content': self.info_content}
|
||||
return info_dict
|
||||
|
||||
else:
|
||||
info_dict = {'Summary': db_info['Summary'], 'Content': db_info['Content']}
|
||||
return info_dict
|
||||
|
||||
def get_image_links(self, ArtistID=None, AlbumID=None):
|
||||
"""
|
||||
Here we're just going to open up the last.fm url, grab the image links and return them
|
||||
Won't save any image urls, or save the artwork in the cache. Useful for search results, etc.
|
||||
"""
|
||||
if ArtistID:
|
||||
|
||||
self.id_type = 'artist'
|
||||
data = lastfm.request_lastfm("artist.getinfo", mbid=ArtistID, api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
try:
|
||||
image_url = data['artist']['image'][-1]['#text']
|
||||
except (KeyError, IndexError):
|
||||
logger.debug('No artist image found')
|
||||
image_url = None
|
||||
|
||||
thumb_url = self._get_thumb_url(data)
|
||||
if not thumb_url:
|
||||
logger.debug('No artist thumbnail image found')
|
||||
|
||||
else:
|
||||
|
||||
self.id_type = 'album'
|
||||
data = lastfm.request_lastfm("album.getinfo", mbid=AlbumID, api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
try:
|
||||
image_url = data['album']['image'][-1]['#text']
|
||||
except (KeyError, IndexError):
|
||||
logger.debug('No album image found on last.fm')
|
||||
image_url = None
|
||||
|
||||
thumb_url = self._get_thumb_url(data)
|
||||
|
||||
if not thumb_url:
|
||||
logger.debug('No album thumbnail image found on last.fm')
|
||||
|
||||
return {'artwork': image_url, 'thumbnail': thumb_url}
|
||||
|
||||
def remove_from_cache(self, ArtistID=None, AlbumID=None):
|
||||
"""
|
||||
Pass a musicbrainz id to this function (either ArtistID or AlbumID)
|
||||
"""
|
||||
|
||||
if ArtistID:
|
||||
self.id = ArtistID
|
||||
self.id_type = 'artist'
|
||||
else:
|
||||
self.id = AlbumID
|
||||
self.id_type = 'album'
|
||||
|
||||
self.query_type = 'artwork'
|
||||
|
||||
if self._exists('artwork'):
|
||||
for artwork_file in self.artwork_files:
|
||||
try:
|
||||
os.remove(artwork_file)
|
||||
except:
|
||||
logger.warn('Error deleting file from the cache: %s', artwork_file)
|
||||
|
||||
self.query_type = 'thumb'
|
||||
|
||||
if self._exists('thumb'):
|
||||
for thumb_file in self.thumb_files:
|
||||
try:
|
||||
os.remove(thumb_file)
|
||||
except Exception:
|
||||
logger.warn('Error deleting file from the cache: %s', thumb_file)
|
||||
|
||||
def _update_cache(self):
|
||||
"""
|
||||
Since we call the same url for both info and artwork, we'll update both at the same time
|
||||
"""
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
# Since lastfm uses release ids rather than release group ids for albums, we have to do a artist + album search for albums
|
||||
# Exception is when adding albums manually, then we should use release id
|
||||
if self.id_type == 'artist':
|
||||
|
||||
data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
try:
|
||||
self.info_summary = data['artist']['bio']['summary']
|
||||
except KeyError:
|
||||
logger.debug('No artist bio summary found')
|
||||
self.info_summary = None
|
||||
try:
|
||||
self.info_content = data['artist']['bio']['content']
|
||||
except KeyError:
|
||||
logger.debug('No artist bio found')
|
||||
self.info_content = None
|
||||
try:
|
||||
image_url = data['artist']['image'][-1]['#text']
|
||||
except KeyError:
|
||||
logger.debug('No artist image found')
|
||||
image_url = None
|
||||
|
||||
thumb_url = self._get_thumb_url(data)
|
||||
if not thumb_url:
|
||||
logger.debug('No artist thumbnail image found')
|
||||
|
||||
else:
|
||||
dbalbum = myDB.action('SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?', [self.id]).fetchone()
|
||||
if dbalbum['ReleaseID'] != self.id:
|
||||
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'], api_key=LASTFM_API_KEY)
|
||||
if not data:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
||||
else:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
try:
|
||||
self.info_summary = data['album']['wiki']['summary']
|
||||
except KeyError:
|
||||
logger.debug('No album summary found')
|
||||
self.info_summary = None
|
||||
try:
|
||||
self.info_content = data['album']['wiki']['content']
|
||||
except KeyError:
|
||||
logger.debug('No album infomation found')
|
||||
self.info_content = None
|
||||
try:
|
||||
image_url = data['album']['image'][-1]['#text']
|
||||
except KeyError:
|
||||
logger.debug('No album image link found')
|
||||
image_url = None
|
||||
|
||||
thumb_url = self._get_thumb_url(data)
|
||||
|
||||
if not thumb_url:
|
||||
logger.debug('No album thumbnail image found')
|
||||
|
||||
# Save the content & summary to the database no matter what if we've
|
||||
# opened up the url
|
||||
if self.id_type == 'artist':
|
||||
controlValueDict = {"ArtistID": self.id}
|
||||
else:
|
||||
controlValueDict = {"ReleaseGroupID": self.id}
|
||||
|
||||
newValueDict = {"Summary": self.info_summary,
|
||||
"Content": self.info_content,
|
||||
"LastUpdated": helpers.today()}
|
||||
|
||||
myDB.upsert("descriptions", newValueDict, controlValueDict)
|
||||
|
||||
# Save the image URL to the database
|
||||
if image_url:
|
||||
if self.id_type == 'artist':
|
||||
myDB.action('UPDATE artists SET ArtworkURL=? WHERE ArtistID=?', [image_url, self.id])
|
||||
else:
|
||||
myDB.action('UPDATE albums SET ArtworkURL=? WHERE AlbumID=?', [image_url, self.id])
|
||||
|
||||
# Save the thumb URL to the database
|
||||
if thumb_url:
|
||||
if self.id_type == 'artist':
|
||||
myDB.action('UPDATE artists SET ThumbURL=? WHERE ArtistID=?', [thumb_url, self.id])
|
||||
else:
|
||||
myDB.action('UPDATE albums SET ThumbURL=? WHERE AlbumID=?', [thumb_url, self.id])
|
||||
|
||||
# Should we grab the artwork here if we're just grabbing thumbs or
|
||||
# info? Probably not since the files can be quite big
|
||||
if image_url and self.query_type == 'artwork':
|
||||
artwork = request.request_content(image_url, timeout=20)
|
||||
|
||||
if artwork:
|
||||
# Make sure the artwork dir exists:
|
||||
if not os.path.isdir(self.path_to_art_cache):
|
||||
try:
|
||||
os.makedirs(self.path_to_art_cache)
|
||||
os.chmod(self.path_to_art_cache, int(plexpy.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
except OSError as e:
|
||||
logger.error('Unable to create artwork cache dir. Error: %s', e)
|
||||
self.artwork_errors = True
|
||||
self.artwork_url = image_url
|
||||
|
||||
# Delete the old stuff
|
||||
for artwork_file in self.artwork_files:
|
||||
try:
|
||||
os.remove(artwork_file)
|
||||
except:
|
||||
logger.error('Error deleting file from the cache: %s', artwork_file)
|
||||
|
||||
ext = os.path.splitext(image_url)[1]
|
||||
|
||||
artwork_path = os.path.join(self.path_to_art_cache, self.id + '.' + helpers.today() + ext)
|
||||
try:
|
||||
with open(artwork_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
|
||||
os.chmod(artwork_path, int(plexpy.CONFIG.FILE_PERMISSIONS, 8))
|
||||
except (OSError, IOError) as e:
|
||||
logger.error('Unable to write to the cache dir: %s', e)
|
||||
self.artwork_errors = True
|
||||
self.artwork_url = image_url
|
||||
|
||||
# Grab the thumbnail as well if we're getting the full artwork (as long
|
||||
# as it's missing/outdated.
|
||||
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (self.thumb_files and self._is_current(self.thumb_files[0])):
|
||||
artwork = request.request_content(thumb_url, timeout=20)
|
||||
|
||||
if artwork:
|
||||
# Make sure the artwork dir exists:
|
||||
if not os.path.isdir(self.path_to_art_cache):
|
||||
try:
|
||||
os.makedirs(self.path_to_art_cache)
|
||||
os.chmod(self.path_to_art_cache, int(plexpy.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
except OSError as e:
|
||||
logger.error('Unable to create artwork cache dir. Error: %s' + e)
|
||||
self.thumb_errors = True
|
||||
self.thumb_url = thumb_url
|
||||
|
||||
# Delete the old stuff
|
||||
for thumb_file in self.thumb_files:
|
||||
try:
|
||||
os.remove(thumb_file)
|
||||
except OSError as e:
|
||||
logger.error('Error deleting file from the cache: %s', thumb_file)
|
||||
|
||||
ext = os.path.splitext(image_url)[1]
|
||||
|
||||
thumb_path = os.path.join(self.path_to_art_cache, 'T_' + self.id + '.' + helpers.today() + ext)
|
||||
try:
|
||||
with open(thumb_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
|
||||
os.chmod(thumb_path, int(plexpy.CONFIG.FILE_PERMISSIONS, 8))
|
||||
except (OSError, IOError) as e:
|
||||
logger.error('Unable to write to the cache dir: %s', e)
|
||||
self.thumb_errors = True
|
||||
self.thumb_url = image_url
|
||||
|
||||
|
||||
def getArtwork(ArtistID=None, AlbumID=None):
|
||||
c = Cache()
|
||||
artwork_path = c.get_artwork_from_cache(ArtistID, AlbumID)
|
||||
|
||||
if not artwork_path:
|
||||
return None
|
||||
|
||||
if artwork_path.startswith('http://'):
|
||||
return artwork_path
|
||||
else:
|
||||
artwork_file = os.path.basename(artwork_path)
|
||||
return "cache/artwork/" + artwork_file
|
||||
|
||||
|
||||
def getThumb(ArtistID=None, AlbumID=None):
|
||||
c = Cache()
|
||||
artwork_path = c.get_thumb_from_cache(ArtistID, AlbumID)
|
||||
|
||||
if not artwork_path:
|
||||
return None
|
||||
|
||||
if artwork_path.startswith('http://'):
|
||||
return artwork_path
|
||||
else:
|
||||
thumbnail_file = os.path.basename(artwork_path)
|
||||
return "cache/artwork/" + thumbnail_file
|
||||
|
||||
|
||||
def getInfo(ArtistID=None, AlbumID=None):
|
||||
c = Cache()
|
||||
info_dict = c.get_info_from_cache(ArtistID, AlbumID)
|
||||
|
||||
return info_dict
|
||||
|
||||
|
||||
def getImageLinks(ArtistID=None, AlbumID=None):
|
||||
c = Cache()
|
||||
image_links = c.get_image_links(ArtistID, AlbumID)
|
||||
|
||||
return image_links
|
67
plexpy/classes.py
Normal file
67
plexpy/classes.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#########################################
|
||||
## Stolen from Sick-Beard's classes.py ##
|
||||
#########################################
|
||||
|
||||
|
||||
import urllib
|
||||
|
||||
from common import USER_AGENT
|
||||
|
||||
|
||||
class PlexPyURLopener(urllib.FancyURLopener):
|
||||
version = USER_AGENT
|
||||
|
||||
|
||||
class AuthURLOpener(PlexPyURLopener):
|
||||
"""
|
||||
URLOpener class that supports http auth without needing interactive password entry.
|
||||
If the provided username/password don't work it simply fails.
|
||||
|
||||
user: username to use for HTTP auth
|
||||
pw: password to use for HTTP auth
|
||||
"""
|
||||
|
||||
def __init__(self, user, pw):
|
||||
self.username = user
|
||||
self.password = pw
|
||||
|
||||
# remember if we've tried the username/password before
|
||||
self.numTries = 0
|
||||
|
||||
# call the base class
|
||||
urllib.FancyURLopener.__init__(self)
|
||||
|
||||
def prompt_user_passwd(self, host, realm):
|
||||
"""
|
||||
Override this function and instead of prompting just give the
|
||||
username/password that were provided when the class was instantiated.
|
||||
"""
|
||||
|
||||
# if this is the first try then provide a username/password
|
||||
if self.numTries == 0:
|
||||
self.numTries = 1
|
||||
return (self.username, self.password)
|
||||
|
||||
# if we've tried before then return blank which cancels the request
|
||||
else:
|
||||
return ('', '')
|
||||
|
||||
# this is pretty much just a hack for convenience
|
||||
def openit(self, url):
|
||||
self.numTries = 0
|
||||
return PlexPyURLopener.open(self, url)
|
48
plexpy/common.py
Normal file
48
plexpy/common.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
Created on Aug 1, 2011
|
||||
|
||||
@author: Michael
|
||||
'''
|
||||
import platform
|
||||
import operator
|
||||
import os
|
||||
import re
|
||||
|
||||
from plexpy import version
|
||||
|
||||
#Identify Our Application
|
||||
USER_AGENT = 'PlexPy/-' + version.PLEXPY_VERSION + ' (' + platform.system() + ' ' + platform.release() + ')'
|
||||
|
||||
### Notification Types
|
||||
NOTIFY_SNATCH = 1
|
||||
NOTIFY_DOWNLOAD = 2
|
||||
|
||||
notifyStrings = {}
|
||||
notifyStrings[NOTIFY_SNATCH] = "Started Download"
|
||||
notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished"
|
||||
|
||||
### Release statuses
|
||||
UNKNOWN = -1 # should never happen
|
||||
UNAIRED = 1 # releases that haven't dropped yet
|
||||
SNATCHED = 2 # qualified with quality
|
||||
WANTED = 3 # releases we don't have but want to get
|
||||
DOWNLOADED = 4 # qualified with quality
|
||||
SKIPPED = 5 # releases we don't want
|
||||
ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats)
|
||||
IGNORED = 7 # releases that you don't want included in your download stats
|
||||
SNATCHED_PROPER = 9 # qualified with quality
|
209
plexpy/config.py
Normal file
209
plexpy/config.py
Normal file
|
@ -0,0 +1,209 @@
|
|||
import plexpy.logger
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
from configobj import ConfigObj
|
||||
|
||||
|
||||
def bool_int(value):
|
||||
"""
|
||||
Casts a config value into a 0 or 1
|
||||
"""
|
||||
if isinstance(value, basestring):
|
||||
if value.lower() in ('', '0', 'false', 'f', 'no', 'n', 'off'):
|
||||
value = 0
|
||||
return int(bool(value))
|
||||
|
||||
|
||||
|
||||
_CONFIG_DEFINITIONS = {
|
||||
'DATE_FORMAT': (str, 'General', 'YYYY-MM-DD'),
|
||||
'GROUPING_GLOBAL_HISTORY': (int, 'PlexWatch', 0),
|
||||
'GROUPING_USER_HISTORY': (int, 'PlexWatch', 0),
|
||||
'GROUPING_CHARTS': (int, 'PlexWatch', 0),
|
||||
'PLEXWATCH_DATABASE': (str, 'PlexWatch', ''),
|
||||
'PMS_IP': (str, 'PMS', '127.0.0.1'),
|
||||
'PMS_PORT': (int, 'PMS', 32400),
|
||||
'PMS_PASSWORD': (str, 'PMS', ''),
|
||||
'PMS_USERNAME': (str, 'PMS', ''),
|
||||
'TIME_FORMAT': (str, 'General', 'HH:mm'),
|
||||
'API_ENABLED': (int, 'General', 0),
|
||||
'API_KEY': (str, 'General', ''),
|
||||
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
|
||||
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
|
||||
'CACHE_DIR': (str, 'General', ''),
|
||||
'CACHE_SIZEMB': (int, 'Advanced', 32),
|
||||
'CHECK_GITHUB': (int, 'General', 1),
|
||||
'CHECK_GITHUB_INTERVAL': (int, 'General', 360),
|
||||
'CHECK_GITHUB_ON_STARTUP': (int, 'General', 1),
|
||||
'CLEANUP_FILES': (int, 'General', 0),
|
||||
'CONFIG_VERSION': (str, 'General', '0'),
|
||||
'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0),
|
||||
'EMAIL_ENABLED': (int, 'Email', 0),
|
||||
'EMAIL_FROM': (str, 'Email', ''),
|
||||
'EMAIL_TO': (str, 'Email', ''),
|
||||
'EMAIL_SMTP_SERVER': (str, 'Email', ''),
|
||||
'EMAIL_SMTP_USER': (str, 'Email', ''),
|
||||
'EMAIL_SMTP_PASSWORD': (str, 'Email', ''),
|
||||
'EMAIL_SMTP_PORT': (int, 'Email', 25),
|
||||
'EMAIL_TLS': (int, 'Email', 0),
|
||||
'ENABLE_HTTPS': (int, 'General', 0),
|
||||
'FREEZE_DB': (int, 'General', 0),
|
||||
'GIT_BRANCH': (str, 'General', 'master'),
|
||||
'GIT_PATH': (str, 'General', ''),
|
||||
'GIT_USER': (str, 'General', 'drzoidberg33'),
|
||||
'GROWL_ENABLED': (int, 'Growl', 0),
|
||||
'GROWL_HOST': (str, 'Growl', ''),
|
||||
'GROWL_PASSWORD': (str, 'Growl', ''),
|
||||
'HTTPS_CERT': (str, 'General', ''),
|
||||
'HTTPS_KEY': (str, 'General', ''),
|
||||
'HTTP_HOST': (str, 'General', '0.0.0.0'),
|
||||
'HTTP_PASSWORD': (str, 'General', ''),
|
||||
'HTTP_PORT': (int, 'General', 8181),
|
||||
'HTTP_PROXY': (int, 'General', 0),
|
||||
'HTTP_ROOT': (str, 'General', '/'),
|
||||
'HTTP_USERNAME': (str, 'General', ''),
|
||||
'INTERFACE': (str, 'General', 'default'),
|
||||
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
||||
'LAUNCH_BROWSER': (int, 'General', 1),
|
||||
'LMS_ENABLED': (int, 'LMS', 0),
|
||||
'LMS_HOST': (str, 'LMS', ''),
|
||||
'LOG_DIR': (str, 'General', ''),
|
||||
'MPC_ENABLED': (bool_int, 'MPC', False),
|
||||
'NMA_APIKEY': (str, 'NMA', ''),
|
||||
'NMA_ENABLED': (int, 'NMA', 0),
|
||||
'NMA_PRIORITY': (int, 'NMA', 0),
|
||||
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/PlexPy'),
|
||||
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
|
||||
'PLEX_CLIENT_HOST': (str, 'Plex', ''),
|
||||
'PLEX_ENABLED': (int, 'Plex', 0),
|
||||
'PLEX_PASSWORD': (str, 'Plex', ''),
|
||||
'PLEX_USERNAME': (str, 'Plex', ''),
|
||||
'PROWL_ENABLED': (int, 'Prowl', 0),
|
||||
'PROWL_KEYS': (str, 'Prowl', ''),
|
||||
'PROWL_PRIORITY': (int, 'Prowl', 0),
|
||||
'PUSHALOT_APIKEY': (str, 'Pushalot', ''),
|
||||
'PUSHALOT_ENABLED': (int, 'Pushalot', 0),
|
||||
'PUSHBULLET_APIKEY': (str, 'PushBullet', ''),
|
||||
'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''),
|
||||
'PUSHBULLET_ENABLED': (int, 'PushBullet', 0),
|
||||
'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
|
||||
'PUSHOVER_ENABLED': (int, 'Pushover', 0),
|
||||
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
||||
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
||||
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
|
||||
'SUBSONIC_HOST': (str, 'Subsonic', ''),
|
||||
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
|
||||
'SUBSONIC_USERNAME': (str, 'Subsonic', ''),
|
||||
'SYNOINDEX_ENABLED': (int, 'Synoindex', 0),
|
||||
'TWITTER_ENABLED': (int, 'Twitter', 0),
|
||||
'TWITTER_PASSWORD': (str, 'Twitter', ''),
|
||||
'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'),
|
||||
'TWITTER_USERNAME': (str, 'Twitter', ''),
|
||||
'UPDATE_DB_INTERVAL': (int, 'General', 24),
|
||||
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
|
||||
'XBMC_ENABLED': (int, 'XBMC', 0),
|
||||
'XBMC_HOST': (str, 'XBMC', ''),
|
||||
'XBMC_PASSWORD': (str, 'XBMC', ''),
|
||||
'XBMC_USERNAME': (str, 'XBMC', '')
|
||||
}
|
||||
# pylint:disable=R0902
|
||||
# it might be nice to refactor for fewer instance variables
|
||||
class Config(object):
|
||||
""" Wraps access to particular values in a config file """
|
||||
|
||||
def __init__(self, config_file):
|
||||
""" Initialize the config with values from a file """
|
||||
self._config_file = config_file
|
||||
self._config = ConfigObj(self._config_file, encoding='utf-8')
|
||||
for key in _CONFIG_DEFINITIONS.keys():
|
||||
self.check_setting(key)
|
||||
|
||||
def _define(self, name):
|
||||
key = name.upper()
|
||||
ini_key = name.lower()
|
||||
definition = _CONFIG_DEFINITIONS[key]
|
||||
if len(definition) == 3:
|
||||
definition_type, section, default = definition
|
||||
else:
|
||||
definition_type, section, _, default = definition
|
||||
return key, definition_type, section, ini_key, default
|
||||
|
||||
def check_section(self, section):
|
||||
""" Check if INI section exists, if not create it """
|
||||
if section not in self._config:
|
||||
self._config[section] = {}
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def check_setting(self, key):
|
||||
""" Cast any value in the config to the right type or use the default """
|
||||
key, definition_type, section, ini_key, default = self._define(key)
|
||||
self.check_section(section)
|
||||
try:
|
||||
my_val = definition_type(self._config[section][ini_key])
|
||||
except Exception:
|
||||
my_val = definition_type(default)
|
||||
self._config[section][ini_key] = my_val
|
||||
return my_val
|
||||
|
||||
def write(self):
|
||||
""" Make a copy of the stored config and write it to the configured file """
|
||||
new_config = ConfigObj(encoding="UTF-8")
|
||||
new_config.filename = self._config_file
|
||||
|
||||
# first copy over everything from the old config, even if it is not
|
||||
# correctly defined to keep from losing data
|
||||
for key, subkeys in self._config.items():
|
||||
if key not in new_config:
|
||||
new_config[key] = {}
|
||||
for subkey, value in subkeys.items():
|
||||
new_config[key][subkey] = value
|
||||
|
||||
# next make sure that everything we expect to have defined is so
|
||||
for key in _CONFIG_DEFINITIONS.keys():
|
||||
key, definition_type, section, ini_key, default = self._define(key)
|
||||
self.check_setting(key)
|
||||
if section not in new_config:
|
||||
new_config[section] = {}
|
||||
new_config[section][ini_key] = self._config[section][ini_key]
|
||||
|
||||
# Write it to file
|
||||
plexpy.logger.info("Writing configuration to file")
|
||||
|
||||
try:
|
||||
new_config.write()
|
||||
except IOError as e:
|
||||
plexpy.logger.error("Error writing configuration file: %s", e)
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Returns something from the ini unless it is a real property
|
||||
of the configuration object or is not all caps.
|
||||
"""
|
||||
if not re.match(r'[A-Z_]+$', name):
|
||||
return super(Config, self).__getattr__(name)
|
||||
else:
|
||||
return self.check_setting(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""
|
||||
Maps all-caps properties to ini values unless they exist on the
|
||||
configuration object.
|
||||
"""
|
||||
if not re.match(r'[A-Z_]+$', name):
|
||||
super(Config, self).__setattr__(name, value)
|
||||
return value
|
||||
else:
|
||||
key, definition_type, section, ini_key, default = self._define(name)
|
||||
self._config[section][ini_key] = definition_type(value)
|
||||
return self._config[section][ini_key]
|
||||
|
||||
def process_kwargs(self, kwargs):
|
||||
"""
|
||||
Given a big bunch of key value pairs, apply them to the ini.
|
||||
"""
|
||||
for name, value in kwargs.items():
|
||||
key, definition_type, section, ini_key, default = self._define(name)
|
||||
self._config[section][ini_key] = definition_type(value)
|
126
plexpy/db.py
Normal file
126
plexpy/db.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#####################################
|
||||
## Stolen from Sick-Beard's db.py ##
|
||||
#####################################
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
import plexpy
|
||||
|
||||
from plexpy import logger
|
||||
|
||||
|
||||
def dbFilename(filename):
|
||||
|
||||
return os.path.join(plexpy.DATA_DIR, filename)
|
||||
|
||||
|
||||
def getCacheSize():
|
||||
#this will protect against typecasting problems produced by empty string and None settings
|
||||
if not plexpy.CONFIG.CACHE_SIZEMB:
|
||||
#sqlite will work with this (very slowly)
|
||||
return 0
|
||||
return int(plexpy.CONFIG.CACHE_SIZEMB)
|
||||
|
||||
|
||||
class DBConnection:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.filename = plexpy.CONFIG.PLEXWATCH_DATABASE
|
||||
#self.connection = sqlite3.connect(dbFilename(plexpy.CONFIG.PLEXWATCH_DATABASE), timeout=20)
|
||||
self.connection = sqlite3.connect(plexpy.CONFIG.PLEXWATCH_DATABASE, timeout=20)
|
||||
#don't wait for the disk to finish writing
|
||||
self.connection.execute("PRAGMA synchronous = OFF")
|
||||
#journal disabled since we never do rollbacks
|
||||
self.connection.execute("PRAGMA journal_mode = %s" % plexpy.CONFIG.JOURNAL_MODE)
|
||||
#64mb of cache memory,probably need to make it user configurable
|
||||
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize() * 1024))
|
||||
self.connection.row_factory = sqlite3.Row
|
||||
|
||||
def action(self, query, args=None):
|
||||
|
||||
if query is None:
|
||||
return
|
||||
|
||||
sqlResult = None
|
||||
|
||||
try:
|
||||
with self.connection as c:
|
||||
if args is None:
|
||||
sqlResult = c.execute(query)
|
||||
else:
|
||||
sqlResult = c.execute(query, args)
|
||||
|
||||
except sqlite3.OperationalError, e:
|
||||
if "unable to open database file" in e.message or "database is locked" in e.message:
|
||||
logger.warn('Database Error: %s', e)
|
||||
else:
|
||||
logger.error('Database error: %s', e)
|
||||
raise
|
||||
|
||||
except sqlite3.DatabaseError, e:
|
||||
logger.error('Fatal Error executing %s :: %s', query, e)
|
||||
raise
|
||||
|
||||
return sqlResult
|
||||
|
||||
def select(self, query, args=None):
|
||||
|
||||
sqlResults = self.action(query, args).fetchall()
|
||||
|
||||
if sqlResults is None or sqlResults == [None]:
|
||||
return []
|
||||
|
||||
return sqlResults
|
||||
|
||||
def get_history_table_name(self):
|
||||
|
||||
if plexpy.CONFIG.GROUPING_GLOBAL_HISTORY:
|
||||
return "grouped"
|
||||
else:
|
||||
return "processed"
|
||||
|
||||
def get_user_table_name(self):
|
||||
|
||||
if plexpy.CONFIG.GROUPING_USER_HISTORY:
|
||||
return "grouped"
|
||||
else:
|
||||
return "processed"
|
||||
|
||||
def upsert(self, tableName, valueDict, keyDict):
|
||||
|
||||
changesBefore = self.connection.total_changes
|
||||
|
||||
genParams = lambda myDict: [x + " = ?" for x in myDict.keys()]
|
||||
|
||||
update_query = "UPDATE " + tableName + " SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
|
||||
|
||||
self.action(update_query, valueDict.values() + keyDict.values())
|
||||
|
||||
if self.connection.total_changes == changesBefore:
|
||||
insert_query = (
|
||||
"INSERT INTO " + tableName + " (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" +
|
||||
" VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")"
|
||||
)
|
||||
try:
|
||||
self.action(insert_query, valueDict.values() + keyDict.values())
|
||||
except sqlite3.IntegrityError:
|
||||
logger.info('Queries failed: %s and %s', update_query, insert_query)
|
21
plexpy/exceptions.py
Normal file
21
plexpy/exceptions.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class PlexPyException(Exception):
|
||||
"""
|
||||
Generic PlexPy Exception - should never be thrown, only subclassed
|
||||
"""
|
||||
|
327
plexpy/helpers.py
Normal file
327
plexpy/helpers.py
Normal file
|
@ -0,0 +1,327 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
import unicodedata
|
||||
import plexpy
|
||||
import datetime
|
||||
import fnmatch
|
||||
import shutil
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
|
||||
def multikeysort(items, columns):
|
||||
comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
|
||||
|
||||
def comparer(left, right):
|
||||
for fn, mult in comparers:
|
||||
result = cmp(fn(left), fn(right))
|
||||
if result:
|
||||
return mult * result
|
||||
else:
|
||||
return 0
|
||||
|
||||
return sorted(items, cmp=comparer)
|
||||
|
||||
|
||||
def checked(variable):
|
||||
if variable:
|
||||
return 'Checked'
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def radio(variable, pos):
|
||||
|
||||
if variable == pos:
|
||||
return 'Checked'
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def latinToAscii(unicrap):
|
||||
"""
|
||||
From couch potato
|
||||
"""
|
||||
xlate = {
|
||||
0xc0: 'A', 0xc1: 'A', 0xc2: 'A', 0xc3: 'A', 0xc4: 'A', 0xc5: 'A',
|
||||
0xc6: 'Ae', 0xc7: 'C',
|
||||
0xc8: 'E', 0xc9: 'E', 0xca: 'E', 0xcb: 'E', 0x86: 'e',
|
||||
0xcc: 'I', 0xcd: 'I', 0xce: 'I', 0xcf: 'I',
|
||||
0xd0: 'Th', 0xd1: 'N',
|
||||
0xd2: 'O', 0xd3: 'O', 0xd4: 'O', 0xd5: 'O', 0xd6: 'O', 0xd8: 'O',
|
||||
0xd9: 'U', 0xda: 'U', 0xdb: 'U', 0xdc: 'U',
|
||||
0xdd: 'Y', 0xde: 'th', 0xdf: 'ss',
|
||||
0xe0: 'a', 0xe1: 'a', 0xe2: 'a', 0xe3: 'a', 0xe4: 'a', 0xe5: 'a',
|
||||
0xe6: 'ae', 0xe7: 'c',
|
||||
0xe8: 'e', 0xe9: 'e', 0xea: 'e', 0xeb: 'e', 0x0259: 'e',
|
||||
0xec: 'i', 0xed: 'i', 0xee: 'i', 0xef: 'i',
|
||||
0xf0: 'th', 0xf1: 'n',
|
||||
0xf2: 'o', 0xf3: 'o', 0xf4: 'o', 0xf5: 'o', 0xf6: 'o', 0xf8: 'o',
|
||||
0xf9: 'u', 0xfa: 'u', 0xfb: 'u', 0xfc: 'u',
|
||||
0xfd: 'y', 0xfe: 'th', 0xff: 'y',
|
||||
0xa1: '!', 0xa2: '{cent}', 0xa3: '{pound}', 0xa4: '{currency}',
|
||||
0xa5: '{yen}', 0xa6: '|', 0xa7: '{section}', 0xa8: '{umlaut}',
|
||||
0xa9: '{C}', 0xaa: '{^a}', 0xab: '<<', 0xac: '{not}',
|
||||
0xad: '-', 0xae: '{R}', 0xaf: '_', 0xb0: '{degrees}',
|
||||
0xb1: '{+/-}', 0xb2: '{^2}', 0xb3: '{^3}', 0xb4: "'",
|
||||
0xb5: '{micro}', 0xb6: '{paragraph}', 0xb7: '*', 0xb8: '{cedilla}',
|
||||
0xb9: '{^1}', 0xba: '{^o}', 0xbb: '>>',
|
||||
0xbc: '{1/4}', 0xbd: '{1/2}', 0xbe: '{3/4}', 0xbf: '?',
|
||||
0xd7: '*', 0xf7: '/'
|
||||
}
|
||||
|
||||
r = ''
|
||||
for i in unicrap:
|
||||
if ord(i) in xlate:
|
||||
r += xlate[ord(i)]
|
||||
elif ord(i) >= 0x80:
|
||||
pass
|
||||
else:
|
||||
r += str(i)
|
||||
return r
|
||||
|
||||
|
||||
def convert_milliseconds(ms):
|
||||
|
||||
seconds = ms / 1000
|
||||
gmtime = time.gmtime(seconds)
|
||||
if seconds > 3600:
|
||||
minutes = time.strftime("%H:%M:%S", gmtime)
|
||||
else:
|
||||
minutes = time.strftime("%M:%S", gmtime)
|
||||
|
||||
return minutes
|
||||
|
||||
|
||||
def convert_seconds(s):
|
||||
|
||||
gmtime = time.gmtime(s)
|
||||
if s > 3600:
|
||||
minutes = time.strftime("%H:%M:%S", gmtime)
|
||||
else:
|
||||
minutes = time.strftime("%M:%S", gmtime)
|
||||
|
||||
return minutes
|
||||
|
||||
|
||||
def today():
|
||||
today = datetime.date.today()
|
||||
yyyymmdd = datetime.date.isoformat(today)
|
||||
return yyyymmdd
|
||||
|
||||
|
||||
def now():
|
||||
now = datetime.datetime.now()
|
||||
return now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def get_age(date):
|
||||
|
||||
try:
|
||||
split_date = date.split('-')
|
||||
except:
|
||||
return False
|
||||
|
||||
try:
|
||||
days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2])
|
||||
except IndexError:
|
||||
days_old = False
|
||||
|
||||
return days_old
|
||||
|
||||
|
||||
def bytes_to_mb(bytes):
|
||||
|
||||
mb = int(bytes) / 1048576
|
||||
size = '%.1f MB' % mb
|
||||
return size
|
||||
|
||||
|
||||
def mb_to_bytes(mb_str):
|
||||
result = re.search('^(\d+(?:\.\d+)?)\s?(?:mb)?', mb_str, flags=re.I)
|
||||
if result:
|
||||
return int(float(result.group(1)) * 1048576)
|
||||
|
||||
|
||||
def piratesize(size):
|
||||
split = size.split(" ")
|
||||
factor = float(split[0])
|
||||
unit = split[1].upper()
|
||||
|
||||
if unit == 'MiB':
|
||||
size = factor * 1048576
|
||||
elif unit == 'MB':
|
||||
size = factor * 1000000
|
||||
elif unit == 'GiB':
|
||||
size = factor * 1073741824
|
||||
elif unit == 'GB':
|
||||
size = factor * 1000000000
|
||||
elif unit == 'KiB':
|
||||
size = factor * 1024
|
||||
elif unit == 'KB':
|
||||
size = factor * 1000
|
||||
elif unit == "B":
|
||||
size = factor
|
||||
else:
|
||||
size = 0
|
||||
|
||||
return size
|
||||
|
||||
|
||||
def replace_all(text, dic, normalize=False):
|
||||
|
||||
if not text:
|
||||
return ''
|
||||
|
||||
for i, j in dic.iteritems():
|
||||
if normalize:
|
||||
try:
|
||||
if sys.platform == 'darwin':
|
||||
j = unicodedata.normalize('NFD', j)
|
||||
else:
|
||||
j = unicodedata.normalize('NFC', j)
|
||||
except TypeError:
|
||||
j = unicodedata.normalize('NFC', j.decode(plexpy.SYS_ENCODING, 'replace'))
|
||||
text = text.replace(i, j)
|
||||
return text
|
||||
|
||||
|
||||
def replace_illegal_chars(string, type="file"):
|
||||
if type == "file":
|
||||
string = re.sub('[\?"*:|<>/]', '_', string)
|
||||
if type == "folder":
|
||||
string = re.sub('[:\?<>"|]', '_', string)
|
||||
|
||||
return string
|
||||
|
||||
|
||||
def cleanName(string):
|
||||
|
||||
pass1 = latinToAscii(string).lower()
|
||||
out_string = re.sub('[\.\-\/\!\@\#\$\%\^\&\*\(\)\+\-\"\'\,\;\:\[\]\{\}\<\>\=\_]', '', pass1).encode('utf-8')
|
||||
|
||||
return out_string
|
||||
|
||||
|
||||
def cleanTitle(title):
|
||||
|
||||
title = re.sub('[\.\-\/\_]', ' ', title).lower()
|
||||
|
||||
# Strip out extra whitespace
|
||||
title = ' '.join(title.split())
|
||||
|
||||
title = title.title()
|
||||
|
||||
return title
|
||||
|
||||
|
||||
def split_path(f):
|
||||
"""
|
||||
Split a path into components, starting with the drive letter (if any). Given
|
||||
a path, os.path.join(*split_path(f)) should be path equal to f.
|
||||
"""
|
||||
|
||||
components = []
|
||||
drive, path = os.path.splitdrive(f)
|
||||
|
||||
# Strip the folder from the path, iterate until nothing is left
|
||||
while True:
|
||||
path, folder = os.path.split(path)
|
||||
|
||||
if folder:
|
||||
components.append(folder)
|
||||
else:
|
||||
if path:
|
||||
components.append(path)
|
||||
|
||||
break
|
||||
|
||||
# Append the drive (if any)
|
||||
if drive:
|
||||
components.append(drive)
|
||||
|
||||
# Reverse components
|
||||
components.reverse()
|
||||
|
||||
# Done
|
||||
return components
|
||||
|
||||
|
||||
def extract_logline(s):
|
||||
# Default log format
|
||||
pattern = re.compile(r'(?P<timestamp>.*?)\s\-\s(?P<level>.*?)\s*\:\:\s(?P<thread>.*?)\s\:\s(?P<message>.*)', re.VERBOSE)
|
||||
match = pattern.match(s)
|
||||
if match:
|
||||
timestamp = match.group("timestamp")
|
||||
level = match.group("level")
|
||||
thread = match.group("thread")
|
||||
message = match.group("message")
|
||||
return (timestamp, level, thread, message)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def split_string(mystring, splitvar=','):
|
||||
mylist = []
|
||||
for each_word in mystring.split(splitvar):
|
||||
mylist.append(each_word.strip())
|
||||
return mylist
|
||||
|
||||
def create_https_certificates(ssl_cert, ssl_key):
|
||||
"""
|
||||
Create a pair of self-signed HTTPS certificares and store in them in
|
||||
'ssl_cert' and 'ssl_key'. Method assumes pyOpenSSL is installed.
|
||||
|
||||
This code is stolen from SickBeard (http://github.com/midgetspy/Sick-Beard).
|
||||
"""
|
||||
|
||||
from plexpy import logger
|
||||
|
||||
from OpenSSL import crypto
|
||||
from certgen import createKeyPair, createCertRequest, createCertificate, \
|
||||
TYPE_RSA, serial
|
||||
|
||||
# Create the CA Certificate
|
||||
cakey = createKeyPair(TYPE_RSA, 2048)
|
||||
careq = createCertRequest(cakey, CN="Certificate Authority")
|
||||
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
|
||||
pkey = createKeyPair(TYPE_RSA, 2048)
|
||||
req = createCertRequest(pkey, CN="PlexPy")
|
||||
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
|
||||
# Save the key and certificate to disk
|
||||
try:
|
||||
with open(ssl_key, "w") as fp:
|
||||
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
with open(ssl_cert, "w") as fp:
|
||||
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
except IOError as e:
|
||||
logger.error("Error creating SSL key and certificate: %s", e)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def cast_to_float(s):
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
return -1
|
87
plexpy/lock.py
Normal file
87
plexpy/lock.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
"""
|
||||
Locking-related classes
|
||||
"""
|
||||
|
||||
import plexpy.logger
|
||||
import time
|
||||
import threading
|
||||
import Queue
|
||||
|
||||
|
||||
class TimedLock(object):
|
||||
"""
|
||||
Enforce request rate limit if applicable. This uses the lock so there
|
||||
is synchronized access to the API. When N threads enter this method, the
|
||||
first will pass trough, since there there was no last request recorded.
|
||||
The last request time will be set. Then, the second thread will unlock,
|
||||
and see that the last request was X seconds ago. It will sleep
|
||||
(request_limit - X) seconds, and then continue. Then the third one will
|
||||
unblock, and so on. After all threads finish, the total time will at
|
||||
least be (N * request_limit) seconds. If some request takes longer than
|
||||
request_limit seconds, the next unblocked thread will wait less.
|
||||
"""
|
||||
|
||||
def __init__(self, minimum_delta=0):
|
||||
"""
|
||||
Set up the lock
|
||||
"""
|
||||
self.lock = threading.Lock()
|
||||
self.last_used = 0
|
||||
self.minimum_delta = minimum_delta
|
||||
self.queue = Queue.Queue()
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Called when with lock: is invoked
|
||||
"""
|
||||
self.lock.acquire()
|
||||
delta = time.time() - self.last_used
|
||||
sleep_amount = self.minimum_delta - delta
|
||||
if sleep_amount >= 0:
|
||||
# zero sleeps give the cpu a chance to task-switch
|
||||
plexpy.logger.debug('Sleeping %s (interval)', sleep_amount)
|
||||
time.sleep(sleep_amount)
|
||||
while not self.queue.empty():
|
||||
try:
|
||||
seconds = self.queue.get(False)
|
||||
plexpy.logger.debug('Sleeping %s (queued)', seconds)
|
||||
time.sleep(seconds)
|
||||
except Queue.Empty:
|
||||
continue
|
||||
self.queue.task_done()
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
"""
|
||||
Called when exiting the with block.
|
||||
"""
|
||||
self.last_used = time.time()
|
||||
self.lock.release()
|
||||
|
||||
def snooze(self, seconds):
|
||||
"""
|
||||
Asynchronously add time to the next request. Can be called outside
|
||||
of the lock context, but it is possible for the next lock holder
|
||||
to not check the queue until after something adds time to it.
|
||||
"""
|
||||
# We use a queue so that we don't have to synchronize
|
||||
# across threads and with or without locks
|
||||
plexpy.logger.info('Adding %s to queue', seconds)
|
||||
self.queue.put(seconds)
|
||||
|
||||
|
||||
class FakeLock(object):
|
||||
"""
|
||||
If no locking or request throttling is needed, use this
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Do nothing on enter
|
||||
"""
|
||||
pass
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
"""
|
||||
Do nothing on exit
|
||||
"""
|
||||
pass
|
226
plexpy/logger.py
Normal file
226
plexpy/logger.py
Normal file
|
@ -0,0 +1,226 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from plexpy import helpers
|
||||
|
||||
from logutils.queue import QueueHandler, QueueListener
|
||||
from logging import handlers
|
||||
|
||||
import multiprocessing
|
||||
import contextlib
|
||||
import plexpy
|
||||
import threading
|
||||
import traceback
|
||||
import logging
|
||||
import errno
|
||||
import sys
|
||||
import os
|
||||
|
||||
# These settings are for file logging only
|
||||
FILENAME = "plexpy.log"
|
||||
MAX_SIZE = 1000000 # 1 MB
|
||||
MAX_FILES = 5
|
||||
|
||||
# PlexPy logger
|
||||
logger = logging.getLogger("plexpy")
|
||||
|
||||
# Global queue for multiprocessing logging
|
||||
queue = None
|
||||
|
||||
class LogListHandler(logging.Handler):
|
||||
"""
|
||||
Log handler for Web UI.
|
||||
"""
|
||||
|
||||
def emit(self, record):
|
||||
message = self.format(record)
|
||||
message = message.replace("\n", "<br />")
|
||||
|
||||
plexpy.LOG_LIST.insert(0, (helpers.now(), message, record.levelname, record.threadName))
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def listener():
|
||||
"""
|
||||
Wrapper that create a QueueListener, starts it and automatically stops it.
|
||||
To be used in a with statement in the main process, for multiprocessing.
|
||||
"""
|
||||
|
||||
global queue
|
||||
|
||||
# Initialize queue if not already done
|
||||
if queue is None:
|
||||
try:
|
||||
queue = multiprocessing.Queue()
|
||||
except OSError as e:
|
||||
queue = False
|
||||
|
||||
# Some machines don't have access to /dev/shm. See
|
||||
# http://stackoverflow.com/questions/2009278 for more information.
|
||||
if e.errno == errno.EACCES:
|
||||
logger.warning('Multiprocess logging disabled, because '
|
||||
'current user cannot map shared memory. You won\'t see any' \
|
||||
'logging generated by the worker processed.')
|
||||
|
||||
# Multiprocess logging may be disabled.
|
||||
if not queue:
|
||||
yield
|
||||
else:
|
||||
queue_listener = QueueListener(queue, *logger.handlers)
|
||||
|
||||
try:
|
||||
queue_listener.start()
|
||||
yield
|
||||
finally:
|
||||
queue_listener.stop()
|
||||
|
||||
|
||||
def initMultiprocessing():
|
||||
"""
|
||||
Remove all handlers and add QueueHandler on top. This should only be called
|
||||
inside a multiprocessing worker process, since it changes the logger
|
||||
completely.
|
||||
"""
|
||||
|
||||
# Multiprocess logging may be disabled.
|
||||
if not queue:
|
||||
return
|
||||
|
||||
# Remove all handlers and add the Queue handler as the only one.
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
|
||||
queue_handler = QueueHandler(queue)
|
||||
queue_handler.setLevel(logging.DEBUG)
|
||||
|
||||
logger.addHandler(queue_handler)
|
||||
|
||||
# Change current thread name for log record
|
||||
threading.current_thread().name = multiprocessing.current_process().name
|
||||
|
||||
|
||||
def initLogger(console=False, log_dir=False, verbose=False):
|
||||
"""
|
||||
Setup logging for PlexPy. It uses the logger instance with the name
|
||||
'plexpy'. Three log handlers are added:
|
||||
|
||||
* RotatingFileHandler: for the file plexpy.log
|
||||
* LogListHandler: for Web UI
|
||||
* StreamHandler: for console (if console)
|
||||
|
||||
Console logging is only enabled if console is set to True. This method can
|
||||
be invoked multiple times, during different stages of PlexPy.
|
||||
"""
|
||||
|
||||
# Close and remove old handlers. This is required to reinit the loggers
|
||||
# at runtime
|
||||
for handler in logger.handlers[:]:
|
||||
# Just make sure it is cleaned up.
|
||||
if isinstance(handler, handlers.RotatingFileHandler):
|
||||
handler.close()
|
||||
elif isinstance(handler, logging.StreamHandler):
|
||||
handler.flush()
|
||||
|
||||
logger.removeHandler(handler)
|
||||
|
||||
# Configure the logger to accept all messages
|
||||
logger.propagate = False
|
||||
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
||||
|
||||
# Add list logger
|
||||
loglist_handler = LogListHandler()
|
||||
loglist_handler.setLevel(logging.DEBUG)
|
||||
|
||||
logger.addHandler(loglist_handler)
|
||||
|
||||
# Setup file logger
|
||||
if log_dir:
|
||||
filename = os.path.join(log_dir, FILENAME)
|
||||
|
||||
file_formatter = logging.Formatter('%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# Setup console logger
|
||||
if console:
|
||||
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(console_formatter)
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# Install exception hooks
|
||||
initHooks()
|
||||
|
||||
|
||||
def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True):
|
||||
"""
|
||||
This method installs exception catching mechanisms. Any exception caught
|
||||
will pass through the exception hook, and will be logged to the logger as
|
||||
an error. Additionally, a traceback is provided.
|
||||
|
||||
This is very useful for crashing threads and any other bugs, that may not
|
||||
be exposed when running as daemon.
|
||||
|
||||
The default exception hook is still considered, if pass_original is True.
|
||||
"""
|
||||
|
||||
def excepthook(*exception_info):
|
||||
# We should always catch this to prevent loops!
|
||||
try:
|
||||
message = "".join(traceback.format_exception(*exception_info))
|
||||
logger.error("Uncaught exception: %s", message)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Original excepthook
|
||||
if pass_original:
|
||||
sys.__excepthook__(*exception_info)
|
||||
|
||||
# Global exception hook
|
||||
if global_exceptions:
|
||||
sys.excepthook = excepthook
|
||||
|
||||
# Thread exception hook
|
||||
if thread_exceptions:
|
||||
old_init = threading.Thread.__init__
|
||||
|
||||
def new_init(self, *args, **kwargs):
|
||||
old_init(self, *args, **kwargs)
|
||||
old_run = self.run
|
||||
|
||||
def new_run(*args, **kwargs):
|
||||
try:
|
||||
old_run(*args, **kwargs)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
excepthook(*sys.exc_info())
|
||||
self.run = new_run
|
||||
|
||||
# Monkey patch the run() by monkey patching the __init__ method
|
||||
threading.Thread.__init__ = new_init
|
||||
|
||||
# Expose logger methods
|
||||
info = logger.info
|
||||
warn = logger.warn
|
||||
error = logger.error
|
||||
debug = logger.debug
|
||||
warning = logger.warning
|
||||
exception = logger.exception
|
862
plexpy/notifiers.py
Normal file
862
plexpy/notifiers.py
Normal file
|
@ -0,0 +1,862 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from plexpy import logger, helpers, common, request
|
||||
|
||||
from xml.dom import minidom
|
||||
from httplib import HTTPSConnection
|
||||
from urlparse import parse_qsl
|
||||
from urllib import urlencode
|
||||
from pynma import pynma
|
||||
|
||||
import base64
|
||||
import cherrypy
|
||||
import urllib
|
||||
import urllib2
|
||||
import plexpy
|
||||
import os.path
|
||||
import subprocess
|
||||
import gntp.notifier
|
||||
import json
|
||||
|
||||
import oauth2 as oauth
|
||||
import pythontwitter as twitter
|
||||
|
||||
from email.mime.text import MIMEText
|
||||
import smtplib
|
||||
import email.utils
|
||||
|
||||
|
||||
class GROWL(object):
|
||||
"""
|
||||
Growl notifications, for OS X.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = plexpy.CONFIG.GROWL_ENABLED
|
||||
self.host = plexpy.CONFIG.GROWL_HOST
|
||||
self.password = plexpy.CONFIG.GROWL_PASSWORD
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('Growl', options)
|
||||
|
||||
def notify(self, message, event):
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
# Split host and port
|
||||
if self.host == "":
|
||||
host, port = "localhost", 23053
|
||||
if ":" in self.host:
|
||||
host, port = self.host.split(':', 1)
|
||||
port = int(port)
|
||||
else:
|
||||
host, port = self.host, 23053
|
||||
|
||||
# If password is empty, assume none
|
||||
if self.password == "":
|
||||
password = None
|
||||
else:
|
||||
password = self.password
|
||||
|
||||
# Register notification
|
||||
growl = gntp.notifier.GrowlNotifier(
|
||||
applicationName='PlexPy',
|
||||
notifications=['New Event'],
|
||||
defaultNotifications=['New Event'],
|
||||
hostname=host,
|
||||
port=port,
|
||||
password=password
|
||||
)
|
||||
|
||||
try:
|
||||
growl.register()
|
||||
except gntp.notifier.errors.NetworkError:
|
||||
logger.warning(u'Growl notification failed: network error')
|
||||
return
|
||||
except gntp.notifier.errors.AuthError:
|
||||
logger.warning(u'Growl notification failed: authentication error')
|
||||
return
|
||||
|
||||
# Fix message
|
||||
message = message.encode(plexpy.SYS_ENCODING, "replace")
|
||||
|
||||
# Send it, including an image
|
||||
image_file = os.path.join(str(plexpy.PROG_DIR),
|
||||
"data/images/plexpylogo.png")
|
||||
|
||||
with open(image_file, 'rb') as f:
|
||||
image = f.read()
|
||||
|
||||
try:
|
||||
growl.notify(
|
||||
noteType='New Event',
|
||||
title=event,
|
||||
description=message,
|
||||
icon=image
|
||||
)
|
||||
except gntp.notifier.errors.NetworkError:
|
||||
logger.warning(u'Growl notification failed: network error')
|
||||
return
|
||||
|
||||
logger.info(u"Growl notifications sent.")
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, host, password):
|
||||
self.enabled = True
|
||||
self.host = host
|
||||
self.password = password
|
||||
|
||||
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
|
||||
|
||||
|
||||
class PROWL(object):
|
||||
"""
|
||||
Prowl notifications.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = plexpy.CONFIG.PROWL_ENABLED
|
||||
self.keys = plexpy.CONFIG.PROWL_KEYS
|
||||
self.priority = plexpy.CONFIG.PROWL_PRIORITY
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('Prowl', options)
|
||||
|
||||
def notify(self, message, event):
|
||||
if not plexpy.CONFIG.PROWL_ENABLED:
|
||||
return
|
||||
|
||||
http_handler = HTTPSConnection("api.prowlapp.com")
|
||||
|
||||
data = {'apikey': plexpy.CONFIG.PROWL_KEYS,
|
||||
'application': 'PlexPy',
|
||||
'event': event,
|
||||
'description': message.encode("utf-8"),
|
||||
'priority': plexpy.CONFIG.PROWL_PRIORITY}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/publicapi/add",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"Prowl notifications sent.")
|
||||
return True
|
||||
elif request_status == 401:
|
||||
logger.info(u"Prowl auth failed: %s" % response.reason)
|
||||
return False
|
||||
else:
|
||||
logger.info(u"Prowl notification failed.")
|
||||
return False
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, keys, priority):
|
||||
self.enabled = True
|
||||
self.keys = keys
|
||||
self.priority = priority
|
||||
|
||||
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
|
||||
|
||||
|
||||
class MPC(object):
|
||||
"""
|
||||
MPC library update
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
pass
|
||||
|
||||
def notify(self):
|
||||
subprocess.call(["mpc", "update"])
|
||||
|
||||
|
||||
class XBMC(object):
|
||||
"""
|
||||
XBMC notifications
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.hosts = plexpy.CONFIG.XBMC_HOST
|
||||
self.username = plexpy.CONFIG.XBMC_USERNAME
|
||||
self.password = plexpy.CONFIG.XBMC_PASSWORD
|
||||
|
||||
def _sendhttp(self, host, command):
|
||||
url_command = urllib.urlencode(command)
|
||||
url = host + '/xbmcCmds/xbmcHttp/?' + url_command
|
||||
|
||||
if self.password:
|
||||
return request.request_content(url, auth=(self.username, self.password))
|
||||
else:
|
||||
return request.request_content(url)
|
||||
|
||||
def _sendjson(self, host, method, params={}):
|
||||
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
url = host + '/jsonrpc'
|
||||
|
||||
if self.password:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers, auth=(self.username, self.password))
|
||||
else:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers)
|
||||
|
||||
if response:
|
||||
return response[0]['result']
|
||||
|
||||
def update(self):
|
||||
# From what I read you can't update the music library on a per directory or per path basis
|
||||
# so need to update the whole thing
|
||||
|
||||
hosts = [x.strip() for x in self.hosts.split(',')]
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending library update command to XBMC @ ' + host)
|
||||
request = self._sendjson(host, 'AudioLibrary.Scan')
|
||||
|
||||
if not request:
|
||||
logger.warn('Error sending update request to XBMC')
|
||||
|
||||
def notify(self, artist, album, albumartpath):
|
||||
|
||||
hosts = [x.strip() for x in self.hosts.split(',')]
|
||||
|
||||
header = "PlexPy"
|
||||
message = "%s - %s added to your library" % (artist, album)
|
||||
time = "3000" # in ms
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending notification command to XMBC @ ' + host)
|
||||
try:
|
||||
version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major']
|
||||
|
||||
if version < 12: #Eden
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
|
||||
else: #Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time), 'image': albumartpath}
|
||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
||||
|
||||
if not request:
|
||||
raise Exception
|
||||
|
||||
except Exception:
|
||||
logger.error('Error sending notification request to XBMC')
|
||||
|
||||
|
||||
class LMS(object):
|
||||
"""
|
||||
Class for updating a Logitech Media Server
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.hosts = plexpy.CONFIG.LMS_HOST
|
||||
|
||||
def _sendjson(self, host):
|
||||
data = {'id': 1, 'method': 'slim.request', 'params': ["", ["rescan"]]}
|
||||
data = json.JSONEncoder().encode(data)
|
||||
|
||||
content = {'Content-Type': 'application/json'}
|
||||
|
||||
req = urllib2.Request(host + '/jsonrpc.js', data, content)
|
||||
|
||||
try:
|
||||
handle = urllib2.urlopen(req)
|
||||
except Exception as e:
|
||||
logger.warn('Error opening LMS url: %s' % e)
|
||||
return
|
||||
|
||||
response = json.JSONDecoder().decode(handle.read())
|
||||
|
||||
try:
|
||||
return response['result']
|
||||
except:
|
||||
logger.warn('LMS returned error: %s' % response['error'])
|
||||
return response['error']
|
||||
|
||||
def update(self):
|
||||
|
||||
hosts = [x.strip() for x in self.hosts.split(',')]
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending library rescan command to LMS @ ' + host)
|
||||
request = self._sendjson(host)
|
||||
|
||||
if request:
|
||||
logger.warn('Error sending rescan request to LMS')
|
||||
|
||||
|
||||
class Plex(object):
|
||||
def __init__(self):
|
||||
|
||||
self.server_hosts = plexpy.CONFIG.PLEX_SERVER_HOST
|
||||
self.client_hosts = plexpy.CONFIG.PLEX_CLIENT_HOST
|
||||
self.username = plexpy.CONFIG.PLEX_USERNAME
|
||||
self.password = plexpy.CONFIG.PLEX_PASSWORD
|
||||
|
||||
def _sendhttp(self, host, command):
|
||||
|
||||
username = self.username
|
||||
password = self.password
|
||||
|
||||
url_command = urllib.urlencode(command)
|
||||
|
||||
url = host + '/xbmcCmds/xbmcHttp/?' + url_command
|
||||
|
||||
req = urllib2.Request(url)
|
||||
|
||||
if password:
|
||||
base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
|
||||
req.add_header("Authorization", "Basic %s" % base64string)
|
||||
|
||||
logger.info('Plex url: %s' % url)
|
||||
|
||||
try:
|
||||
handle = urllib2.urlopen(req)
|
||||
except Exception as e:
|
||||
logger.warn('Error opening Plex url: %s' % e)
|
||||
return
|
||||
|
||||
response = handle.read().decode(plexpy.SYS_ENCODING)
|
||||
|
||||
return response
|
||||
|
||||
def update(self):
|
||||
|
||||
# From what I read you can't update the music library on a per directory or per path basis
|
||||
# so need to update the whole thing
|
||||
|
||||
hosts = [x.strip() for x in self.server_hosts.split(',')]
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending library update command to Plex Media Server@ ' + host)
|
||||
url = "%s/library/sections" % host
|
||||
try:
|
||||
xml_sections = minidom.parse(urllib.urlopen(url))
|
||||
except IOError, e:
|
||||
logger.warn("Error while trying to contact Plex Media Server: %s" % e)
|
||||
return False
|
||||
|
||||
sections = xml_sections.getElementsByTagName('Directory')
|
||||
if not sections:
|
||||
logger.info(u"Plex Media Server not running on: " + host)
|
||||
return False
|
||||
|
||||
for s in sections:
|
||||
if s.getAttribute('type') == "artist":
|
||||
url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key'))
|
||||
try:
|
||||
urllib.urlopen(url)
|
||||
except Exception as e:
|
||||
logger.warn("Error updating library section for Plex Media Server: %s" % e)
|
||||
return False
|
||||
|
||||
def notify(self, artist, album, albumartpath):
|
||||
|
||||
hosts = [x.strip() for x in self.client_hosts.split(',')]
|
||||
|
||||
header = "PlexPy"
|
||||
message = "%s - %s added to your library" % (artist, album)
|
||||
time = "3000" # in ms
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending notification command to Plex Media Server @ ' + host)
|
||||
try:
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
|
||||
if not request:
|
||||
raise Exception
|
||||
|
||||
except:
|
||||
logger.warn('Error sending notification request to Plex Media Server')
|
||||
|
||||
|
||||
class NMA(object):
|
||||
def notify(self, artist=None, album=None, snatched=None):
|
||||
title = 'PlexPy'
|
||||
api = plexpy.CONFIG.NMA_APIKEY
|
||||
nma_priority = plexpy.CONFIG.NMA_PRIORITY
|
||||
|
||||
logger.debug(u"NMA title: " + title)
|
||||
logger.debug(u"NMA API: " + api)
|
||||
logger.debug(u"NMA Priority: " + str(nma_priority))
|
||||
|
||||
if snatched:
|
||||
event = snatched + " snatched!"
|
||||
message = "PlexPy has snatched: " + snatched
|
||||
else:
|
||||
event = artist + ' - ' + album + ' complete!'
|
||||
message = "PlexPy has downloaded and postprocessed: " + artist + ' [' + album + ']'
|
||||
|
||||
logger.debug(u"NMA event: " + event)
|
||||
logger.debug(u"NMA message: " + message)
|
||||
|
||||
batch = False
|
||||
|
||||
p = pynma.PyNMA()
|
||||
keys = api.split(',')
|
||||
p.addkey(keys)
|
||||
|
||||
if len(keys) > 1:
|
||||
batch = True
|
||||
|
||||
response = p.push(title, event, message, priority=nma_priority, batch_mode=batch)
|
||||
|
||||
if not response[api][u'code'] == u'200':
|
||||
logger.error(u'Could not send notification to NotifyMyAndroid')
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class PUSHBULLET(object):
|
||||
|
||||
def __init__(self):
|
||||
self.apikey = plexpy.CONFIG.PUSHBULLET_APIKEY
|
||||
self.deviceid = plexpy.CONFIG.PUSHBULLET_DEVICEID
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('PUSHBULLET', options)
|
||||
|
||||
def notify(self, message, event):
|
||||
if not plexpy.CONFIG.PUSHBULLET_ENABLED:
|
||||
return
|
||||
|
||||
http_handler = HTTPSConnection("api.pushbullet.com")
|
||||
|
||||
data = {'type': "note",
|
||||
'title': "PlexPy",
|
||||
'body': message.encode("utf-8")}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/v2/pushes",
|
||||
headers={'Content-type': "application/json",
|
||||
'Authorization': 'Basic %s' % base64.b64encode(plexpy.CONFIG.PUSHBULLET_APIKEY + ":")},
|
||||
body=json.dumps(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
logger.debug(u"PushBullet response status: %r" % request_status)
|
||||
logger.debug(u"PushBullet response headers: %r" % response.getheaders())
|
||||
logger.debug(u"PushBullet response body: %r" % response.read())
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"PushBullet notifications sent.")
|
||||
return True
|
||||
elif request_status >= 400 and request_status < 500:
|
||||
logger.info(u"PushBullet request failed: %s" % response.reason)
|
||||
return False
|
||||
else:
|
||||
logger.info(u"PushBullet notification failed serverside.")
|
||||
return False
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, apikey, deviceid):
|
||||
|
||||
self.enabled = True
|
||||
self.apikey = apikey
|
||||
self.deviceid = deviceid
|
||||
|
||||
self.notify('Main Screen Activate', 'Test Message')
|
||||
|
||||
|
||||
class PUSHALOT(object):
|
||||
|
||||
def notify(self, message, event):
|
||||
if not plexpy.CONFIG.PUSHALOT_ENABLED:
|
||||
return
|
||||
|
||||
pushalot_authorizationtoken = plexpy.CONFIG.PUSHALOT_APIKEY
|
||||
|
||||
logger.debug(u"Pushalot event: " + event)
|
||||
logger.debug(u"Pushalot message: " + message)
|
||||
logger.debug(u"Pushalot api: " + pushalot_authorizationtoken)
|
||||
|
||||
http_handler = HTTPSConnection("pushalot.com")
|
||||
|
||||
data = {'AuthorizationToken': pushalot_authorizationtoken,
|
||||
'Title': event.encode('utf-8'),
|
||||
'Body': message.encode("utf-8")}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/api/sendmessage",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
|
||||
logger.debug(u"Pushalot response status: %r" % request_status)
|
||||
logger.debug(u"Pushalot response headers: %r" % response.getheaders())
|
||||
logger.debug(u"Pushalot response body: %r" % response.read())
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"Pushalot notifications sent.")
|
||||
return True
|
||||
elif request_status == 410:
|
||||
logger.info(u"Pushalot auth failed: %s" % response.reason)
|
||||
return False
|
||||
else:
|
||||
logger.info(u"Pushalot notification failed.")
|
||||
return False
|
||||
|
||||
|
||||
class Synoindex(object):
|
||||
def __init__(self, util_loc='/usr/syno/bin/synoindex'):
|
||||
self.util_loc = util_loc
|
||||
|
||||
def util_exists(self):
|
||||
return os.path.exists(self.util_loc)
|
||||
|
||||
def notify(self, path):
|
||||
path = os.path.abspath(path)
|
||||
|
||||
if not self.util_exists():
|
||||
logger.warn("Error sending notification: synoindex utility not found at %s" % self.util_loc)
|
||||
return
|
||||
|
||||
if os.path.isfile(path):
|
||||
cmd_arg = '-a'
|
||||
elif os.path.isdir(path):
|
||||
cmd_arg = '-A'
|
||||
else:
|
||||
logger.warn("Error sending notification: Path passed to synoindex was not a file or folder.")
|
||||
return
|
||||
|
||||
cmd = [self.util_loc, cmd_arg, path]
|
||||
logger.info("Calling synoindex command: %s" % str(cmd))
|
||||
try:
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=plexpy.PROG_DIR)
|
||||
out, error = p.communicate()
|
||||
#synoindex never returns any codes other than '0', highly irritating
|
||||
except OSError, e:
|
||||
logger.warn("Error sending notification: %s" % str(e))
|
||||
|
||||
def notify_multiple(self, path_list):
|
||||
if isinstance(path_list, list):
|
||||
for path in path_list:
|
||||
self.notify(path)
|
||||
|
||||
|
||||
class PUSHOVER(object):
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = plexpy.CONFIG.PUSHOVER_ENABLED
|
||||
self.keys = plexpy.CONFIG.PUSHOVER_KEYS
|
||||
self.priority = plexpy.CONFIG.PUSHOVER_PRIORITY
|
||||
|
||||
if plexpy.CONFIG.PUSHOVER_APITOKEN:
|
||||
self.application_token = plexpy.CONFIG.PUSHOVER_APITOKEN
|
||||
else:
|
||||
self.application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz"
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('Pushover', options)
|
||||
|
||||
def notify(self, message, event):
|
||||
if not plexpy.CONFIG.PUSHOVER_ENABLED:
|
||||
return
|
||||
|
||||
http_handler = HTTPSConnection("api.pushover.net")
|
||||
|
||||
data = {'token': self.application_token,
|
||||
'user': plexpy.CONFIG.PUSHOVER_KEYS,
|
||||
'title': event,
|
||||
'message': message.encode("utf-8"),
|
||||
'priority': plexpy.CONFIG.PUSHOVER_PRIORITY}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/1/messages.json",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
logger.debug(u"Pushover response status: %r" % request_status)
|
||||
logger.debug(u"Pushover response headers: %r" % response.getheaders())
|
||||
logger.debug(u"Pushover response body: %r" % response.read())
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"Pushover notifications sent.")
|
||||
return True
|
||||
elif request_status >= 400 and request_status < 500:
|
||||
logger.info(u"Pushover request failed: %s" % response.reason)
|
||||
return False
|
||||
else:
|
||||
logger.info(u"Pushover notification failed.")
|
||||
return False
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, keys, priority):
|
||||
self.enabled = True
|
||||
self.keys = keys
|
||||
self.priority = priority
|
||||
|
||||
self.notify('Main Screen Activate', 'Test Message')
|
||||
|
||||
|
||||
class TwitterNotifier(object):
|
||||
|
||||
REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token'
|
||||
ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token'
|
||||
AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize'
|
||||
SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate'
|
||||
|
||||
def __init__(self):
|
||||
self.consumer_key = "oYKnp2ddX5gbARjqX8ZAAg"
|
||||
self.consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk"
|
||||
|
||||
def notify_snatch(self, title):
|
||||
if plexpy.CONFIG.TWITTER_ONSNATCH:
|
||||
self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now())
|
||||
|
||||
def notify_download(self, title):
|
||||
if plexpy.CONFIG.TWITTER_ENABLED:
|
||||
self._notifyTwitter(common.notifyStrings[common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now())
|
||||
|
||||
def test_notify(self):
|
||||
return self._notifyTwitter("This is a test notification from PlexPy at " + helpers.now(), force=True)
|
||||
|
||||
def _get_authorization(self):
|
||||
|
||||
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
|
||||
oauth_client = oauth.Client(oauth_consumer)
|
||||
|
||||
logger.info('Requesting temp token from Twitter')
|
||||
|
||||
resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET')
|
||||
|
||||
if resp['status'] != '200':
|
||||
logger.info('Invalid respond from Twitter requesting temp token: %s' % resp['status'])
|
||||
else:
|
||||
request_token = dict(parse_qsl(content))
|
||||
|
||||
plexpy.CONFIG.TWITTER_USERNAME = request_token['oauth_token']
|
||||
plexpy.CONFIG.TWITTER_PASSWORD = request_token['oauth_token_secret']
|
||||
|
||||
return self.AUTHORIZATION_URL + "?oauth_token=" + request_token['oauth_token']
|
||||
|
||||
def _get_credentials(self, key):
|
||||
request_token = {}
|
||||
|
||||
request_token['oauth_token'] = plexpy.CONFIG.TWITTER_USERNAME
|
||||
request_token['oauth_token_secret'] = plexpy.CONFIG.TWITTER_PASSWORD
|
||||
request_token['oauth_callback_confirmed'] = 'true'
|
||||
|
||||
token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret'])
|
||||
token.set_verifier(key)
|
||||
|
||||
logger.info('Generating and signing request for an access token using key ' + key)
|
||||
|
||||
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
|
||||
logger.info('oauth_consumer: ' + str(oauth_consumer))
|
||||
oauth_client = oauth.Client(oauth_consumer, token)
|
||||
logger.info('oauth_client: ' + str(oauth_client))
|
||||
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key)
|
||||
logger.info('resp, content: ' + str(resp) + ',' + str(content))
|
||||
|
||||
access_token = dict(parse_qsl(content))
|
||||
logger.info('access_token: ' + str(access_token))
|
||||
|
||||
logger.info('resp[status] = ' + str(resp['status']))
|
||||
if resp['status'] != '200':
|
||||
logger.info('The request for a token with did not succeed: ' + str(resp['status']), logger.ERROR)
|
||||
return False
|
||||
else:
|
||||
logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token'])
|
||||
logger.info('Access Token secret: %s' % access_token['oauth_token_secret'])
|
||||
plexpy.CONFIG.TWITTER_USERNAME = access_token['oauth_token']
|
||||
plexpy.CONFIG.TWITTER_PASSWORD = access_token['oauth_token_secret']
|
||||
return True
|
||||
|
||||
def _send_tweet(self, message=None):
|
||||
|
||||
username = self.consumer_key
|
||||
password = self.consumer_secret
|
||||
access_token_key = plexpy.CONFIG.TWITTER_USERNAME
|
||||
access_token_secret = plexpy.CONFIG.TWITTER_PASSWORD
|
||||
|
||||
logger.info(u"Sending tweet: " + message)
|
||||
|
||||
api = twitter.Api(username, password, access_token_key, access_token_secret)
|
||||
|
||||
try:
|
||||
api.PostUpdate(message)
|
||||
except Exception as e:
|
||||
logger.info(u"Error Sending Tweet: %s" % e)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _notifyTwitter(self, message='', force=False):
|
||||
prefix = plexpy.CONFIG.TWITTER_PREFIX
|
||||
|
||||
if not plexpy.CONFIG.TWITTER_ENABLED and not force:
|
||||
return False
|
||||
|
||||
return self._send_tweet(prefix + ": " + message)
|
||||
|
||||
|
||||
class OSX_NOTIFY(object):
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.objc = __import__("objc")
|
||||
self.AppKit = __import__("AppKit")
|
||||
except:
|
||||
return False
|
||||
|
||||
def swizzle(self, cls, SEL, func):
|
||||
old_IMP = cls.instanceMethodForSelector_(SEL)
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
return func(self, old_IMP, *args, **kwargs)
|
||||
new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector,
|
||||
signature=old_IMP.signature)
|
||||
self.objc.classAddMethod(cls, SEL, new_IMP)
|
||||
|
||||
def notify(self, title, subtitle=None, text=None, sound=True, image=None):
|
||||
|
||||
try:
|
||||
self.swizzle(self.objc.lookUpClass('NSBundle'),
|
||||
b'bundleIdentifier',
|
||||
self.swizzled_bundleIdentifier)
|
||||
|
||||
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
|
||||
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
|
||||
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
|
||||
|
||||
if not NSUserNotification or not NSUserNotificationCenter:
|
||||
return False
|
||||
|
||||
pool = NSAutoreleasePool.alloc().init()
|
||||
|
||||
notification = NSUserNotification.alloc().init()
|
||||
notification.setTitle_(title)
|
||||
if subtitle:
|
||||
notification.setSubtitle_(subtitle)
|
||||
if text:
|
||||
notification.setInformativeText_(text)
|
||||
if sound:
|
||||
notification.setSoundName_("NSUserNotificationDefaultSoundName")
|
||||
if image:
|
||||
source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image)
|
||||
notification.setContentImage_(source_img)
|
||||
#notification.set_identityImage_(source_img)
|
||||
notification.setHasActionButton_(False)
|
||||
|
||||
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
||||
notification_center.deliverNotification_(notification)
|
||||
|
||||
del pool
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warn('Error sending OS X Notification: %s' % e)
|
||||
return False
|
||||
|
||||
def swizzled_bundleIdentifier(self, original, swizzled):
|
||||
return 'ade.plexpy.osxnotify'
|
||||
|
||||
|
||||
class BOXCAR(object):
|
||||
|
||||
def __init__(self):
|
||||
self.url = 'https://new.boxcar.io/api/notifications'
|
||||
|
||||
def notify(self, title, message, rgid=None):
|
||||
try:
|
||||
if rgid:
|
||||
message += '<br></br><a href="http://musicbrainz.org/release-group/%s">MusicBrainz</a>' % rgid
|
||||
|
||||
data = urllib.urlencode({
|
||||
'user_credentials': plexpy.CONFIG.BOXCAR_TOKEN,
|
||||
'notification[title]': title.encode('utf-8'),
|
||||
'notification[long_message]': message.encode('utf-8'),
|
||||
'notification[sound]': "done"
|
||||
})
|
||||
|
||||
req = urllib2.Request(self.url)
|
||||
handle = urllib2.urlopen(req, data)
|
||||
handle.close()
|
||||
return True
|
||||
|
||||
except urllib2.URLError as e:
|
||||
logger.warn('Error sending Boxcar2 Notification: %s' % e)
|
||||
return False
|
||||
|
||||
|
||||
class SubSonicNotifier(object):
|
||||
|
||||
def __init__(self):
|
||||
self.host = plexpy.CONFIG.SUBSONIC_HOST
|
||||
self.username = plexpy.CONFIG.SUBSONIC_USERNAME
|
||||
self.password = plexpy.CONFIG.SUBSONIC_PASSWORD
|
||||
|
||||
def notify(self, albumpaths):
|
||||
# Correct URL
|
||||
if not self.host.lower().startswith("http"):
|
||||
self.host = "http://" + self.host
|
||||
|
||||
if not self.host.lower().endswith("/"):
|
||||
self.host = self.host + "/"
|
||||
|
||||
# Invoke request
|
||||
request.request_response(self.host + "musicFolderSettings.view?scanNow",
|
||||
auth=(self.username, self.password))
|
||||
|
||||
class Email(object):
|
||||
|
||||
def notify(self, subject, message):
|
||||
|
||||
message = MIMEText(message, 'plain', "utf-8")
|
||||
message['Subject'] = subject
|
||||
message['From'] = email.utils.formataddr(('PlexPy', plexpy.CONFIG.EMAIL_FROM))
|
||||
message['To'] = plexpy.CONFIG.EMAIL_TO
|
||||
|
||||
try:
|
||||
mailserver = smtplib.SMTP(plexpy.CONFIG.EMAIL_SMTP_SERVER, plexpy.CONFIG.EMAIL_SMTP_PORT)
|
||||
|
||||
if (plexpy.CONFIG.EMAIL_TLS):
|
||||
mailserver.starttls()
|
||||
|
||||
mailserver.ehlo()
|
||||
|
||||
if plexpy.CONFIG.EMAIL_SMTP_USER:
|
||||
mailserver.login(plexpy.CONFIG.EMAIL_SMTP_USER, plexpy.CONFIG.EMAIL_SMTP_PASSWORD)
|
||||
|
||||
mailserver.sendmail(plexpy.CONFIG.EMAIL_FROM, plexpy.CONFIG.EMAIL_TO, message.as_string())
|
||||
mailserver.quit()
|
||||
return True
|
||||
|
||||
except Exception, e:
|
||||
logger.warn('Error sending Email: %s' % e)
|
||||
return False
|
237
plexpy/request.py
Normal file
237
plexpy/request.py
Normal file
|
@ -0,0 +1,237 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from plexpy import logger
|
||||
|
||||
from xml.dom import minidom
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import requests
|
||||
import feedparser
|
||||
import plexpy
|
||||
import plexpy.lock
|
||||
import collections
|
||||
|
||||
# Dictionary with last request times, for rate limiting.
|
||||
last_requests = collections.defaultdict(int)
|
||||
fake_lock = plexpy.lock.FakeLock()
|
||||
|
||||
|
||||
def request_response(url, method="get", auto_raise=True,
|
||||
whitelist_status_code=None, lock=fake_lock, **kwargs):
|
||||
"""
|
||||
Convenient wrapper for `requests.get', which will capture the exceptions
|
||||
and log them. On success, the Response object is returned. In case of a
|
||||
exception, None is returned.
|
||||
|
||||
Additionally, there is support for rate limiting. To use this feature,
|
||||
supply a tuple of (lock, request_limit). The lock is used to make sure no
|
||||
other request with the same lock is executed. The request limit is the
|
||||
minimal time between two requests (and so 1/request_limit is the number of
|
||||
requests per seconds).
|
||||
"""
|
||||
|
||||
# Convert whitelist_status_code to a list if needed
|
||||
if whitelist_status_code and type(whitelist_status_code) != list:
|
||||
whitelist_status_code = [whitelist_status_code]
|
||||
|
||||
# Disable verification of SSL certificates if requested. Note: this could
|
||||
# pose a security issue!
|
||||
kwargs["verify"] = bool(plexpy.CONFIG.VERIFY_SSL_CERT)
|
||||
|
||||
# Map method to the request.XXX method. This is a simple hack, but it
|
||||
# allows requests to apply more magic per method. See lib/requests/api.py.
|
||||
request_method = getattr(requests, method.lower())
|
||||
|
||||
try:
|
||||
# Request URL and wait for response
|
||||
with lock:
|
||||
logger.debug(
|
||||
"Requesting URL via %s method: %s", method.upper(), url)
|
||||
response = request_method(url, **kwargs)
|
||||
|
||||
# If status code != OK, then raise exception, except if the status code
|
||||
# is white listed.
|
||||
if whitelist_status_code and auto_raise:
|
||||
if response.status_code not in whitelist_status_code:
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except:
|
||||
logger.debug(
|
||||
"Response status code %d is not white "
|
||||
"listed, raised exception", response.status_code)
|
||||
raise
|
||||
elif auto_raise:
|
||||
response.raise_for_status()
|
||||
|
||||
return response
|
||||
except requests.exceptions.SSLError as e:
|
||||
if kwargs["verify"]:
|
||||
logger.error(
|
||||
"Unable to connect to remote host because of a SSL error. "
|
||||
"It is likely that your system cannot verify the validity"
|
||||
"of the certificate. The remote certificate is either "
|
||||
"self-signed, or the remote server uses SNI. See the wiki for "
|
||||
"more information on this topic.")
|
||||
else:
|
||||
logger.error(
|
||||
"SSL error raised during connection, with certificate "
|
||||
"verification turned off: %s", e)
|
||||
except requests.ConnectionError:
|
||||
logger.error(
|
||||
"Unable to connect to remote host. Check if the remote "
|
||||
"host is up and running.")
|
||||
except requests.Timeout:
|
||||
logger.error(
|
||||
"Request timed out. The remote host did not respond timely.")
|
||||
except requests.HTTPError as e:
|
||||
if e.response is not None:
|
||||
if e.response.status_code >= 500:
|
||||
cause = "remote server error"
|
||||
elif e.response.status_code >= 400:
|
||||
cause = "local client error"
|
||||
else:
|
||||
# I don't think we will end up here, but for completeness
|
||||
cause = "unknown"
|
||||
|
||||
logger.error(
|
||||
"Request raise HTTP error with status code %d (%s).",
|
||||
e.response.status_code, cause)
|
||||
|
||||
# Debug response
|
||||
if plexpy.VERBOSE:
|
||||
server_message(e.response)
|
||||
else:
|
||||
logger.error("Request raised HTTP error.")
|
||||
except requests.RequestException as e:
|
||||
logger.error("Request raised exception: %s", e)
|
||||
|
||||
|
||||
def request_soup(url, **kwargs):
|
||||
"""
|
||||
Wrapper for `request_response', which will return a BeatifulSoup object if
|
||||
no exceptions are raised.
|
||||
"""
|
||||
|
||||
parser = kwargs.pop("parser", "html5lib")
|
||||
response = request_response(url, **kwargs)
|
||||
|
||||
if response is not None:
|
||||
return BeautifulSoup(response.content, parser)
|
||||
|
||||
|
||||
def request_minidom(url, **kwargs):
|
||||
"""
|
||||
Wrapper for `request_response', which will return a Minidom object if no
|
||||
exceptions are raised.
|
||||
"""
|
||||
|
||||
response = request_response(url, **kwargs)
|
||||
|
||||
if response is not None:
|
||||
return minidom.parseString(response.content)
|
||||
|
||||
|
||||
def request_json(url, **kwargs):
|
||||
"""
|
||||
Wrapper for `request_response', which will decode the response as JSON
|
||||
object and return the result, if no exceptions are raised.
|
||||
|
||||
As an option, a validator callback can be given, which should return True
|
||||
if the result is valid.
|
||||
"""
|
||||
|
||||
validator = kwargs.pop("validator", None)
|
||||
response = request_response(url, **kwargs)
|
||||
|
||||
if response is not None:
|
||||
try:
|
||||
result = response.json()
|
||||
|
||||
if validator and not validator(result):
|
||||
logger.error("JSON validation result failed")
|
||||
else:
|
||||
return result
|
||||
except ValueError:
|
||||
logger.error("Response returned invalid JSON data")
|
||||
|
||||
# Debug response
|
||||
if plexpy.VERBOSE:
|
||||
server_message(response)
|
||||
|
||||
|
||||
def request_content(url, **kwargs):
|
||||
"""
|
||||
Wrapper for `request_response', which will return the raw content.
|
||||
"""
|
||||
|
||||
response = request_response(url, **kwargs)
|
||||
|
||||
if response is not None:
|
||||
return response.content
|
||||
|
||||
|
||||
def request_feed(url, **kwargs):
|
||||
"""
|
||||
Wrapper for `request_response', which will return a feed object.
|
||||
"""
|
||||
|
||||
response = request_response(url, **kwargs)
|
||||
|
||||
if response is not None:
|
||||
return feedparser.parse(response.content)
|
||||
|
||||
|
||||
def server_message(response):
|
||||
"""
|
||||
Extract server message from response and log in to logger with DEBUG level.
|
||||
|
||||
Some servers return extra information in the result. Try to parse it for
|
||||
debugging purpose. Messages are limited to 150 characters, since it may
|
||||
return the whole page in case of normal web page URLs
|
||||
"""
|
||||
|
||||
message = None
|
||||
|
||||
# First attempt is to 'read' the response as HTML
|
||||
if "text/html" in response.headers.get("content-type"):
|
||||
try:
|
||||
soup = BeautifulSoup(response.content, "html5lib")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Find body and cleanup common tags to grab content, which probably
|
||||
# contains the message.
|
||||
message = soup.find("body")
|
||||
elements = ("header", "script", "footer", "nav", "input", "textarea")
|
||||
|
||||
for element in elements:
|
||||
|
||||
for tag in soup.find_all(element):
|
||||
tag.replaceWith("")
|
||||
|
||||
message = message.text if message else soup.text
|
||||
message = message.strip()
|
||||
|
||||
# Second attempt is to just take the response
|
||||
if message is None:
|
||||
message = response.content.strip()
|
||||
|
||||
if message:
|
||||
# Truncate message if it is too long.
|
||||
if len(message) > 150:
|
||||
message = message[:150] + "..."
|
||||
|
||||
logger.debug("Server responded with message: %s", message)
|
1
plexpy/version.py
Normal file
1
plexpy/version.py
Normal file
|
@ -0,0 +1 @@
|
|||
PLEXPY_VERSION = "master"
|
243
plexpy/versioncheck.py
Normal file
243
plexpy/versioncheck.py
Normal file
|
@ -0,0 +1,243 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import os
|
||||
import tarfile
|
||||
import platform
|
||||
import plexpy
|
||||
import subprocess
|
||||
|
||||
from plexpy import logger, version, request
|
||||
|
||||
|
||||
def runGit(args):
|
||||
|
||||
if plexpy.CONFIG.GIT_PATH:
|
||||
git_locations = ['"' + plexpy.CONFIG.GIT_PATH + '"']
|
||||
else:
|
||||
git_locations = ['git']
|
||||
|
||||
if platform.system().lower() == 'darwin':
|
||||
git_locations.append('/usr/local/git/bin/git')
|
||||
|
||||
output = err = None
|
||||
|
||||
for cur_git in git_locations:
|
||||
cmd = cur_git + ' ' + args
|
||||
|
||||
try:
|
||||
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + plexpy.PROG_DIR)
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=plexpy.PROG_DIR)
|
||||
output, err = p.communicate()
|
||||
output = output.strip()
|
||||
|
||||
logger.debug('Git output: ' + output)
|
||||
except OSError:
|
||||
logger.debug('Command failed: %s', cmd)
|
||||
continue
|
||||
|
||||
if 'not found' in output or "not recognized as an internal or external command" in output:
|
||||
logger.debug('Unable to find git with command ' + cmd)
|
||||
output = None
|
||||
elif 'fatal:' in output or err:
|
||||
logger.error('Git returned bad info. Are you sure this is a git installation?')
|
||||
output = None
|
||||
elif output:
|
||||
break
|
||||
|
||||
return (output, err)
|
||||
|
||||
|
||||
def getVersion():
|
||||
|
||||
if version.PLEXPY_VERSION.startswith('win32build'):
|
||||
plexpy.INSTALL_TYPE = 'win'
|
||||
|
||||
# Don't have a way to update exe yet, but don't want to set VERSION to None
|
||||
return 'Windows Install', 'master'
|
||||
|
||||
elif os.path.isdir(os.path.join(plexpy.PROG_DIR, '.git')):
|
||||
|
||||
plexpy.INSTALL_TYPE = 'git'
|
||||
output, err = runGit('rev-parse HEAD')
|
||||
|
||||
if not output:
|
||||
logger.error('Couldn\'t find latest installed version.')
|
||||
cur_commit_hash = None
|
||||
|
||||
cur_commit_hash = str(output)
|
||||
|
||||
if not re.match('^[a-z0-9]+$', cur_commit_hash):
|
||||
logger.error('Output doesn\'t look like a hash, not using it')
|
||||
cur_commit_hash = None
|
||||
|
||||
if plexpy.CONFIG.DO_NOT_OVERRIDE_GIT_BRANCH and plexpy.CONFIG.GIT_BRANCH:
|
||||
branch_name = plexpy.CONFIG.GIT_BRANCH
|
||||
|
||||
else:
|
||||
branch_name, err = runGit('rev-parse --abbrev-ref HEAD')
|
||||
branch_name = branch_name
|
||||
|
||||
if not branch_name and plexpy.CONFIG.GIT_BRANCH:
|
||||
logger.error('Could not retrieve branch name from git. Falling back to %s' % plexpy.CONFIG.GIT_BRANCH)
|
||||
branch_name = plexpy.CONFIG.GIT_BRANCH
|
||||
if not branch_name:
|
||||
logger.error('Could not retrieve branch name from git. Defaulting to master')
|
||||
branch_name = 'master'
|
||||
|
||||
return cur_commit_hash, branch_name
|
||||
|
||||
else:
|
||||
|
||||
plexpy.INSTALL_TYPE = 'source'
|
||||
|
||||
version_file = os.path.join(plexpy.PROG_DIR, 'version.txt')
|
||||
|
||||
if not os.path.isfile(version_file):
|
||||
return None, 'master'
|
||||
|
||||
with open(version_file, 'r') as f:
|
||||
current_version = f.read().strip(' \n\r')
|
||||
|
||||
if current_version:
|
||||
return current_version, plexpy.CONFIG.GIT_BRANCH
|
||||
else:
|
||||
return None, 'master'
|
||||
|
||||
|
||||
def checkGithub():
|
||||
plexpy.COMMITS_BEHIND = 0
|
||||
|
||||
# Get the latest version available from github
|
||||
logger.info('Retrieving latest version information from GitHub')
|
||||
url = 'https://api.github.com/repos/%s/plexpy/commits/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_BRANCH)
|
||||
version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict)
|
||||
|
||||
if version is None:
|
||||
logger.warn('Could not get the latest version from GitHub. Are you running a local development version?')
|
||||
return plexpy.CURRENT_VERSION
|
||||
|
||||
plexpy.LATEST_VERSION = version['sha']
|
||||
logger.debug("Latest version is %s", plexpy.LATEST_VERSION)
|
||||
|
||||
# See how many commits behind we are
|
||||
if not plexpy.CURRENT_VERSION:
|
||||
logger.info('You are running an unknown version of PlexPy. Run the updater to identify your version')
|
||||
return plexpy.LATEST_VERSION
|
||||
|
||||
if plexpy.LATEST_VERSION == plexpy.CURRENT_VERSION:
|
||||
logger.info('PlexPy is up to date')
|
||||
return plexpy.LATEST_VERSION
|
||||
|
||||
logger.info('Comparing currently installed version with latest GitHub version')
|
||||
url = 'https://api.github.com/repos/%s/plexpy/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.LATEST_VERSION, plexpy.CURRENT_VERSION)
|
||||
commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
|
||||
|
||||
if commits is None:
|
||||
logger.warn('Could not get commits behind from GitHub.')
|
||||
return plexpy.LATEST_VERSION
|
||||
|
||||
try:
|
||||
plexpy.COMMITS_BEHIND = int(commits['behind_by'])
|
||||
logger.debug("In total, %d commits behind", plexpy.COMMITS_BEHIND)
|
||||
except KeyError:
|
||||
logger.info('Cannot compare versions. Are you running a local development version?')
|
||||
plexpy.COMMITS_BEHIND = 0
|
||||
|
||||
if plexpy.COMMITS_BEHIND > 0:
|
||||
logger.info('New version is available. You are %s commits behind' % plexpy.COMMITS_BEHIND)
|
||||
elif plexpy.COMMITS_BEHIND == 0:
|
||||
logger.info('PlexPy is up to date')
|
||||
|
||||
return plexpy.LATEST_VERSION
|
||||
|
||||
|
||||
def update():
|
||||
if plexpy.INSTALL_TYPE == 'win':
|
||||
logger.info('Windows .exe updating not supported yet.')
|
||||
|
||||
elif plexpy.INSTALL_TYPE == 'git':
|
||||
output, err = runGit('pull origin ' + plexpy.CONFIG.GIT_BRANCH)
|
||||
|
||||
if not output:
|
||||
logger.error('Couldn\'t download latest version')
|
||||
|
||||
for line in output.split('\n'):
|
||||
|
||||
if 'Already up-to-date.' in line:
|
||||
logger.info('No update available, not updating')
|
||||
logger.info('Output: ' + str(output))
|
||||
elif line.endswith('Aborting.'):
|
||||
logger.error('Unable to update from git: ' + line)
|
||||
logger.info('Output: ' + str(output))
|
||||
|
||||
else:
|
||||
tar_download_url = 'https://github.com/%s/plexpy/tarball/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_BRANCH)
|
||||
update_dir = os.path.join(plexpy.PROG_DIR, 'update')
|
||||
version_path = os.path.join(plexpy.PROG_DIR, 'version.txt')
|
||||
|
||||
logger.info('Downloading update from: ' + tar_download_url)
|
||||
data = request.request_content(tar_download_url)
|
||||
|
||||
if not data:
|
||||
logger.error("Unable to retrieve new version from '%s', can't update", tar_download_url)
|
||||
return
|
||||
|
||||
download_name = plexpy.CONFIG.GIT_BRANCH + '-github'
|
||||
tar_download_path = os.path.join(plexpy.PROG_DIR, download_name)
|
||||
|
||||
# Save tar to disk
|
||||
with open(tar_download_path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
# Extract the tar to update folder
|
||||
logger.info('Extracting file: ' + tar_download_path)
|
||||
tar = tarfile.open(tar_download_path)
|
||||
tar.extractall(update_dir)
|
||||
tar.close()
|
||||
|
||||
# Delete the tar.gz
|
||||
logger.info('Deleting file: ' + tar_download_path)
|
||||
os.remove(tar_download_path)
|
||||
|
||||
# Find update dir name
|
||||
update_dir_contents = [x for x in os.listdir(update_dir) if os.path.isdir(os.path.join(update_dir, x))]
|
||||
if len(update_dir_contents) != 1:
|
||||
logger.error("Invalid update data, update failed: " + str(update_dir_contents))
|
||||
return
|
||||
content_dir = os.path.join(update_dir, update_dir_contents[0])
|
||||
|
||||
# walk temp folder and move files to main folder
|
||||
for dirname, dirnames, filenames in os.walk(content_dir):
|
||||
dirname = dirname[len(content_dir) + 1:]
|
||||
for curfile in filenames:
|
||||
old_path = os.path.join(content_dir, dirname, curfile)
|
||||
new_path = os.path.join(plexpy.PROG_DIR, dirname, curfile)
|
||||
|
||||
if os.path.isfile(new_path):
|
||||
os.remove(new_path)
|
||||
os.renames(old_path, new_path)
|
||||
|
||||
# Update version.txt
|
||||
try:
|
||||
with open(version_path, 'w') as f:
|
||||
f.write(str(plexpy.LATEST_VERSION))
|
||||
except IOError as e:
|
||||
logger.error(
|
||||
"Unable to write current version to version.txt, update not complete: %s",
|
||||
e
|
||||
)
|
||||
return
|
423
plexpy/webserve.py
Normal file
423
plexpy/webserve.py
Normal file
|
@ -0,0 +1,423 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from plexpy import logger, db, helpers, notifiers
|
||||
from plexpy.helpers import checked, radio, today, cleanName
|
||||
from xml.dom import minidom
|
||||
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako import exceptions
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
import plexpy
|
||||
import threading
|
||||
import cherrypy
|
||||
import urllib2
|
||||
import hashlib
|
||||
import random
|
||||
import urllib
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
# pylint:disable=E0611
|
||||
# ignore this error because we are catching the ImportError
|
||||
from collections import OrderedDict
|
||||
# pylint:enable=E0611
|
||||
except ImportError:
|
||||
# Python 2.6.x fallback, from libs
|
||||
from ordereddict import OrderedDict
|
||||
|
||||
|
||||
def serve_template(templatename, **kwargs):
|
||||
|
||||
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
|
||||
template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.INTERFACE)
|
||||
|
||||
_hplookup = TemplateLookup(directories=[template_dir])
|
||||
|
||||
try:
|
||||
template = _hplookup.get_template(templatename)
|
||||
return template.render(**kwargs)
|
||||
except:
|
||||
return exceptions.html_error_template().render()
|
||||
|
||||
|
||||
class WebInterface(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
|
||||
@cherrypy.expose
|
||||
def home(self):
|
||||
return serve_template(templatename="index.html", title="Home")
|
||||
|
||||
@cherrypy.expose
|
||||
def history(self):
|
||||
if plexpy.CONFIG.DATE_FORMAT:
|
||||
date_format = plexpy.CONFIG.DATE_FORMAT
|
||||
else:
|
||||
date_format = 'YYYY-MM-DD'
|
||||
if plexpy.CONFIG.TIME_FORMAT:
|
||||
time_format = plexpy.CONFIG.TIME_FORMAT
|
||||
else:
|
||||
time_format = 'HH:mm'
|
||||
|
||||
return serve_template(templatename="history.html", title="History", date_format=date_format, time_format=time_format)
|
||||
|
||||
@cherrypy.expose
|
||||
def checkGithub(self):
|
||||
from plexpy import versioncheck
|
||||
versioncheck.checkGithub()
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
|
||||
@cherrypy.expose
|
||||
def logs(self):
|
||||
return serve_template(templatename="logs.html", title="Log", lineList=plexpy.LOG_LIST)
|
||||
|
||||
@cherrypy.expose
|
||||
def clearLogs(self):
|
||||
plexpy.LOG_LIST = []
|
||||
logger.info("Web logs cleared")
|
||||
raise cherrypy.HTTPRedirect("logs")
|
||||
|
||||
@cherrypy.expose
|
||||
def toggleVerbose(self):
|
||||
plexpy.VERBOSE = not plexpy.VERBOSE
|
||||
logger.initLogger(console=not plexpy.QUIET,
|
||||
log_dir=plexpy.CONFIG.LOG_DIR, verbose=plexpy.VERBOSE)
|
||||
logger.info("Verbose toggled, set to %s", plexpy.VERBOSE)
|
||||
logger.debug("If you read this message, debug logging is available")
|
||||
raise cherrypy.HTTPRedirect("logs")
|
||||
|
||||
@cherrypy.expose
|
||||
def getLog(self, iDisplayStart=0, iDisplayLength=100, iSortCol_0=0, sSortDir_0="desc", sSearch="", **kwargs):
|
||||
iDisplayStart = int(iDisplayStart)
|
||||
iDisplayLength = int(iDisplayLength)
|
||||
|
||||
filtered = []
|
||||
if sSearch == "":
|
||||
filtered = plexpy.LOG_LIST[::]
|
||||
else:
|
||||
filtered = [row for row in plexpy.LOG_LIST for column in row if sSearch.lower() in column.lower()]
|
||||
|
||||
sortcolumn = 0
|
||||
if iSortCol_0 == '1':
|
||||
sortcolumn = 2
|
||||
elif iSortCol_0 == '2':
|
||||
sortcolumn = 1
|
||||
filtered.sort(key=lambda x: x[sortcolumn], reverse=sSortDir_0 == "desc")
|
||||
|
||||
rows = filtered[iDisplayStart:(iDisplayStart + iDisplayLength)]
|
||||
rows = [[row[0], row[2], row[1]] for row in rows]
|
||||
|
||||
return json.dumps({
|
||||
'iTotalDisplayRecords': len(filtered),
|
||||
'iTotalRecords': len(plexpy.LOG_LIST),
|
||||
'aaData': rows,
|
||||
})
|
||||
|
||||
@cherrypy.expose
|
||||
def generateAPI(self):
|
||||
apikey = hashlib.sha224(str(random.getrandbits(256))).hexdigest()[0:32]
|
||||
logger.info("New API generated")
|
||||
return apikey
|
||||
|
||||
@cherrypy.expose
|
||||
def config(self):
|
||||
interface_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/')
|
||||
interface_list = [name for name in os.listdir(interface_dir) if os.path.isdir(os.path.join(interface_dir, name))]
|
||||
|
||||
config = {
|
||||
"http_host": plexpy.CONFIG.HTTP_HOST,
|
||||
"http_username": plexpy.CONFIG.HTTP_USERNAME,
|
||||
"http_port": plexpy.CONFIG.HTTP_PORT,
|
||||
"http_password": plexpy.CONFIG.HTTP_PASSWORD,
|
||||
"launch_browser": checked(plexpy.CONFIG.LAUNCH_BROWSER),
|
||||
"enable_https": checked(plexpy.CONFIG.ENABLE_HTTPS),
|
||||
"https_cert": plexpy.CONFIG.HTTPS_CERT,
|
||||
"https_key": plexpy.CONFIG.HTTPS_KEY,
|
||||
"api_enabled": checked(plexpy.CONFIG.API_ENABLED),
|
||||
"api_key": plexpy.CONFIG.API_KEY,
|
||||
"update_db_interval": plexpy.CONFIG.UPDATE_DB_INTERVAL,
|
||||
"freeze_db": checked(plexpy.CONFIG.FREEZE_DB),
|
||||
"log_dir": plexpy.CONFIG.LOG_DIR,
|
||||
"cache_dir": plexpy.CONFIG.CACHE_DIR,
|
||||
"interface_list": interface_list,
|
||||
"growl_enabled": checked(plexpy.CONFIG.GROWL_ENABLED),
|
||||
"growl_host": plexpy.CONFIG.GROWL_HOST,
|
||||
"growl_password": plexpy.CONFIG.GROWL_PASSWORD,
|
||||
"prowl_enabled": checked(plexpy.CONFIG.PROWL_ENABLED),
|
||||
"prowl_keys": plexpy.CONFIG.PROWL_KEYS,
|
||||
"prowl_priority": plexpy.CONFIG.PROWL_PRIORITY,
|
||||
"xbmc_enabled": checked(plexpy.CONFIG.XBMC_ENABLED),
|
||||
"xbmc_host": plexpy.CONFIG.XBMC_HOST,
|
||||
"xbmc_username": plexpy.CONFIG.XBMC_USERNAME,
|
||||
"xbmc_password": plexpy.CONFIG.XBMC_PASSWORD,
|
||||
"lms_enabled": checked(plexpy.CONFIG.LMS_ENABLED),
|
||||
"lms_host": plexpy.CONFIG.LMS_HOST,
|
||||
"plex_enabled": checked(plexpy.CONFIG.PLEX_ENABLED),
|
||||
"plex_client_host": plexpy.CONFIG.PLEX_CLIENT_HOST,
|
||||
"plex_username": plexpy.CONFIG.PLEX_USERNAME,
|
||||
"plex_password": plexpy.CONFIG.PLEX_PASSWORD,
|
||||
"nma_enabled": checked(plexpy.CONFIG.NMA_ENABLED),
|
||||
"nma_apikey": plexpy.CONFIG.NMA_APIKEY,
|
||||
"nma_priority": int(plexpy.CONFIG.NMA_PRIORITY),
|
||||
"pushalot_enabled": checked(plexpy.CONFIG.PUSHALOT_ENABLED),
|
||||
"pushalot_apikey": plexpy.CONFIG.PUSHALOT_APIKEY,
|
||||
"synoindex_enabled": checked(plexpy.CONFIG.SYNOINDEX_ENABLED),
|
||||
"pushover_enabled": checked(plexpy.CONFIG.PUSHOVER_ENABLED),
|
||||
"pushover_keys": plexpy.CONFIG.PUSHOVER_KEYS,
|
||||
"pushover_apitoken": plexpy.CONFIG.PUSHOVER_APITOKEN,
|
||||
"pushover_priority": plexpy.CONFIG.PUSHOVER_PRIORITY,
|
||||
"pushbullet_enabled": checked(plexpy.CONFIG.PUSHBULLET_ENABLED),
|
||||
"pushbullet_apikey": plexpy.CONFIG.PUSHBULLET_APIKEY,
|
||||
"pushbullet_deviceid": plexpy.CONFIG.PUSHBULLET_DEVICEID,
|
||||
"subsonic_enabled": checked(plexpy.CONFIG.SUBSONIC_ENABLED),
|
||||
"subsonic_host": plexpy.CONFIG.SUBSONIC_HOST,
|
||||
"subsonic_username": plexpy.CONFIG.SUBSONIC_USERNAME,
|
||||
"subsonic_password": plexpy.CONFIG.SUBSONIC_PASSWORD,
|
||||
"twitter_enabled": checked(plexpy.CONFIG.TWITTER_ENABLED),
|
||||
"osx_notify_enabled": checked(plexpy.CONFIG.OSX_NOTIFY_ENABLED),
|
||||
"osx_notify_app": plexpy.CONFIG.OSX_NOTIFY_APP,
|
||||
"boxcar_enabled": checked(plexpy.CONFIG.BOXCAR_ENABLED),
|
||||
"boxcar_token": plexpy.CONFIG.BOXCAR_TOKEN,
|
||||
"cache_sizemb": plexpy.CONFIG.CACHE_SIZEMB,
|
||||
"mpc_enabled": checked(plexpy.CONFIG.MPC_ENABLED),
|
||||
"email_enabled": checked(plexpy.CONFIG.EMAIL_ENABLED),
|
||||
"email_from": plexpy.CONFIG.EMAIL_FROM,
|
||||
"email_to": plexpy.CONFIG.EMAIL_TO,
|
||||
"email_smtp_server": plexpy.CONFIG.EMAIL_SMTP_SERVER,
|
||||
"email_smtp_user": plexpy.CONFIG.EMAIL_SMTP_USER,
|
||||
"email_smtp_password": plexpy.CONFIG.EMAIL_SMTP_PASSWORD,
|
||||
"email_smtp_port": int(plexpy.CONFIG.EMAIL_SMTP_PORT),
|
||||
"email_tls": checked(plexpy.CONFIG.EMAIL_TLS),
|
||||
"pms_ip": plexpy.CONFIG.PMS_IP,
|
||||
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||
"pms_username": plexpy.CONFIG.PMS_USERNAME,
|
||||
"pms_password": plexpy.CONFIG.PMS_PASSWORD,
|
||||
"plexwatch_database": plexpy.CONFIG.PLEXWATCH_DATABASE,
|
||||
"date_format": plexpy.CONFIG.DATE_FORMAT,
|
||||
"time_format": plexpy.CONFIG.TIME_FORMAT,
|
||||
"grouping_global_history": checked(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY),
|
||||
"grouping_user_history": checked(plexpy.CONFIG.GROUPING_USER_HISTORY),
|
||||
"grouping_charts": checked(plexpy.CONFIG.GROUPING_CHARTS)
|
||||
}
|
||||
|
||||
return serve_template(templatename="config.html", title="Settings", config=config)
|
||||
|
||||
@cherrypy.expose
|
||||
def configUpdate(self, **kwargs):
|
||||
# Handle the variable config options. Note - keys with False values aren't getting passed
|
||||
|
||||
checked_configs = [
|
||||
"launch_browser", "enable_https", "api_enabled", "freeze_db", "growl_enabled",
|
||||
"prowl_enabled", "xbmc_enabled", "lms_enabled",
|
||||
"plex_enabled", "nma_enabled", "pushalot_enabled",
|
||||
"synoindex_enabled", "pushover_enabled", "pushbullet_enabled",
|
||||
"subsonic_enabled", "twitter_enabled", "osx_notify_enabled",
|
||||
"boxcar_enabled", "mpc_enabled", "email_enabled", "email_tls",
|
||||
"grouping_global_history", "grouping_user_history", "grouping_charts"
|
||||
]
|
||||
for checked_config in checked_configs:
|
||||
if checked_config not in kwargs:
|
||||
# checked items should be zero or one. if they were not sent then the item was not checked
|
||||
kwargs[checked_config] = 0
|
||||
|
||||
for plain_config, use_config in [(x[4:], x) for x in kwargs if x.startswith('use_')]:
|
||||
# the use prefix is fairly nice in the html, but does not match the actual config
|
||||
kwargs[plain_config] = kwargs[use_config]
|
||||
del kwargs[use_config]
|
||||
|
||||
plexpy.CONFIG.process_kwargs(kwargs)
|
||||
|
||||
# Write the config
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
# Reconfigure scheduler
|
||||
plexpy.initialize_scheduler()
|
||||
|
||||
raise cherrypy.HTTPRedirect("config")
|
||||
|
||||
@cherrypy.expose
|
||||
def do_state_change(self, signal, title, timer):
|
||||
plexpy.SIGNAL = signal
|
||||
message = title + '...'
|
||||
return serve_template(templatename="shutdown.html", title=title,
|
||||
message=message, timer=timer)
|
||||
|
||||
@cherrypy.expose
|
||||
def getHistory_json(self, iDisplayStart=0, iDisplayLength=100, sSearch="", iSortCol_0='0', sSortDir_0='asc', **kwargs):
|
||||
iDisplayStart = int(iDisplayStart)
|
||||
iDisplayLength = int(iDisplayLength)
|
||||
filtered = []
|
||||
totalcount = 0
|
||||
myDB = db.DBConnection()
|
||||
db_table = db.DBConnection().get_history_table_name()
|
||||
|
||||
sortcolumn = 'time'
|
||||
sortbyhavepercent = False
|
||||
if iSortCol_0 == '1':
|
||||
sortcolumn = 'user'
|
||||
if iSortCol_0 == '2':
|
||||
sortcolumn = 'platform'
|
||||
elif iSortCol_0 == '3':
|
||||
sortcolumn = 'ip_address'
|
||||
elif iSortCol_0 == '4':
|
||||
sortcolumn = 'title'
|
||||
elif iSortCol_0 == '5':
|
||||
sortcolumn = 'time'
|
||||
elif iSortCol_0 == '6':
|
||||
sortcolumn = 'paused_counter'
|
||||
elif iSortCol_0 == '7':
|
||||
sortcolumn = 'stopped'
|
||||
elif iSortCol_0 == '8':
|
||||
sortbyhavepercent = True
|
||||
|
||||
if sSearch == "":
|
||||
query = 'SELECT * from %s order by %s COLLATE NOCASE %s' % (db_table, sortcolumn, sSortDir_0)
|
||||
filtered = myDB.select(query)
|
||||
totalcount = len(filtered)
|
||||
else:
|
||||
query = 'SELECT * from ' + db_table + ' WHERE user LIKE "%' + sSearch + \
|
||||
'%" OR title LIKE "%' + sSearch + '%"' + 'ORDER BY %s COLLATE NOCASE %s' % (sortcolumn, sSortDir_0)
|
||||
filtered = myDB.select(query)
|
||||
totalcount = myDB.select('SELECT COUNT(*) from processed')[0][0]
|
||||
|
||||
history = filtered[iDisplayStart:(iDisplayStart + iDisplayLength)]
|
||||
rows = []
|
||||
for item in history:
|
||||
row = {"date": item['time'],
|
||||
"user": item["user"],
|
||||
"platform": item["platform"],
|
||||
"ip_address": item["ip_address"],
|
||||
"title": item["title"],
|
||||
"started": item["time"],
|
||||
"paused": item["paused_counter"],
|
||||
"stopped": item["stopped"],
|
||||
"duration": "",
|
||||
"percent_complete": 0,
|
||||
}
|
||||
|
||||
if item['paused_counter'] > 0:
|
||||
row['paused'] = item['paused_counter']
|
||||
else:
|
||||
row['paused'] = 0
|
||||
|
||||
if item['time']:
|
||||
if item['stopped'] > 0:
|
||||
stopped = item['stopped']
|
||||
else:
|
||||
stopped = 0
|
||||
if item['paused_counter'] > 0:
|
||||
paused_counter = item['paused_counter']
|
||||
else:
|
||||
paused_counter = 0
|
||||
|
||||
row['duration'] = stopped - item['time'] + paused_counter
|
||||
|
||||
try:
|
||||
xml_parse = minidom.parseString(helpers.latinToAscii(item['xml']))
|
||||
except IOError, e:
|
||||
logger.warn("Error parsing XML in PlexWatch db: %s" % e)
|
||||
|
||||
xml_head = xml_parse.getElementsByTagName('opt')
|
||||
if not xml_head:
|
||||
logger.warn("Error parsing XML in PlexWatch db: %s" % e)
|
||||
|
||||
for s in xml_head:
|
||||
if s.getAttribute('duration') and s.getAttribute('viewOffset'):
|
||||
view_offset = helpers.cast_to_float(s.getAttribute('viewOffset'))
|
||||
duration = helpers.cast_to_float(s.getAttribute('duration'))
|
||||
if duration > 0:
|
||||
row['percent_complete'] = (view_offset / duration)*100
|
||||
else:
|
||||
row['percent_complete'] = 0
|
||||
|
||||
rows.append(row)
|
||||
|
||||
dict = {'iTotalDisplayRecords': len(filtered),
|
||||
'iTotalRecords': totalcount,
|
||||
'aaData': rows,
|
||||
}
|
||||
s = json.dumps(dict)
|
||||
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||
return s
|
||||
|
||||
@cherrypy.expose
|
||||
def shutdown(self):
|
||||
return self.do_state_change('shutdown', 'Shutting Down', 15)
|
||||
|
||||
@cherrypy.expose
|
||||
def restart(self):
|
||||
return self.do_state_change('restart', 'Restarting', 30)
|
||||
|
||||
@cherrypy.expose
|
||||
def update(self):
|
||||
return self.do_state_change('update', 'Updating', 120)
|
||||
|
||||
@cherrypy.expose
|
||||
def api(self, *args, **kwargs):
|
||||
from plexpy.api import Api
|
||||
|
||||
a = Api()
|
||||
a.checkParams(*args, **kwargs)
|
||||
|
||||
return a.fetchData()
|
||||
|
||||
@cherrypy.expose
|
||||
def twitterStep1(self):
|
||||
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
|
||||
tweet = notifiers.TwitterNotifier()
|
||||
return tweet._get_authorization()
|
||||
|
||||
@cherrypy.expose
|
||||
def twitterStep2(self, key):
|
||||
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
|
||||
tweet = notifiers.TwitterNotifier()
|
||||
result = tweet._get_credentials(key)
|
||||
logger.info(u"result: " + str(result))
|
||||
if result:
|
||||
return "Key verification successful"
|
||||
else:
|
||||
return "Unable to verify key"
|
||||
|
||||
@cherrypy.expose
|
||||
def testTwitter(self):
|
||||
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
|
||||
tweet = notifiers.TwitterNotifier()
|
||||
result = tweet.test_notify()
|
||||
if result:
|
||||
return "Tweet successful, check your twitter to make sure it worked"
|
||||
else:
|
||||
return "Error sending tweet"
|
||||
|
||||
@cherrypy.expose
|
||||
def osxnotifyregister(self, app):
|
||||
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
|
||||
from osxnotify import registerapp as osxnotify
|
||||
result, msg = osxnotify.registerapp(app)
|
||||
if result:
|
||||
osx_notify = notifiers.OSX_NOTIFY()
|
||||
osx_notify.notify('Registered', result, 'Success :-)')
|
||||
logger.info('Registered %s, to re-register a different app, delete this app first' % result)
|
||||
else:
|
||||
logger.warn(msg)
|
||||
return msg
|
||||
|
124
plexpy/webstart.py
Normal file
124
plexpy/webstart.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import cherrypy
|
||||
import plexpy
|
||||
|
||||
from plexpy import logger
|
||||
from plexpy.webserve import WebInterface
|
||||
from plexpy.helpers import create_https_certificates
|
||||
|
||||
|
||||
def initialize(options):
|
||||
|
||||
# HTTPS stuff stolen from sickbeard
|
||||
enable_https = options['enable_https']
|
||||
https_cert = options['https_cert']
|
||||
https_key = options['https_key']
|
||||
|
||||
if enable_https:
|
||||
# If either the HTTPS certificate or key do not exist, try to make
|
||||
# self-signed ones.
|
||||
if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)):
|
||||
if not create_https_certificates(https_cert, https_key):
|
||||
logger.warn("Unable to create certificate and key. Disabling " \
|
||||
"HTTPS")
|
||||
enable_https = False
|
||||
|
||||
if not (os.path.exists(https_cert) and os.path.exists(https_key)):
|
||||
logger.warn("Disabled HTTPS because of missing certificate and " \
|
||||
"key.")
|
||||
enable_https = False
|
||||
|
||||
options_dict = {
|
||||
'server.socket_port': options['http_port'],
|
||||
'server.socket_host': options['http_host'],
|
||||
'server.thread_pool': 10,
|
||||
'tools.encode.on': True,
|
||||
'tools.encode.encoding': 'utf-8',
|
||||
'tools.decode.on': True,
|
||||
'log.screen': False,
|
||||
'engine.autoreload.on': False,
|
||||
}
|
||||
|
||||
if enable_https:
|
||||
options_dict['server.ssl_certificate'] = https_cert
|
||||
options_dict['server.ssl_private_key'] = https_key
|
||||
protocol = "https"
|
||||
else:
|
||||
protocol = "http"
|
||||
|
||||
logger.info("Starting PlexPy web server on %s://%s:%d/", protocol,
|
||||
options['http_host'], options['http_port'])
|
||||
cherrypy.config.update(options_dict)
|
||||
|
||||
conf = {
|
||||
'/': {
|
||||
'tools.staticdir.root': os.path.join(plexpy.PROG_DIR, 'data'),
|
||||
'tools.proxy.on': options['http_proxy'] # pay attention to X-Forwarded-Proto header
|
||||
},
|
||||
'/interfaces': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "interfaces"
|
||||
},
|
||||
'/images': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "images"
|
||||
},
|
||||
'/css': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "css"
|
||||
},
|
||||
'/js': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "js"
|
||||
},
|
||||
'/favicon.ico': {
|
||||
'tools.staticfile.on': True,
|
||||
'tools.staticfile.filename': os.path.join(os.path.abspath(
|
||||
os.curdir), "images" + os.sep + "favicon.ico")
|
||||
},
|
||||
'/cache': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': plexpy.CONFIG.CACHE_DIR
|
||||
}
|
||||
}
|
||||
|
||||
if options['http_password']:
|
||||
logger.info("Web server authentication is enabled, username is '%s'", options['http_username'])
|
||||
|
||||
conf['/'].update({
|
||||
'tools.auth_basic.on': True,
|
||||
'tools.auth_basic.realm': 'PlexPy web server',
|
||||
'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict({
|
||||
options['http_username']: options['http_password']
|
||||
})
|
||||
})
|
||||
conf['/api'] = {'tools.auth_basic.on': False}
|
||||
|
||||
# Prevent time-outs
|
||||
cherrypy.engine.timeout_monitor.unsubscribe()
|
||||
cherrypy.tree.mount(WebInterface(), str(options['http_root']), config=conf)
|
||||
|
||||
try:
|
||||
cherrypy.process.servers.check_port(str(options['http_host']), options['http_port'])
|
||||
cherrypy.server.start()
|
||||
except IOError:
|
||||
sys.stderr.write('Failed to start on port: %i. Is something else running?\n' % (options['http_port']))
|
||||
sys.exit(1)
|
||||
|
||||
cherrypy.server.wait()
|
Loading…
Add table
Add a link
Reference in a new issue