Compare commits

...

40 Commits

Author SHA1 Message Date
JonnyWong16
a08bce2073 v2.0.22 2018-03-10 09:32:08 -08:00
JonnyWong16
4e9c8322c3 Don't overwrite tautulli db on move 2018-03-10 09:32:05 -08:00
JonnyWong16
89bfe85be3 Workaround for duration reported as minutes for a show 2018-03-10 08:58:15 -08:00
JonnyWong16
98d994591c Fix runtime round to minutes 2018-03-09 19:12:12 -08:00
JonnyWong16
a29bc7f4f9 v2.0.22-beta 2018-03-09 17:58:40 -08:00
JonnyWong16
288f4c5f7f Fix expanding selectize box 2018-03-09 15:50:53 -08:00
JonnyWong16
a6bf78ed56 Check is schedulers running before shutdown 2018-03-08 18:32:47 -08:00
JonnyWong16
8dbb05931e Fix library refresh when missing library 2018-03-08 18:23:12 -08:00
JonnyWong16
ac8a712ff0 Fix refreshing activity after losing connection 2018-03-06 20:01:11 -08:00
JonnyWong16
39406c25c3 Add retry and expire for Pushover priority 2 2018-03-06 09:57:06 -08:00
JonnyWong16
48d7c2c54c Fix photo library count and media info table 2018-03-05 09:49:48 -08:00
JonnyWong16
0217188274 Fix update check 2018-03-04 22:49:32 -08:00
JonnyWong16
fd762e71de Fix cherrypy sending wrong Content-Type header for svg 2018-03-04 22:32:26 -08:00
JonnyWong16
4d5c3b6df0 v2.0.21-beta 2018-03-04 14:51:27 -08:00
JonnyWong16
7df54e4d1b Replace Flattr with Patreon 2018-03-04 14:25:38 -08:00
JonnyWong16
5d085de9d3 Rename logger name 2018-03-04 12:24:25 -08:00
JonnyWong16
a8a4299086 Add execute permission to PlexPy.py 2018-03-04 12:17:09 -08:00
JonnyWong16
86f0e8425c Add execute permission to Tautulli.py 2018-03-04 12:15:05 -08:00
JonnyWong16
d2e879be4a Add PlexPy.py file to run Tautulli.py 2018-03-04 12:01:31 -08:00
JonnyWong16
544114fffe Rename css files to tautulli 2018-03-04 11:40:38 -08:00
JonnyWong16
3b3e207b11 Rename log files to tautulli 2018-03-04 11:38:31 -08:00
JonnyWong16
84aad638ac Rename database backup to tautulli 2018-03-04 11:28:35 -08:00
JonnyWong16
2bb691966e Rename default notifier settings to tautulli 2018-03-04 11:18:04 -08:00
JonnyWong16
8f5e788270 Rename plexpy.db to tautulli.db 2018-03-04 11:17:35 -08:00
JonnyWong16
7c43ea2f46 Rename PlexPy.py to Tautulli.py 2018-03-04 11:17:11 -08:00
JonnyWong16
8146e1e3cf Capitalize Tautulli folder in init scripts 2018-03-04 10:28:28 -08:00
JonnyWong16
51b1ff6d4a Rename variables in Ubuntu script 2018-03-04 10:17:16 -08:00
JonnyWong16
403e8dfbea Update all init scripts to Tautulli 2018-03-04 09:44:02 -08:00
JonnyWong16
9d08717c83 Fix missing country in whois lookup causing error 2018-03-02 15:39:05 -08:00
JonnyWong16
66167d5960 Remove word "allowed" 2018-03-02 10:24:28 -08:00
JonnyWong16
624863d826 Hide number input spinners on Firefox 2018-03-02 08:48:31 -08:00
JonnyWong16
d4b3810fbc Reduce number input width 2018-03-01 19:34:52 -08:00
JonnyWong16
6056e1d3b9 Hide arrows on number inputes 2018-03-01 13:08:43 -08:00
JonnyWong16
1a293d525f Update database session on state change 2018-02-28 13:34:21 -08:00
JonnyWong16
b87eb68bdd Identify if a stream is using Plex Relay 2018-02-27 20:03:31 -08:00
JonnyWong16
8620546d07 Move import from a082109 2018-02-27 15:17:35 -08:00
JonnyWong16
a082109045 Don't ping for activity if websocket is not connected 2018-02-27 15:02:17 -08:00
JonnyWong16
559a9b393e Catch failure to send analytics event 2018-02-24 15:08:58 -08:00
JonnyWong16
ae41b22e59 Forgot one version number in 754fd24 2018-02-24 14:51:19 -08:00
JonnyWong16
754fd24421 Refactor some code 2018-02-24 10:09:02 -08:00
53 changed files with 781 additions and 646 deletions

1
API.md
View File

@@ -401,6 +401,7 @@ Returns:
"quality_profile": "Original",
"rating": "7.8",
"rating_key": "153037",
"relay": 0,
"section_id": "2",
"session_id": "helf15l3rxgw01xxe0jf3l3d",
"session_key": "27",

View File

@@ -1,5 +1,37 @@
# Changelog
## v2.0.22 (2018-03-10)
* Tautulli v2 release!
## v2.0.22-beta (2018-03-09)
* Notifications:
* Fix: Pushover notifications failing with priority 2 is set.
* Fix: Expanding selectize box for some notification agent settings.
* Other:
* Fix: Update check failing when an update is available.
* Fix: Item count incorrect for photo libraries.
## v2.0.21-beta (2018-03-04)
* Monitoring:
* New: Identify if a stream is using Plex Relay.
* Change: Don't ping the Plex server if the websocket is disconnected.
* Notifications:
* Fix: Pause/resume state not being sent correctly in some instances.
* Other:
* New: Add Patreon donation method.
* Fix: Catch failure to send analytics.
* Fix: IP address connection lookup error when the country is missing.
* Change: Updated all init scripts to Tautulli.
* Change: Move database to tautulli.db.
* Change: Move logs to tautulli.log.
* Change: Move startup file to Tautulli.py.
## v2.0.20-beta (2018-02-24)
* Notifications:

235
PlexPy.py
View File

@@ -21,239 +21,8 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
from Tautulli import main
# Ensure lib added to path, before any other imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
import argparse
import locale
import signal
import time
import plexpy
from plexpy import config, database, logger, webstart
# Register signals, such as CTRL + C
signal.signal(signal.SIGINT, plexpy.sig_handler)
signal.signal(signal.SIGTERM, plexpy.sig_handler)
def main():
"""
Tautulli application entry point. Parses arguments, setups encoding and
initializes the application.
"""
# Fixed paths to Tautulli
if hasattr(sys, 'frozen'):
plexpy.FULL_PATH = os.path.abspath(sys.executable)
else:
plexpy.FULL_PATH = os.path.abspath(__file__)
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
plexpy.ARGS = sys.argv[1:]
# From sickbeard
plexpy.SYS_PLATFORM = sys.platform
plexpy.SYS_ENCODING = None
try:
locale.setlocale(locale.LC_ALL, "")
plexpy.SYS_LANGUAGE, plexpy.SYS_ENCODING = locale.getdefaultlocale()
except (locale.Error, IOError):
pass
# for OSes that are poorly configured I'll just force UTF-8
if not plexpy.SYS_ENCODING or plexpy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
plexpy.SYS_ENCODING = 'UTF-8'
# Set up and gather command line arguments
parser = argparse.ArgumentParser(
description='A Python based monitoring and tracking tool for Plex Media Server.')
parser.add_argument(
'-v', '--verbose', action='store_true', help='Increase console logging verbosity')
parser.add_argument(
'-q', '--quiet', action='store_true', help='Turn off console logging')
parser.add_argument(
'-d', '--daemon', action='store_true', help='Run as a daemon')
parser.add_argument(
'-p', '--port', type=int, help='Force Tautulli to run on a specified port')
parser.add_argument(
'--dev', action='store_true', help='Start Tautulli in the development environment')
parser.add_argument(
'--datadir', help='Specify a directory where to store your data files')
parser.add_argument(
'--config', help='Specify a config file to use')
parser.add_argument(
'--nolaunch', action='store_true', help='Prevent browser from launching on startup')
parser.add_argument(
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
parser.add_argument(
'--nofork', action='store_true', help='Start Tautulli as a service, do not fork when restarting')
args = parser.parse_args()
if args.verbose:
plexpy.VERBOSE = True
if args.quiet:
plexpy.QUIET = True
# Do an intial setup of the logger.
logger.initLogger(console=not plexpy.QUIET, log_dir=False,
verbose=plexpy.VERBOSE)
if args.dev:
plexpy.DEV = True
logger.debug(u"Tautulli is running in the dev environment.")
if args.daemon:
if sys.platform == 'win32':
sys.stderr.write(
"Daemonizing not supported under Windows, starting normally\n")
else:
plexpy.DAEMON = True
plexpy.QUIET = True
if args.nofork:
plexpy.NOFORK = True
logger.info("Tautulli is running as a service, it will not fork when restarted.")
if args.pidfile:
plexpy.PIDFILE = str(args.pidfile)
# If the pidfile already exists, plexpy may still be running, so
# exit
if os.path.exists(plexpy.PIDFILE):
try:
with open(plexpy.PIDFILE, 'r') as fp:
pid = int(fp.read())
os.kill(pid, 0)
except IOError as e:
raise SystemExit("Unable to read PID file: %s", e)
except OSError:
logger.warn("PID file '%s' already exists, but PID %d is " \
"not running. Ignoring PID file." %
(plexpy.PIDFILE, pid))
else:
# The pidfile exists and points to a live PID. plexpy may
# still be running, so exit.
raise SystemExit("PID file '%s' already exists. Exiting." %
plexpy.PIDFILE)
# The pidfile is only useful in daemon mode, make sure we can write the
# file properly
if plexpy.DAEMON:
plexpy.CREATEPID = True
try:
with open(plexpy.PIDFILE, 'w') as fp:
fp.write("pid\n")
except IOError as e:
raise SystemExit("Unable to write PID file: %s", e)
else:
logger.warn("Not running in daemon mode. PID file creation " \
"disabled.")
# Determine which data directory and config file to use
if args.datadir:
plexpy.DATA_DIR = args.datadir
else:
plexpy.DATA_DIR = plexpy.PROG_DIR
if args.config:
config_file = args.config
else:
config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME)
# Try to create the DATA_DIR if it doesn't exist
if not os.path.exists(plexpy.DATA_DIR):
try:
os.makedirs(plexpy.DATA_DIR)
except OSError:
raise SystemExit(
'Could not create data directory: ' + plexpy.DATA_DIR + '. Exiting....')
# Make sure the DATA_DIR is writeable
if not os.access(plexpy.DATA_DIR, os.W_OK):
raise SystemExit(
'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...')
# Put the database in the DATA_DIR
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME)
if plexpy.DAEMON:
plexpy.daemonize()
# Read config and start logging
plexpy.initialize(config_file)
# Start the background threads
plexpy.start()
# Force the http port if neccessary
if args.port:
http_port = args.port
logger.info('Using forced web server port: %i', http_port)
else:
http_port = int(plexpy.CONFIG.HTTP_PORT)
# Check if pyOpenSSL is installed. It is required for certificate generation
# and for CherryPy.
if plexpy.CONFIG.ENABLE_HTTPS:
try:
import OpenSSL
except ImportError:
logger.warn("The pyOpenSSL module is missing. Install this " \
"module to enable HTTPS. HTTPS will be disabled.")
plexpy.CONFIG.ENABLE_HTTPS = False
# Try to start the server. Will exit here is address is already in use.
web_config = {
'http_port': http_port,
'http_host': plexpy.CONFIG.HTTP_HOST,
'http_root': plexpy.CONFIG.HTTP_ROOT,
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT,
'http_proxy': plexpy.CONFIG.HTTP_PROXY,
'enable_https': plexpy.CONFIG.ENABLE_HTTPS,
'https_cert': plexpy.CONFIG.HTTPS_CERT,
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
'https_key': plexpy.CONFIG.HTTPS_KEY,
'http_username': plexpy.CONFIG.HTTP_USERNAME,
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
}
webstart.initialize(web_config)
# Open webbrowser
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, http_port,
plexpy.CONFIG.HTTP_ROOT)
# Wait endlessy for a signal to happen
while True:
if not plexpy.SIGNAL:
try:
time.sleep(1)
except KeyboardInterrupt:
plexpy.SIGNAL = 'shutdown'
else:
logger.info('Received signal: %s', plexpy.SIGNAL)
if plexpy.SIGNAL == 'shutdown':
plexpy.shutdown()
elif plexpy.SIGNAL == 'restart':
plexpy.shutdown(restart=True)
elif plexpy.SIGNAL == 'checkout':
plexpy.shutdown(restart=True, checkout=True)
else:
plexpy.shutdown(restart=True, update=True)
plexpy.SIGNAL = None
# Call main()
# Call main() from Tautulli.py
if __name__ == "__main__":
main()

264
Tautulli.py Executable file
View File

@@ -0,0 +1,264 @@
#!/bin/sh
''''which python >/dev/null 2>&1 && exec python "$0" "$@" # '''
''''which python2 >/dev/null 2>&1 && exec python2 "$0" "$@" # '''
''''which python2.7 >/dev/null 2>&1 && exec python2.7 "$0" "$@" # '''
''''exec echo "Error: Python not found!" # '''
# -*- coding: utf-8 -*-
# This file is part of Tautulli.
#
# Tautulli 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.
#
# Tautulli 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 Tautulli. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
# Ensure lib added to path, before any other imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
import argparse
import locale
import signal
import time
import plexpy
from plexpy import config, database, logger, webstart
# Register signals, such as CTRL + C
signal.signal(signal.SIGINT, plexpy.sig_handler)
signal.signal(signal.SIGTERM, plexpy.sig_handler)
def main():
"""
Tautulli application entry point. Parses arguments, setups encoding and
initializes the application.
"""
# Fixed paths to Tautulli
if hasattr(sys, 'frozen'):
plexpy.FULL_PATH = os.path.abspath(sys.executable)
else:
plexpy.FULL_PATH = os.path.abspath(__file__)
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
plexpy.ARGS = sys.argv[1:]
# From sickbeard
plexpy.SYS_PLATFORM = sys.platform
plexpy.SYS_ENCODING = None
try:
locale.setlocale(locale.LC_ALL, "")
plexpy.SYS_LANGUAGE, plexpy.SYS_ENCODING = locale.getdefaultlocale()
except (locale.Error, IOError):
pass
# for OSes that are poorly configured I'll just force UTF-8
if not plexpy.SYS_ENCODING or plexpy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
plexpy.SYS_ENCODING = 'UTF-8'
# Set up and gather command line arguments
parser = argparse.ArgumentParser(
description='A Python based monitoring and tracking tool for Plex Media Server.')
parser.add_argument(
'-v', '--verbose', action='store_true', help='Increase console logging verbosity')
parser.add_argument(
'-q', '--quiet', action='store_true', help='Turn off console logging')
parser.add_argument(
'-d', '--daemon', action='store_true', help='Run as a daemon')
parser.add_argument(
'-p', '--port', type=int, help='Force Tautulli to run on a specified port')
parser.add_argument(
'--dev', action='store_true', help='Start Tautulli in the development environment')
parser.add_argument(
'--datadir', help='Specify a directory where to store your data files')
parser.add_argument(
'--config', help='Specify a config file to use')
parser.add_argument(
'--nolaunch', action='store_true', help='Prevent browser from launching on startup')
parser.add_argument(
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
parser.add_argument(
'--nofork', action='store_true', help='Start Tautulli as a service, do not fork when restarting')
args = parser.parse_args()
if args.verbose:
plexpy.VERBOSE = True
if args.quiet:
plexpy.QUIET = True
# Do an intial setup of the logger.
logger.initLogger(console=not plexpy.QUIET, log_dir=False,
verbose=plexpy.VERBOSE)
if args.dev:
plexpy.DEV = True
logger.debug(u"Tautulli is running in the dev environment.")
if args.daemon:
if sys.platform == 'win32':
sys.stderr.write(
"Daemonizing not supported under Windows, starting normally\n")
else:
plexpy.DAEMON = True
plexpy.QUIET = True
if args.nofork:
plexpy.NOFORK = True
logger.info("Tautulli is running as a service, it will not fork when restarted.")
if args.pidfile:
plexpy.PIDFILE = str(args.pidfile)
# If the pidfile already exists, plexpy may still be running, so
# exit
if os.path.exists(plexpy.PIDFILE):
try:
with open(plexpy.PIDFILE, 'r') as fp:
pid = int(fp.read())
os.kill(pid, 0)
except IOError as e:
raise SystemExit("Unable to read PID file: %s", e)
except OSError:
logger.warn("PID file '%s' already exists, but PID %d is " \
"not running. Ignoring PID file." %
(plexpy.PIDFILE, pid))
else:
# The pidfile exists and points to a live PID. plexpy may
# still be running, so exit.
raise SystemExit("PID file '%s' already exists. Exiting." %
plexpy.PIDFILE)
# The pidfile is only useful in daemon mode, make sure we can write the
# file properly
if plexpy.DAEMON:
plexpy.CREATEPID = True
try:
with open(plexpy.PIDFILE, 'w') as fp:
fp.write("pid\n")
except IOError as e:
raise SystemExit("Unable to write PID file: %s", e)
else:
logger.warn("Not running in daemon mode. PID file creation " \
"disabled.")
# Determine which data directory and config file to use
if args.datadir:
plexpy.DATA_DIR = args.datadir
else:
plexpy.DATA_DIR = plexpy.PROG_DIR
if args.config:
config_file = args.config
else:
config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME)
# Try to create the DATA_DIR if it doesn't exist
if not os.path.exists(plexpy.DATA_DIR):
try:
os.makedirs(plexpy.DATA_DIR)
except OSError:
raise SystemExit(
'Could not create data directory: ' + plexpy.DATA_DIR + '. Exiting....')
# Make sure the DATA_DIR is writeable
if not os.access(plexpy.DATA_DIR, os.W_OK):
raise SystemExit(
'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...')
# Put the database in the DATA_DIR
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME)
# Move 'plexpy.db' to 'tautulli.db'
if os.path.isfile(os.path.join(plexpy.DATA_DIR, 'plexpy.db')) and \
not os.path.isfile(os.path.join(plexpy.DATA_DIR, plexpy.DB_FILE)):
os.rename(os.path.join(plexpy.DATA_DIR, 'plexpy.db'), plexpy.DB_FILE)
if plexpy.DAEMON:
plexpy.daemonize()
# Read config and start logging
plexpy.initialize(config_file)
# Start the background threads
plexpy.start()
# Force the http port if neccessary
if args.port:
http_port = args.port
logger.info('Using forced web server port: %i', http_port)
else:
http_port = int(plexpy.CONFIG.HTTP_PORT)
# Check if pyOpenSSL is installed. It is required for certificate generation
# and for CherryPy.
if plexpy.CONFIG.ENABLE_HTTPS:
try:
import OpenSSL
except ImportError:
logger.warn("The pyOpenSSL module is missing. Install this " \
"module to enable HTTPS. HTTPS will be disabled.")
plexpy.CONFIG.ENABLE_HTTPS = False
# Try to start the server. Will exit here is address is already in use.
web_config = {
'http_port': http_port,
'http_host': plexpy.CONFIG.HTTP_HOST,
'http_root': plexpy.CONFIG.HTTP_ROOT,
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT,
'http_proxy': plexpy.CONFIG.HTTP_PROXY,
'enable_https': plexpy.CONFIG.ENABLE_HTTPS,
'https_cert': plexpy.CONFIG.HTTPS_CERT,
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
'https_key': plexpy.CONFIG.HTTPS_KEY,
'http_username': plexpy.CONFIG.HTTP_USERNAME,
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
}
webstart.initialize(web_config)
# Open webbrowser
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, http_port,
plexpy.CONFIG.HTTP_ROOT)
# Wait endlessy for a signal to happen
while True:
if not plexpy.SIGNAL:
try:
time.sleep(1)
except KeyboardInterrupt:
plexpy.SIGNAL = 'shutdown'
else:
logger.info('Received signal: %s', plexpy.SIGNAL)
if plexpy.SIGNAL == 'shutdown':
plexpy.shutdown()
elif plexpy.SIGNAL == 'restart':
plexpy.shutdown(restart=True)
elif plexpy.SIGNAL == 'checkout':
plexpy.shutdown(restart=True, checkout=True)
else:
plexpy.shutdown(restart=True, update=True)
plexpy.SIGNAL = None
# Call main()
if __name__ == "__main__":
main()

View File

@@ -15,7 +15,7 @@
<meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
<link href="${http_root}css/plexpy.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
${next.headIncludes()}
@@ -47,7 +47,7 @@
You are running an unknown version of Tautulli.<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
</div>
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.COMMITS_BEHIND > 0 and plexpy.common.BRANCH in ('master', 'beta') and plexpy.common.VERSION_NUMBER != plexpy.LATEST_RELEASE:
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.COMMITS_BEHIND > 0 and plexpy.common.BRANCH in ('master', 'beta') and plexpy.common.RELEASE != plexpy.LATEST_RELEASE:
<div id="updatebar" style="display: none;">
A <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank">
new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br />
@@ -227,15 +227,23 @@ ${next.modalIncludes()}
</div>
</div>
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
<li class="active"><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
<li><a href="#flattr-donation" role="tab" data-toggle="tab">Flattr</a></li>
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoin" data-name="Bitcoin" data-address="3FdfJAyNWU15Sf11U9FTgPHuP1hPz32eEN">Bitcoin</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoincash" data-name="Bitcoin Cash" data-address="1H2atabxAQGaFAWYQEiLkXKSnK9CZZvt2n">Bitcoin Cash</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="ethereum" data-name="Ethereum" data-address="0x77ae4c2b8de1a1ccfa93553db39971da58c873d3">Ethereum</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="litecoin" data-name="Litecoin" data-address="LWpPmUqQYHBhMV83XSCsHzPmKLhJt6r57J">Litecoin</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="paypal-donation" style="text-align: center">
<div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center">
<p>
Click the button below to continue to Patreon.
</p>
<a href="${anon_url('https://www.patreon.com/bePatron?u=10078609')}" target="_blank">
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
<p>
Click the button below to continue to PayPal.
</p>
@@ -243,14 +251,6 @@ ${next.modalIncludes()}
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="flattr-donation" style="text-align: center">
<p>
Click the button below to continue to Flattr.
</p>
<a href="${anon_url('https://flattr.com/submit/auto?user_id=JonnyWong16&url=https://github.com/%s/%s&title=Tautulli&language=en_GB&tags=github&category=software' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">
<img src="images/flattr-badge-large.png" alt="Flattr">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="crypto-donation">
<label>QR Code</label>
<pre id="crypto_qr_code" style="text-align: center"></pre>

View File

@@ -66,7 +66,6 @@ div.form-control .selectize-input {
color: #fff;
border: 0px solid #444;
background: #555;
height: 32px;
padding: 6px 12px;
background-color: #555;
border-radius: 3px;
@@ -92,6 +91,7 @@ div.form-control .selectize-input {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
min-height: 32px !important;
height: 32px !important;
}
.input-group .selectize-control.form-control.selectize-pms-ip .selectize-input > div {
max-width: 450px;
@@ -1419,7 +1419,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
}
.dashboard-stats-info-item .sub-count {
height: 100%;
margin-left: 10px;
margin-left: 5px;
color: #f9be03;
font-size: 12px;
text-align: right;
@@ -1430,7 +1430,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
}
.dashboard-stats-info-item .sub-divider {
height: 100%;
margin-left: 10px;
margin-left: 5px;
color: #aaa;
font-size: 12px;
text-align: left;
@@ -2372,21 +2372,6 @@ a .library-user-instance-box:hover {
#watched-stats-days-selection label {
margin-bottom: 0;
}
#watched-stats-days {
margin: 0;
width: 75px;
height: 34px;
}
#watched-stats-count {
margin: 0;
width: 75px;
height: 34px;
}
#recently-added-count {
margin: 0;
width: 75px;
height: 34px;
}
.home-padded-header {
margin: 25px 0;
height: 34px;
@@ -3435,22 +3420,10 @@ pre::-webkit-scrollbar-thumb {
.notification-params tr:nth-child(even) td {
background-color: rgba(255,255,255,0.010);
}
#days-selection label {
margin-bottom: 0;
}
#graph-days {
margin: 0;
width: 75px;
height: 34px;
}
#days-selection label,
#months-selection label {
margin-bottom: 0;
}
#graph-months {
margin: 0;
width: 75px;
height: 34px;
}
.card-sortable {
height: 36px;
padding: 0 20px 0 0;
@@ -3967,3 +3940,14 @@ a:hover .overlay-refresh-image:hover {
.stream-info tr:nth-child(even) td {
background-color: rgba(255,255,255,0.010);
}
.number-input {
margin: 0 !important;
width: 55px !important;
height: 34px !important;
-moz-appearance: textfield;
}
.number-input::-webkit-inner-spin-button,
.number-input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

View File

@@ -279,16 +279,20 @@ DOCUMENTATION :: END
<span id="location-${sk}">${data['location'].upper()}</span>:
% if data['ip_address'] != 'N/A':
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
</a>
<script>
isPrivateIP("${data['ip_address']}").then(function () {
$("#external_ip-${sk}").hide();
}, function () {
$("#external_ip-${sk}").show();
});
</script>
% if data['relay']:
<span data-toggle="tooltip" title="Plex Relay"><i class="fa fa-exclamation-circle"></i></span>
% else:
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
</a>
<script>
isPrivateIP("${data['ip_address']}").then(function () {
$("#external_ip-${sk}").hide();
}, function () {
$("#external_ip-${sk}").show();
});
</script>
% endif
% else:
N/A
% endif

View File

@@ -2,7 +2,7 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">
@@ -39,12 +39,12 @@
</div>
<div class="input-group pull-right" style="width: 1px;" id="days-selection">
<span class="input-group-addon btn-dark inactive">Last</span>
<input type="number" class="form-control" name="graph-days" id="graph-days" value="${config['graph_days']}" min="1" data-default="7" data-toggle="tooltip" title="Min: 1 day" />
<input type="number" class="form-control number-input" name="graph-days" id="graph-days" value="${config['graph_days']}" min="1" data-default="7" data-toggle="tooltip" title="Min: 1 day" />
<span class="input-group-addon btn-dark inactive">days</span>
</div>
<div class="input-group pull-right" style="width: 1px;" id="months-selection">
<span class="input-group-addon btn-dark inactive">Last</span>
<input type="number" class="form-control" name="graph-months" id="graph-months" value="${config['graph_months']}" min="1" data-default="12" data-toggle="tooltip" title="Min: 1 month" />
<input type="number" class="form-control number-input" name="graph-months" id="graph-months" value="${config['graph_months']}" min="1" data-default="12" data-toggle="tooltip" title="Min: 1 month" />
<span class="input-group-addon btn-dark inactive">months</span>
</div>
</div>

View File

@@ -3,7 +3,7 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -22,7 +22,16 @@
</h3>
</div>
<div id="currentActivity">
<% from plexpy import PLEX_SERVER_UP %>
% if PLEX_SERVER_UP:
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
% else:
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
% if _session['user_group'] == 'admin':
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
% endif
</div>
% endif
</div>
</div>
</div>
@@ -51,7 +60,7 @@
</div>
<div class="input-group pull-left" style="width: 1px; margin-right: 3px" id="watched-stats-days-selection">
<span class="input-group-addon btn-dark inactive">Last</span>
<input type="number" class="form-control" name="watched-stats-days" id="watched-stats-days" value="${config['home_stats_length']}" min="1" data-default="30" data-toggle="tooltip" title="Min: 1 day" />
<input type="number" class="form-control number-input" name="watched-stats-days" id="watched-stats-days" value="${config['home_stats_length']}" min="1" data-default="30" data-toggle="tooltip" title="Min: 1 day" />
<span class="input-group-addon btn-dark inactive">days</span>
</div>
</div>
@@ -114,7 +123,7 @@
</label>
</div>
<div class="input-group pull-left" style="width: 1px;" id="recently-added-count-selection">
<input type="number" class="form-control" name="recently-added-count" id="recently-added-count" value="${config['home_stats_recently_added_count']}" min="1" max="100" data-default="50" data-toggle="tooltip" title="Min: 1 item<br>Max: 100 items" />
<input type="number" class="form-control number-input" name="recently-added-count" id="recently-added-count" value="${config['home_stats_recently_added_count']}" min="1" max="100" data-default="50" data-toggle="tooltip" title="Min: 1 item<br>Max: 100 items" />
<span class="input-group-addon btn-dark inactive">items</span>
</div>
</div>
@@ -137,13 +146,13 @@
<%def name="modalIncludes()">
% if _session['user_group'] == 'admin' and config['update_show_changelog']:
<% from plexpy.common import VERSION_NUMBER %>
<% from plexpy.common import RELEASE %>
<div id="changelog-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">Tautulli Updated to <strong>${VERSION_NUMBER}</strong></h4>
<h4 class="modal-title">Tautulli Updated to <strong>${RELEASE}</strong></h4>
</div>
<div class="modal-body">
</div>
@@ -241,9 +250,10 @@
});
}
});
};
}
</script>
% if 'current_activity' in config['home_sections']:
<% from plexpy import PLEX_SERVER_UP %>
% if 'current_activity' in config['home_sections'] and PLEX_SERVER_UP:
<script>
var defaultHandler = {
get: function(target, name) {
@@ -266,6 +276,7 @@
async: true,
error: function (xhr, status, error) {
console.log(status + ': ' + error);
activity_ready = true;
},
complete: function (xhr, status) {
$('#dashboard-checking-activity').remove();
@@ -280,9 +291,9 @@
if (!(current_activity)) {
% if _session['user_group'] == 'admin':
var msg_settings = ' Verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
var msg_settings = ' Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
% else:
var msg_settings = ''
var msg_settings = '';
% endif
$('#currentActivityHeader').hide();
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');

View File

@@ -64,7 +64,7 @@ DOCUMENTATION :: END
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">

View File

@@ -292,7 +292,11 @@ function millisecondsToMinutes(ms, roundToMinute) {
if (ms > 0) {
var minutes = Math.floor(ms / 60000);
var seconds = ((ms % 60000) / 1000).toFixed(0);
return (seconds == 60 ? (minutes+1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
if (roundToMinute) {
return (seconds >= 30 ? (minutes + 1) : minutes);
} else {
return (seconds == 60 ? (minutes + 1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
}
} else {
if (roundToMinute) {
return '0';

View File

@@ -54,7 +54,7 @@ media_info_table_options = {
} else if (rowData['media_type'] === 'album') {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Tracks"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else if (rowData['media_type'] === 'photo' && rowData['parent_rating_key'] == '') {
} else if (rowData['media_type'] === 'photo_album') {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Photos"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else {
@@ -77,32 +77,44 @@ media_info_table_options = {
if (rowData['media_type'] === 'movie') {
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + parent_info + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'show') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="TV Show"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'season') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Season"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">E' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">E' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'artist') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Artist"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'album') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Album"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">T' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">T' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'photo_album') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo Album"><i class="fa fa-camera fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else if (rowData['media_type'] === 'photo') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else if (rowData['media_type'] === 'clip') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Video"><i class="fa fa-video-camera fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else {
$(td).html(cellData);
}
@@ -335,7 +347,7 @@ function childTableOptionsMedia(rowData) {
case 'album':
section_type = 'track';
break;
case 'photo':
case 'photo_album':
section_type = 'picture';
break;
}

View File

@@ -3,7 +3,7 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">

View File

@@ -30,7 +30,7 @@ DOCUMENTATION :: END
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">

View File

@@ -29,7 +29,7 @@ DOCUMENTATION :: END
headers = {'movie': ('Movie Libraries', ('Movies', '', '')),
'show': ('TV Show Libraries', ('Shows', 'Seasons', 'Episodes')),
'artist': ('Music Libraries', ('Artists', 'Albums', 'Tracks')),
'photo': ('Photo Libraries', ('Albums', '', 'Photos'))}
'photo': ('Photo Libraries', ('Albums', 'Photos', 'Videos'))}
%>
% for section_type in types:
% if section_type in data:

View File

@@ -9,7 +9,7 @@
<meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
<link href="${http_root}css/plexpy.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">

View File

@@ -5,7 +5,7 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
<style>
td {word-break: break-all;}
</style>
@@ -21,9 +21,9 @@
<span><i class="fa fa-list-alt"></i> Logs</span>
</div>
<div class="button-bar">
<div class="btn-group" id="plexpy-log-levels">
<div class="btn-group" id="tautulli-log-levels">
<label>
<select name="plexpy-log-level-filter" id="plexpy-log-level-filter" class="btn" style="color: inherit;">
<select name="tautulli-log-level-filter" id="tautulli-log-level-filter" class="btn" style="color: inherit;">
<option value="">All log levels</option>
<option disabled>&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;</option>
<option value="DEBUG">Debug</option>
@@ -45,7 +45,7 @@
</select>
</label>
</div>
<button class="btn btn-dark" id="download-plexpylog"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="download-tautullilog"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="download-plexserverlog" style="display: none;"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="download-plexscannerlog" style="display: none;"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="clear-logs"><i class="fa fa-trash-o"></i> Clear logs</button>
@@ -56,17 +56,17 @@
<div class='table-card-back'>
<div>
<ul id="log_tabs" class="nav nav-pills" role="tablist">
<li role="presentation" class="active"><a id="plexpy-logs-btn" href="#tabs-plexpy_log" aria-controls="tabs-plexpy_log" role="tab" data-toggle="tab">Tautulli Logs</a></li>
<li role="presentation"><a id="plexpy-api-logs-btn" href="#tabs-plexpy_api_log" aria-controls="tabs-plexpy_api_log" role="tab" data-toggle="tab">Tautulli API Logs</a></li>
<li role="presentation" class="active"><a id="tautulli-logs-btn" href="#tabs-tautulli_log" aria-controls="tabs-tautulli_log" role="tab" data-toggle="tab">Tautulli Logs</a></li>
<li role="presentation"><a id="tautulli-api-logs-btn" href="#tabs-tautulli_api_log" aria-controls="tabs-tautulli_api_log" role="tab" data-toggle="tab">Tautulli API Logs</a></li>
<li role="presentation"><a id="plex-logs-btn" href="#tabs-plex_log" aria-controls="tabs-plex_log" role="tab" data-toggle="tab">Plex Media Server Logs</a></li>
<li role="presentation"><a id="plex-scanner-logs-btn" href="#tabs-plex_scanner_log" aria-controls="tabs-plex_scanner_log" role="tab" data-toggle="tab">Plex Media Scanner Logs</a></li>
<li role="presentation"><a id="plexpy-websocket-logs-btn" href="#tabs-plex_websocket_log" aria-controls="tabs-plex_websocket_log" role="tab" data-toggle="tab">Plex Websocket Logs</a></li>
<li role="presentation"><a id="plex-websocket-logs-btn" href="#tabs-plex_websocket_log" aria-controls="tabs-plex_websocket_log" role="tab" data-toggle="tab">Plex Websocket Logs</a></li>
<li role="presentation"><a id="notification-logs-btn" href="#tabs-notification_log" aria-controls="tabs-notification_log" role="tab" data-toggle="tab">Notification Logs</a></li>
<li role="presentation"><a id="login-logs-btn" href="#tabs-login_log" aria-controls="tabs-login_log" role="tab" data-toggle="tab">Login Logs</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-plexpy_log" data-logfile="plexpy">
<table class="display" id="plexpy_log_table" width="100%">
<div role="tabpanel" class="tab-pane active" id="tabs-tautulli_log" data-logfile="tautulli">
<table class="display" id="tautulli_log_table" width="100%">
<thead>
<tr>
<th class="min-tablet" align="left" id="timestamp">Timestamp</th>
@@ -77,8 +77,8 @@
<tbody></tbody>
</table>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-plexpy_api_log" data-logfile="plexpy_api">
<table class="display" id="plexpy_api_log_table" width="100%">
<div role="tabpanel" class="tab-pane" id="tabs-tautulli_api_log" data-logfile="tautulli_api">
<table class="display" id="tautulli_api_log_table" width="100%">
<thead>
<tr>
<th class="min-tablet" align="left" id="timestamp">Timestamp</th>
@@ -195,8 +195,8 @@
<script>
$(document).ready(function() {
loadPlexPyLogs('plexpy', selected_log_level);
clearSearchButton('plexpy_log_table', log_table);
loadtautullilogs('tautulli', selected_log_level);
clearSearchButton('tautulli_log_table', log_table);
});
var log_levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
@@ -227,7 +227,7 @@
}
var selected_log_level = null;
function loadPlexPyLogs(logfile, selected_log_level) {
function loadtautullilogs(logfile, selected_log_level) {
log_table_options.ajax = {
url: "get_log",
type: 'post',
@@ -238,10 +238,10 @@
log_level: selected_log_level
};
}
}
};
log_table = $('#' + logfile + '_log_table').DataTable(log_table_options);
$('#plexpy-log-level-filter').on('change', function () {
$('#tautulli-log-level-filter').on('change', function () {
selected_log_level = $(this).val() || null;
log_table.draw();
});
@@ -250,7 +250,7 @@
function loadPlexLogs() {
plex_log_table_options.ajax = {
url: "get_plex_log?log_type=server"
}
};
plex_log_table_options.initComplete = bindLogLevelFilter;
plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options);
}
@@ -258,7 +258,7 @@
function loadPlexScannerLogs() {
plex_log_table_options.ajax = {
url: "get_plex_log?log_type=scanner"
}
};
plex_log_table_options.initComplete = bindLogLevelFilter;
plex_scanner_log_table = $('#plex_scanner_log_table').DataTable(plex_log_table_options);
}
@@ -271,7 +271,7 @@
json_data: JSON.stringify(d)
};
}
}
};
notification_log_table = $('#notification_log_table').DataTable(notification_log_table_options);
}
@@ -284,56 +284,56 @@
json_data: JSON.stringify(d)
};
}
}
};
login_log_table = $('#login_log_table').DataTable(login_log_table_options);
}
$("#plexpy-logs-btn").click(function () {
$("#plexpy-log-levels").show();
$("#tautulli-logs-btn").click(function () {
$("#tautulli-log-levels").show();
$("#plex-log-levels").hide();
$("#clear-logs").show();
$("#download-plexpylog").show()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").show();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-login-logs").hide();
loadPlexPyLogs('plexpy', selected_log_level);
clearSearchButton('plexpy_log_table', log_table);
loadtautullilogs('tautulli', selected_log_level);
clearSearchButton('tautulli_log_table', log_table);
});
$("#plexpy-api-logs-btn").click(function () {
$("#plexpy-log-levels").show();
$("#tautulli-api-logs-btn").click(function () {
$("#tautulli-log-levels").show();
$("#plex-log-levels").hide();
$("#clear-logs").show();
$("#download-plexpylog").show()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").show();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-login-logs").hide();
loadPlexPyLogs('plexpy_api', selected_log_level);
clearSearchButton('plexpy_api_log_table', log_table);
loadtautullilogs('tautulli_api', selected_log_level);
clearSearchButton('tautulli_api_log_table', log_table);
});
$("#plexpy-websocket-logs-btn").click(function () {
$("#plexpy-log-levels").show();
$("#plex-websocket-logs-btn").click(function () {
$("#tautulli-log-levels").show();
$("#plex-log-levels").hide();
$("#clear-logs").show();
$("#download-plexpylog").show()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").show();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-login-logs").hide();
loadPlexPyLogs('plex_websocket', selected_log_level);
loadtautullilogs('plex_websocket', selected_log_level);
clearSearchButton('plex_websocket_log_table', log_table);
});
$("#plex-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#tautulli-log-levels").hide();
$("#plex-log-levels").show();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").show()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").hide();
$("#download-plexserverlog").show();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-login-logs").hide();
loadPlexLogs();
@@ -341,12 +341,12 @@
});
$("#plex-scanner-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#tautulli-log-levels").hide();
$("#plex-log-levels").show();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").show()
$("#download-tautullilog").hide();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").show();
$("#clear-notify-logs").hide();
$("#clear-login-logs").hide();
loadPlexScannerLogs();
@@ -354,12 +354,12 @@
});
$("#notification-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#tautulli-log-levels").hide();
$("#plex-log-levels").hide();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").hide();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").show();
$("#clear-login-logs").hide();
loadNotificationLogs();
@@ -367,12 +367,12 @@
});
$("#login-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#tautulli-log-levels").hide();
$("#plex-log-levels").hide();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").hide();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-login-logs").show();
loadLoginLogs();
@@ -384,8 +384,8 @@
});
$("#clear-logs").click(function () {
var logfile = $(".tab-pane.active").data('logfile')
var title = $("#log_tabs li.active a").text()
var logfile = $(".tab-pane.active").data('logfile');
var title = $("#log_tabs li.active a").text();
$("#confirm-message").text("Are you sure you want to clear the " + title + "?");
$('#confirm-modal').modal();
@@ -397,7 +397,7 @@
complete: function (xhr, status) {
result = $.parseJSON(xhr.responseText);
msg = result.message;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
@@ -408,7 +408,7 @@
});
});
$("#download-plexpylog").click(function () {
$("#download-tautullilog").click(function () {
var logfile = $(".tab-pane.active").data('logfile');
window.location.href = "download_log?logfile=" + logfile;
});
@@ -431,7 +431,7 @@
complete: function (xhr, status) {
result = $.parseJSON(xhr.responseText);
msg = result.message;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
@@ -452,7 +452,7 @@
complete: function (xhr, status) {
result = $.parseJSON(xhr.responseText);
msg = result.message;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
@@ -473,10 +473,10 @@
{
clearInterval(timer);
}
if(refreshrate.value != 0)
if(refreshrate.value !== 0)
{
timer = setInterval(function() {
if ($("#tabs-plexpy_log").hasClass("active") || $("#tabs-plexpy_api_log").hasClass("active") || $("#tabs-plex_websocket_log").hasClass("active")) {
if ($("#tabs-tautulli_log").hasClass("active") || $("#tabs-tautulli_api_log").hasClass("active") || $("#tabs-plex_websocket_log").hasClass("active")) {
log_table.ajax.reload();
} else if ($("#tabs-plex_log").hasClass("active")) {
plex_log_table.ajax.reload();

View File

@@ -606,6 +606,22 @@
});
});
% elif notifier['agent_name'] == 'pushover':
function pushoverPriority() {
if ($('#pushover_priority').val() == '2') {
$('#pushover_retry').closest('.form-group').show();
$('#pushover_expire').closest('.form-group').show();
} else {
$('#pushover_retry').closest('.form-group').hide();
$('#pushover_expire').closest('.form-group').hide();
}
}
pushoverPriority();
$('#pushover_priority').change( function () {
pushoverPriority();
});
% endif
function validateLogic() {

View File

@@ -60,9 +60,9 @@
<input type="hidden" id="show_advanced_settings" name="show_advanced_settings" value="${config['show_advanced_settings']}" required>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-help_info">
% if common.VERSION_NUMBER:
% if common.RELEASE:
<div class="padded-header">
<h3>Version ${common.VERSION_NUMBER} <small><a id="changelog-modal-link" href="#"><i class="fa fa-info-circle"></i> Changelog</a></small></h3>
<h3>Version ${common.RELEASE} <small><a id="changelog-modal-link" href="#"><i class="fa fa-info-circle"></i> Changelog</a></small></h3>
</div>
% endif
<div class="padded-header">

View File

@@ -2,7 +2,7 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<style>
td {word-wrap: break-word}

View File

@@ -32,7 +32,7 @@ DOCUMENTATION :: END
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">

View File

@@ -3,7 +3,7 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">

View File

@@ -14,7 +14,7 @@
<meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/bootstrap-wizard.css" rel="stylesheet">
<link href="${http_root}css/plexpy.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">

View File

@@ -1,54 +1,54 @@
#!/bin/sh
#
# PROVIDE: plexpy
# REQUIRE: plexpy
# PROVIDE: tautulli
# REQUIRE: tautulli
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# plexpy_enable (bool): Set to NO by default.
# tautulli_enable (bool): Set to NO by default.
# Set it to YES to enable it.
# plexpy_user: The user account PlexPy daemon runs as what
# you want it to be. It uses 'plexpy' user by
# tautulli_user: The user account Tautulli daemon runs as what
# you want it to be. It uses 'tautulli' user by
# default. Do not sets it as empty or it will run
# as root.
# plexpy_dir: Directory where PlexPy lives.
# Default: /usr/local/plexpy
# plexpy_chdir: Change to this directory before running PlexPy.
# Default is same as plexpy_dir.
# plexpy_pid: The name of the pidfile to create.
# Default is plexpy.pid in plexpy_dir.
# tautulli_dir: Directory where Tautulli lives.
# Default: /usr/local/share/Tautulli
# tautulli_chdir: Change to this directory before running Tautulli.
# Default is same as tautulli_dir.
# tautulli_pid: The name of the pidfile to create.
# Default is tautulli.pid in tautulli_dir.
. /etc/rc.subr
name="plexpy"
name="tautulli"
rcvar=${name}_enable
load_rc_config ${name}
: ${plexpy_enable:="NO"}
: ${plexpy_user:="plexpy"}
: ${plexpy_dir:="/usr/local/plexpy"}
: ${plexpy_chdir:="${plexpy_dir}"}
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
: ${plexpy_conf:="${plexpy_dir}/config.ini"}
: ${tautulli_enable:="NO"}
: ${tautulli_user:="tautulli"}
: ${tautulli_dir:="/usr/local/share/Tautulli"}
: ${tautulli_chdir:="${tautulli_dir}"}
: ${tautulli_pid:="${tautulli_dir}/tautulli.pid"}
: ${tautulli_conf:="${tautulli_dir}/config.ini"}
WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown PlexPy.
if [ -e "${plexpy_conf}" ]; then
HOST=`grep -A64 "\[General\]" "${plexpy_conf}"|egrep "^http_host"|perl -wple 's/^http_host = (.*)$/$1/'`
PORT=`grep -A64 "\[General\]" "${plexpy_conf}"|egrep "^http_port"|perl -wple 's/^http_port = (.*)$/$1/'`
WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown Tautulli.
if [ -e "${tautulli_conf}" ]; then
HOST=`grep -A64 "\[General\]" "${tautulli_conf}"|egrep "^http_host"|perl -wple 's/^http_host = (.*)$/$1/'`
PORT=`grep -A64 "\[General\]" "${tautulli_conf}"|egrep "^http_port"|perl -wple 's/^http_port = (.*)$/$1/'`
fi
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="${plexpy_dir}/PlexPy.py"
command_args="--daemon --quiet --nolaunch --port ${PORT} --pidfile ${plexpy_pid} --config ${plexpy_conf}"
command="${tautulli_dir}/Tautulli.py"
command_args="--daemon --quiet --nolaunch --port ${PORT} --pidfile ${tautulli_pid} --config ${tautulli_conf}"
# Check for wget and refuse to start without it.
if [ ! -x "${WGET}" ]; then
warn "PlexPy not started: You need wget to safely shut down PlexPy."
warn "Tautulli not started: You need wget to safely shut down Tautulli."
exit 1
fi
@@ -58,21 +58,21 @@ if [ `id -u` != "0" ]; then
exit 1
fi
verify_plexpy_pid() {
# Make sure the pid corresponds to the PlexPy process.
pid=`cat ${plexpy_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python ${plexpy_dir}/PlexPy.py"
verify_tautulli_pid() {
# Make sure the pid corresponds to the Tautulli process.
pid=`cat ${tautulli_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python ${tautulli_dir}/Tautulli.py"
return $?
}
# Try to stop PlexPy cleanly by calling shutdown over http.
plexpy_stop() {
if [ ! -e "${plexpy_conf}" ]; then
echo "PlexPy' settings file does not exist. Try starting PlexPy, as this should create the file."
# Try to stop Tautulli cleanly by calling shutdown over http.
tautulli_stop() {
if [ ! -e "${tautulli_conf}" ]; then
echo "Tautulli' settings file does not exist. Try starting Tautulli, as this should create the file."
exit 1
fi
echo "Stopping $name"
verify_plexpy_pid
verify_tautulli_pid
${WGET} -O - -q --user=${SBUSR} --password=${SBPWD} "http://${HOST}:${PORT}/shutdown/" >/dev/null
if [ -n "${pid}" ]; then
@@ -81,8 +81,8 @@ plexpy_stop() {
fi
}
plexpy_status() {
verify_plexpy_pid && echo "$name is running as ${pid}" || echo "$name is not running"
tautulli_status() {
verify_tautulli_pid && echo "$name is running as ${pid}" || echo "$name is not running"
}
run_rc_command "$1"

View File

@@ -1,25 +1,25 @@
#!/bin/sh
#
### BEGIN INIT INFO
# Provides: PlexPy
# Provides: Tautulli
# Required-Start: $all
# Required-Stop: $all
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: starts PlexPy
# Description: starts PlexPy
# Short-Description: starts Tautulli
# Description: starts Tautulli
### END INIT INFO
# Source function library.
. /etc/init.d/functions
## Variables
prog=plexpy
prog=tautulli
lockfile=/var/lock/subsys/$prog
homedir=/opt/plexpy
datadir=/opt/plexpy
configfile=/opt/plexpy/config.ini
pidfile=/var/run/plexpy.pid
homedir=/opt/Tautulli
datadir=/opt/Tautulli
configfile=/opt/Tautulli/config.ini
pidfile=/var/run/tautulli.pid
nice=
# The following line must point to your Python 2.7 install
python27=/usr/src/Python-2.7.11/python
@@ -30,7 +30,7 @@ options=" --daemon --config $configfile --pidfile $pidfile --datadir $datadir --
start() {
# Start daemon.
echo -n $"Starting $prog: "
daemon --pidfile=$pidfile $nice $python27 $homedir/PlexPy.py $options
daemon --pidfile=$pidfile $nice $python27 $homedir/Tautulli.py $options
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch $lockfile

View File

@@ -1,45 +1,45 @@
#!/bin/sh
#
# PROVIDE: plexpy
# REQUIRE: DAEMON plexpy
# PROVIDE: tautulli
# REQUIRE: DAEMON tautulli
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# plexpy_enable (bool): Set to NO by default.
# tautulli_enable (bool): Set to NO by default.
# Set it to YES to enable it.
# plexpy_user: The user account PlexPy daemon runs as what
# you want it to be. It uses 'plexpy' user by
# tautulli_user: The user account Tautulli daemon runs as what
# you want it to be. It uses 'tautulli' user by
# default. Do not sets it as empty or it will run
# as root.
# plexpy_dir: Directory where PlexPy lives.
# Default: /usr/local/share/plexpy
# plexpy_chdir: Change to this directory before running PlexPy.
# Default is same as plexpy_dir.
# plexpy_pid: The name of the pidfile to create.
# Default is plexpy.pid in plexpy_dir.
# tautulli_dir: Directory where Tautulli lives.
# Default: /usr/local/share/Tautulli
# tautulli_chdir: Change to this directory before running Tautulli.
# Default is same as tautulli_dir.
# tautulli_pid: The name of the pidfile to create.
# Default is tautulli.pid in tautulli_dir.
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
. /etc/rc.subr
name="plexpy"
name="tautulli"
rcvar=${name}_enable
load_rc_config ${name}
: ${plexpy_enable:="NO"}
: ${plexpy_user:="plexpy"}
: ${plexpy_dir:="/usr/local/share/plexpy"}
: ${plexpy_chdir:="${plexpy_dir}"}
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
: ${plexpy_flags:=""}
: ${tautulli_enable:="NO"}
: ${tautulli_user:="tautulli"}
: ${tautulli_dir:="/usr/local/share/Tautulli"}
: ${tautulli_chdir:="${tautulli_dir}"}
: ${tautulli_pid:="${tautulli_dir}/tautulli.pid"}
: ${tautulli_flags:=""}
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="${plexpy_dir}/PlexPy.py"
command_args="--daemon --pidfile ${plexpy_pid} --quiet --nolaunch ${plexpy_flags}"
command="${tautulli_dir}/Tautulli.py"
command_args="--daemon --pidfile ${tautulli_pid} --quiet --nolaunch ${tautulli_flags}"
# Ensure user is root when running this script.
if [ `id -u` != "0" ]; then
@@ -47,21 +47,21 @@ if [ `id -u` != "0" ]; then
exit 1
fi
verify_plexpy_pid() {
# Make sure the pid corresponds to the PlexPy process.
if [ -f ${plexpy_pid} ]; then
pid=`cat ${plexpy_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python2 ${plexpy_dir}/PlexPy.py"
verify_tautulli_pid() {
# Make sure the pid corresponds to the Tautulli process.
if [ -f ${tautulli_pid} ]; then
pid=`cat ${tautulli_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python2 ${tautulli_dir}/Tautulli.py"
return $?
else
return 0
fi
}
# Try to stop PlexPy cleanly by sending SIGTERM
plexpy_stop() {
# Try to stop Tautulli cleanly by sending SIGTERM
tautulli_stop() {
echo "Stopping $name"
verify_plexpy_pid
verify_tautulli_pid
if [ -n "${pid}" ]; then
kill ${pid}
wait_for_pids ${pid}
@@ -69,8 +69,8 @@ plexpy_stop() {
fi
}
plexpy_status() {
verify_plexpy_pid
tautulli_status() {
verify_tautulli_pid
if [ -n "${pid}" ]; then
echo "$name is running as ${pid}."
else

View File

@@ -1,45 +1,45 @@
#!/bin/sh
#
# PROVIDE: plexpy
# REQUIRE: DAEMON plexpy
# PROVIDE: tautulli
# REQUIRE: DAEMON tautulli
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# plexpy_enable (bool): Set to NO by default.
# tautulli_enable (bool): Set to NO by default.
# Set it to YES to enable it.
# plexpy_user: The user account PlexPy daemon runs as what
# you want it to be. It uses 'plexpy' user by
# tautulli_user: The user account Tautulli daemon runs as what
# you want it to be. It uses 'tautulli' user by
# default. Do not sets it as empty or it will run
# as root.
# plexpy_dir: Directory where PlexPy lives.
# Default: /usr/local/share/plexpy
# plexpy_chdir: Change to this directory before running PlexPy.
# Default is same as plexpy_dir.
# plexpy_pid: The name of the pidfile to create.
# Default is plexpy.pid in plexpy_dir.
# tautulli_dir: Directory where Tautulli lives.
# Default: /usr/local/share/Tautulli
# tautulli_chdir: Change to this directory before running Tautulli.
# Default is same as tautulli_dir.
# tautulli_pid: The name of the pidfile to create.
# Default is tautulli.pid in tautulli_dir.
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
. /etc/rc.subr
name="plexpy"
name="tautulli"
rcvar=${name}_enable
load_rc_config ${name}
: ${plexpy_enable:="NO"}
: ${plexpy_user:="plexpy"}
: ${plexpy_dir:="/usr/local/share/plexpy"}
: ${plexpy_chdir:="${plexpy_dir}"}
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
: ${plexpy_flags:=""}
: ${tautulli_enable:="NO"}
: ${tautulli_user:="tautulli"}
: ${tautulli_dir:="/usr/local/share/Tautulli"}
: ${tautulli_chdir:="${tautulli_dir}"}
: ${tautulli_pid:="${tautulli_dir}/tautulli.pid"}
: ${tautulli_flags:=""}
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="${plexpy_dir}/PlexPy.py"
command_args="--daemon --pidfile ${plexpy_pid} --quiet --nolaunch ${plexpy_flags}"
command="${tautulli_dir}/Tautulli.py"
command_args="--daemon --pidfile ${tautulli_pid} --quiet --nolaunch ${tautulli_flags}"
# Ensure user is root when running this script.
if [ `id -u` != "0" ]; then
@@ -47,21 +47,21 @@ if [ `id -u` != "0" ]; then
exit 1
fi
verify_plexpy_pid() {
# Make sure the pid corresponds to the PlexPy process.
if [ -f ${plexpy_pid} ]; then
pid=`cat ${plexpy_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python2 ${plexpy_dir}/PlexPy.py"
verify_tautulli_pid() {
# Make sure the pid corresponds to the Tautulli process.
if [ -f ${tautulli_pid} ]; then
pid=`cat ${tautulli_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python2 ${tautulli_dir}/Tautulli.py"
return $?
else
return 0
fi
}
# Try to stop PlexPy cleanly by sending SIGTERM
plexpy_stop() {
# Try to stop Tautulli cleanly by sending SIGTERM
tautulli_stop() {
echo "Stopping $name."
verify_plexpy_pid
verify_tautulli_pid
if [ -n "${pid}" ]; then
kill ${pid}
wait_for_pids ${pid}
@@ -69,8 +69,8 @@ plexpy_stop() {
fi
}
plexpy_status() {
verify_plexpy_pid
tautulli_status() {
verify_tautulli_pid
if [ -n "${pid}" ]; then
echo "$name is running as ${pid}."
else

View File

@@ -3,12 +3,12 @@
<plist version="1.0">
<dict>
<key>Label</key>
<string>plexpy</string>
<string>tautulli</string>
<key>ProgramArguments</key>
<array>
<!-- Modify these two lines if you need to to reflect your python location and PlexPy install location -->
<!-- Modify these two lines if you need to to reflect your python location and Tautulli install location -->
<string>/usr/bin/python</string>
<string>/Applications/PlexPy/PlexPy.py</string>
<string>/Applications/Tautulli/Tautulli.py</string>
</array>
<key>RunAtLoad</key>
<true/>

View File

@@ -2,9 +2,9 @@
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
<!--
Created by Manifold
--><service_bundle type="manifest" name="plexpy">
--><service_bundle type="manifest" name="tautulli">
<service name="application/plexpy" type="service" version="1">
<service name="application/tautulli" type="service" version="1">
<create_default_instance enabled="true"/>
@@ -19,10 +19,10 @@
</dependency>
<method_context>
<method_credential user="plexpy" group="nogroup"/>
<method_credential user="tautulli" group="nogroup"/>
</method_context>
<exec_method type="method" name="start" exec="python /opt/plexpy/PlexPy.py --daemon --quiet --nolaunch" timeout_seconds="60"/>
<exec_method type="method" name="start" exec="python /opt/Tautulli/Tautulli.py --daemon --quiet --nolaunch" timeout_seconds="60"/>
<exec_method type="method" name="stop" exec=":kill" timeout_seconds="60"/>
@@ -37,7 +37,7 @@
<template>
<common_name>
<loctext xml:lang="C">
PlexPy
Tautulli
</loctext>
</common_name>
</template>

View File

@@ -1,11 +1,11 @@
# PlexPy - Stats for Plex Media Server usage
# Tautulli - Stats for Plex Media Server usage
#
# Service Unit file for systemd system manager
#
# INSTALLATION NOTES
#
# 1. Rename this file as you want, ensuring that it ends in .service
# e.g. 'plexpy.service'
# e.g. 'tautulli.service'
#
# 2. Adjust configuration settings as required. More details in the
# "CONFIGURATION NOTES" section shown below.
@@ -15,39 +15,39 @@
#
# 4. Enable boot-time autostart with the following commands:
# systemctl daemon-reload
# systemctl enable plexpy.service
# systemctl enable tautulli.service
#
# 5. Start now with the following command:
# systemctl start plexpy.service
# systemctl start tautulli.service
#
# CONFIGURATION NOTES
#
# - The example settings in this file assume that you will run PlexPy as user: plexpy
# - To create this user and give it ownership of the plexpy directory:
# sudo adduser --system --no-create-home plexpy
# sudo chown plexpy:nogroup -R /opt/plexpy
# - The example settings in this file assume that you will run Tautulli as user: tautulli
# - To create this user and give it ownership of the tautulli directory:
# sudo adduser --system --no-create-home tautulli
# sudo chown tautulli:nogroup -R /opt/Tautulli
#
# - Option names (e.g. ExecStart=, Type=) appear to be case-sensitive)
#
# - Adjust ExecStart= to point to:
# 1. Your PlexPy executable,
# 1. Your Tautulli executable,
# 2. Your config file (recommended is to put it somewhere in /etc)
# 3. Your datadir (recommended is to NOT put it in your PlexPy exec dir)
# 3. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
#
# - Adjust User= and Group= to the user/group you want PlexPy to run as.
# - Adjust User= and Group= to the user/group you want Tautulli to run as.
#
# - WantedBy= specifies which target (i.e. runlevel) to start PlexPy for.
# - WantedBy= specifies which target (i.e. runlevel) to start Tautulli for.
# multi-user.target equates to runlevel 3 (multi-user text mode)
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
[Unit]
Description=PlexPy - Stats for Plex Media Server usage
Description=Tautulli - Stats for Plex Media Server usage
[Service]
ExecStart=/opt/plexpy/PlexPy.py --quiet --daemon --nolaunch --config /opt/plexpy/config.ini --datadir /opt/plexpy
ExecStart=/opt/Tautulli/Tautulli.py --quiet --daemon --nolaunch --config /opt/Tautulli/config.ini --datadir /opt/Tautulli
GuessMainPID=no
Type=forking
User=plexpy
User=tautulli
Group=nogroup
[Install]

View File

@@ -1,71 +1,71 @@
#!/bin/sh
#
## Don't edit this file
## Edit user configuation in /etc/default/plexpy to change
## Edit user configuation in /etc/default/tautulli to change
##
## Make sure init script is executable
## sudo chmod +x /path/to/init.ubuntu
##
## Install the init script
## sudo ln -s /path/to/init.ubuntu /etc/init.d/plexpy
## sudo ln -s /path/to/init.ubuntu /etc/init.d/tautulli
##
## Create the plexpy daemon user:
## sudo adduser --system --no-create-home plexpy
## Create the tautulli daemon user:
## sudo adduser --system --no-create-home tautulli
##
## Make sure /opt/plexpy is owned by the plexpy user
## sudo chown plexpy:nogroup -R /opt/plexpy
## Make sure /opt/Tautulli is owned by the tautulli user
## sudo chown tautulli:nogroup -R /opt/Tautulli
##
## Touch the default file to stop the warning message when starting
## sudo touch /etc/default/plexpy
## sudo touch /etc/default/tautulli
##
## To start PlexPy automatically
## sudo update-rc.d plexpy defaults
## To start Tautulli automatically
## sudo update-rc.d tautulli defaults
##
## To start/stop/restart PlexPy
## sudo service plexpy start
## sudo service plexpy stop
## sudo service plexpy restart
## To start/stop/restart Tautulli
## sudo service tautulli start
## sudo service tautulli stop
## sudo service tautulli restart
##
## HP_USER= #$RUN_AS, username to run plexpy under, the default is plexpy
## HP_HOME= #$APP_PATH, the location of PlexPy.py, the default is /opt/plexpy
## HP_DATA= #$DATA_DIR, the location of plexpy.db, cache, logs, the default is /opt/plexpy
## HP_PIDFILE= #$PID_FILE, the location of plexpy.pid, the default is /var/run/plexpy/plexpy.pid
## TAUTULLI_USER= #$RUN_AS, username to run Tautulli under, the default is tautulli
## TAUTULLI_HOME= #$APP_PATH, the location of Tautulli.py, the default is /opt/Tautulli
## TAUTULLI_DATA= #$DATA_DIR, the location of plexpy.db, cache, logs, the default is /opt/Tautulli
## TAUTULLI_PIDFILE= #$PID_FILE, the location of tautulli.pid, the default is /var/run/tautulli/tautulli.pid
## PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python
## HP_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for plexpy, i.e. " --config=/home/plexpy/config.ini"
## TAUTULLI_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for Tautulli, i.e. " --config=/home/Tautulli/config.ini"
## SSD_OPTS= #$EXTRA_SSD_OPTS, extra start-stop-daemon option like " --group=users"
## HP_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini
## TAUTULLI_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini
##
## EXAMPLE if want to run as different user
## add HP_USER=username to /etc/default/plexpy
## otherwise default plexpy is used
## add TAUTULLI_USER=username to /etc/default/tautulli
## otherwise default tautulli is used
#
### BEGIN INIT INFO
# Provides: plexpy
# Provides: tautulli
# Required-Start: $local_fs $network $remote_fs
# Required-Stop: $local_fs $network $remote_fs
# Should-Start: $NetworkManager
# Should-Stop: $NetworkManager
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: starts instance of PlexPy
# Description: starts instance of PlexPy using start-stop-daemon
# Short-Description: starts instance of Tautulli
# Description: starts instance of Tautulli using start-stop-daemon
### END INIT INFO
# Script name
NAME=plexpy
NAME=tautulli
# App name
DESC=PlexPy
DESC=Tautulli
SETTINGS_LOADED=FALSE
. /lib/lsb/init-functions
# Source PlexPy configuration
if [ -f /etc/default/plexpy ]; then
SETTINGS=/etc/default/plexpy
# Source Tautulli configuration
if [ -f /etc/default/tautulli ]; then
SETTINGS=/etc/default/tautulli
else
log_warning_msg "/etc/default/plexpy not found using default settings.";
log_warning_msg "/etc/default/tautulli not found using default settings.";
fi
check_retval() {
@@ -84,32 +84,32 @@ load_settings() {
## The defaults
# Run as username
RUN_AS=${HP_USER-plexpy}
RUN_AS=${TAUTULLI_USER-tautulli}
# Path to app HP_HOME=path_to_app_PlexPy.py
APP_PATH=${HP_HOME-/opt/plexpy}
# Path to app TAUTULLI_HOME=path_to_app_Tautulli.py
APP_PATH=${TAUTULLI_HOME-/opt/Tautulli}
# Data directory where plexpy.db, cache and logs are stored
DATA_DIR=${HP_DATA-/opt/plexpy}
DATA_DIR=${TAUTULLI_DATA-/opt/Tautulli}
# Path to store PID file
PID_FILE=${HP_PIDFILE-/var/run/plexpy/plexpy.pid}
PID_FILE=${TAUTULLI_PIDFILE-/var/run/tautulli/tautulli.pid}
# Path to python bin
DAEMON=${PYTHON_BIN-/usr/bin/python}
# Extra daemon option like: HP_OPTS=" --config=/home/plexpy/config.ini"
EXTRA_DAEMON_OPTS=${HP_OPTS-}
# Extra daemon option like: TAUTULLI_OPTS=" --config=/home/Tautulli/config.ini"
EXTRA_DAEMON_OPTS=${TAUTULLI_OPTS-}
# Extra start-stop-daemon option like START_OPTS=" --group=users"
EXTRA_SSD_OPTS=${SSD_OPTS-}
# Hardcoded port to run on, overrides config.ini settings
[ -n "$HP_PORT" ] && {
PORT_OPTS=" --port=${HP_PORT} "
[ -n "$TAUTULLI_PORT" ] && {
PORT_OPTS=" --port=${TAUTULLI_PORT} "
}
DAEMON_OPTS=" PlexPy.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS}${EXTRA_DAEMON_OPTS}"
DAEMON_OPTS=" Tautulli.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS}${EXTRA_DAEMON_OPTS}"
SETTINGS_LOADED=TRUE
fi
@@ -162,7 +162,7 @@ handle_updates () {
return 0; }
}
start_plexpy () {
start_tautulli () {
handle_pid
handle_datadir
handle_updates
@@ -175,7 +175,7 @@ start_plexpy () {
fi
}
stop_plexpy () {
stop_tautulli () {
if is_running; then
log_daemon_msg "Stopping $DESC"
start-stop-daemon -o --stop --pidfile $PID_FILE --retry 15
@@ -187,14 +187,14 @@ stop_plexpy () {
case "$1" in
start)
start_plexpy
start_tautulli
;;
stop)
stop_plexpy
stop_tautulli
;;
restart|force-reload)
stop_plexpy
start_plexpy
stop_tautulli
start_tautulli
;;
status)
status_of_proc -p "$PID_FILE" "$DAEMON" "$DESC"

View File

@@ -1,18 +1,18 @@
# plexpy
# tautulli
#
# This is a session/user job. Install this file into /usr/share/upstart/sessions
# if plexpy is installed system wide, and into $XDG_CONFIG_HOME/upstart if
# plexpy is installed per user. Change the executable path appropiately.
# if Tautulli is installed system wide, and into $XDG_CONFIG_HOME/upstart if
# Tautulli is installed per user. Change the executable path appropiately.
start on desktop-start
stop on desktop-end
env CONFIG=""$XDG_CONFIG_HOME"/plexpy"
env DATA=""$XDG_DATA_HOME"/plexpy"
env CONFIG=""$XDG_CONFIG_HOME"/Tautulli"
env DATA=""$XDG_DATA_HOME"/Tautulli"
pre-start script
[ -d "$CONFIG" ] || mkdir -p "$CONFIG"
[ -d "$DATA" ] || mkdir -p "$DATA"
end script
exec PlexPy.py --nolaunch --config "$CONFIG"/config.ini --datadir "$DATA"
exec Tautulli.py --nolaunch --config "$CONFIG"/config.ini --datadir "$DATA"

View File

@@ -240,7 +240,7 @@ def initialize(config_file):
# Get the previous release from the file
release_file = os.path.join(DATA_DIR, "release.lock")
PREV_RELEASE = common.VERSION_NUMBER
PREV_RELEASE = common.RELEASE
if os.path.isfile(release_file):
try:
with open(release_file, "r") as fp:
@@ -252,7 +252,7 @@ def initialize(config_file):
PREV_RELEASE = 'v1.4.25'
# Check if the release was updated
if common.VERSION_NUMBER != PREV_RELEASE:
if common.RELEASE != PREV_RELEASE:
CONFIG.UPDATE_SHOW_CHANGELOG = 1
CONFIG.write()
_UPDATE = True
@@ -260,7 +260,7 @@ def initialize(config_file):
# Write current release version to file for update checking
try:
with open(release_file, "w") as fp:
fp.write(common.VERSION_NUMBER)
fp.write(common.RELEASE)
except IOError as e:
logger.error(u"Unable to write current release to file '%s': %s" %
(release_file, e))
@@ -1621,8 +1621,11 @@ def upgrade():
def shutdown(restart=False, update=False, checkout=False):
cherrypy.engine.exit()
SCHED.shutdown(wait=False)
activity_handler.ACTIVITY_SCHED.shutdown(wait=False)
if SCHED.running:
SCHED.shutdown(wait=False)
if activity_handler.ACTIVITY_SCHED.running:
activity_handler.ACTIVITY_SCHED.shutdown(wait=False)
# Stop the notification threads
for i in range(CONFIG.NOTIFICATION_THREADS):
@@ -1693,7 +1696,7 @@ def initialize_tracker():
data = {
'dataSource': 'server',
'appName': 'Tautulli',
'appVersion': common.VERSION_NUMBER,
'appVersion': common.RELEASE,
'appId': plexpy.INSTALL_TYPE,
'appInstallerId': plexpy.CONFIG.GIT_BRANCH,
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_VERSION), # App Platform
@@ -1702,7 +1705,8 @@ def initialize_tracker():
'noninteractive': True
}
tracker = Tracker.create('UA-111522699-2', client_id=CONFIG.PMS_UUID, hash_client_id=True)
tracker = Tracker.create('UA-111522699-2', client_id=CONFIG.PMS_UUID, hash_client_id=True,
user_agent=common.USER_AGENT)
tracker.set(data)
return tracker
@@ -1721,4 +1725,7 @@ def analytics_event(category, action, label=None, value=None, **kwargs):
data.update(kwargs)
if TRACKER:
TRACKER.send('event', data)
try:
TRACKER.send('event', data)
except Exception as e:
logger.warn(u"Failed to send analytics event for category '%s', action '%s': %s" % (category, action, e))

View File

@@ -74,9 +74,22 @@ class ActivityHandler(object):
return None
def update_db_session(self, session=None):
# Update our session temp table values
monitor_proc = activity_processor.ActivityProcessor()
monitor_proc.write_session(session=session, notify=False)
if session is None:
session = self.get_live_session()
if session:
# Update our session temp table values
ap = activity_processor.ActivityProcessor()
ap.write_session(session=session, notify=False)
self.set_session_state()
def set_session_state(self):
ap = activity_processor.ActivityProcessor()
ap.set_session_state(session_key=self.get_session_key(),
state=self.timeline['state'],
view_offset=self.timeline['viewOffset'],
stopped=int(time.time()))
def on_start(self):
if self.is_valid_session():
@@ -114,10 +127,7 @@ class ActivityHandler(object):
# Update the session state and viewOffset
# Set force_stop to true to disable the state set
if not force_stop:
ap.set_session_state(session_key=self.get_session_key(),
state=self.timeline['state'],
view_offset=self.timeline['viewOffset'],
stopped=int(time.time()))
self.set_session_state()
# Retrieve the session data from our temp table
db_session = ap.get_session_by_key(session_key=self.get_session_key())
@@ -150,10 +160,7 @@ class ActivityHandler(object):
ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=int(time.time()))
# Update the session state and viewOffset
ap.set_session_state(session_key=self.get_session_key(),
state=self.timeline['state'],
view_offset=self.timeline['viewOffset'],
stopped=int(time.time()))
self.update_db_session()
# Retrieve the session data from our temp table
db_session = ap.get_session_by_key(session_key=self.get_session_key())
@@ -170,10 +177,7 @@ class ActivityHandler(object):
ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=None)
# Update the session state and viewOffset
ap.set_session_state(session_key=self.get_session_key(),
state=self.timeline['state'],
view_offset=self.timeline['viewOffset'],
stopped=int(time.time()))
self.update_db_session()
# Retrieve the session data from our temp table
db_session = ap.get_session_by_key(session_key=self.get_session_key())
@@ -198,10 +202,7 @@ class ActivityHandler(object):
buffer_last_triggered = ap.get_session_buffer_trigger_time(self.get_session_key())
# Update the session state and viewOffset
ap.set_session_state(session_key=self.get_session_key(),
state=self.timeline['state'],
view_offset=self.timeline['viewOffset'],
stopped=int(time.time()))
self.update_db_session()
time_since_last_trigger = 0
if buffer_last_triggered:
@@ -243,9 +244,7 @@ class ActivityHandler(object):
# Update the session in our temp session table
# if the last set temporary stopped time exceeds 15 seconds
if int(time.time()) - db_session['stopped'] > 60:
session = self.get_live_session()
if session:
self.update_db_session(session=session)
self.update_db_session()
# Start our state checks
if this_state != last_state:

View File

@@ -167,8 +167,8 @@ class API2:
"""
logfile = os.path.join(plexpy.CONFIG.LOG_DIR, logger.FILENAME)
templog = []
start = int(kwargs.get('start', 0))
end = int(kwargs.get('end', 0))
start = int(start)
end = int(end)
if regex:
logger.api_debug(u'Tautulli APIv2 :: Filtering log using regex %s' % regex)

View File

@@ -19,13 +19,12 @@ from collections import OrderedDict
import version
# Identify Our Application
USER_AGENT = 'Tautulli/-' + version.PLEXPY_BRANCH + ' v' + version.PLEXPY_RELEASE_VERSION + ' (' + platform.system() + \
' ' + platform.release() + ')'
PLATFORM = platform.system()
PLATFORM_VERSION = platform.release()
BRANCH = version.PLEXPY_BRANCH
VERSION_NUMBER = version.PLEXPY_RELEASE_VERSION
RELEASE = version.PLEXPY_RELEASE_VERSION
USER_AGENT = 'Tautulli/{} ({} {})'.format(RELEASE, PLATFORM, PLATFORM_VERSION)
DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"

View File

@@ -23,7 +23,7 @@ import time
import plexpy
import logger
FILENAME = "plexpy.db"
FILENAME = "tautulli.db"
db_lock = threading.Lock()
@@ -63,9 +63,9 @@ def make_backup(cleanup=False, scheduler=False):
""" Makes a backup of db, removes all but the last 5 backups """
if scheduler:
backup_file = 'plexpy.backup-%s.sched.db' % arrow.now().format('YYYYMMDDHHmmss')
backup_file = 'tautulli.backup-%s.sched.db' % arrow.now().format('YYYYMMDDHHmmss')
else:
backup_file = 'plexpy.backup-%s.db' % arrow.now().format('YYYYMMDDHHmmss')
backup_file = 'tautulli.backup-%s.db' % arrow.now().format('YYYYMMDDHHmmss')
backup_folder = plexpy.CONFIG.BACKUP_DIR
backup_file_fp = os.path.join(backup_folder, backup_file)

View File

@@ -646,7 +646,7 @@ def whois_lookup(ip_address):
countries = ipwhois.utils.get_countries()
nets = whois['nets']
for net in nets:
net['country'] = countries[net['country']]
net['country'] = countries.get(net['country'])
if net['postal_code']:
net['postal_code'] = net['postal_code'].replace('-', ' ')
except ValueError as e:

View File

@@ -41,7 +41,7 @@ class HTTPHandler(object):
self.headers = {'X-Plex-Device-Name': 'Tautulli',
'X-Plex-Product': 'Tautulli',
'X-Plex-Version': plexpy.common.VERSION_NUMBER,
'X-Plex-Version': plexpy.common.RELEASE,
'X-Plex-Platform': plexpy.common.PLATFORM,
'X-Plex-Platform-Version': plexpy.common.PLATFORM_VERSION,
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,

View File

@@ -744,7 +744,7 @@ class Libraries(object):
logger.warn(u"Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh."
% section_id)
# Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet
pmsconnect.refresh_libraries()
refresh_libraries()
library_details = get_library_details(section_id=section_id)

View File

@@ -30,8 +30,8 @@ import plexpy
import helpers
# These settings are for file logging only
FILENAME = "plexpy.log"
FILENAME_API = "plexpy_api.log"
FILENAME = "tautulli.log"
FILENAME_API = "tautulli_api.log"
FILENAME_PLEX_WEBSOCKET = "plex_websocket.log"
MAX_SIZE = 5000000 # 5 MB
MAX_FILES = 5
@@ -39,9 +39,9 @@ MAX_FILES = 5
_BLACKLIST_WORDS = set()
# Tautulli logger
logger = logging.getLogger("plexpy")
logger = logging.getLogger("tautulli")
# Tautulli API logger
logger_api = logging.getLogger("plexpy_api")
logger_api = logging.getLogger("tautulli_api")
# Tautulli websocket logger
logger_plex_websocket = logging.getLogger("plex_websocket")
@@ -178,9 +178,9 @@ def initMultiprocessing():
def initLogger(console=False, log_dir=False, verbose=False):
"""
Setup logging for Tautulli. It uses the logger instance with the name
'plexpy'. Three log handlers are added:
'tautulli'. Three log handlers are added:
* RotatingFileHandler: for the file plexpy.log
* RotatingFileHandler: for the file tautulli.log
* LogListHandler: for Web UI
* StreamHandler: for console (if console)

View File

@@ -677,7 +677,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
available_params = {
# Global paramaters
'tautulli_version': common.VERSION_NUMBER,
'tautulli_version': common.RELEASE,
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
'tautulli_commit': plexpy.CURRENT_VERSION,
@@ -877,7 +877,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
available_params = {
# Global paramaters
'tautulli_version': common.VERSION_NUMBER,
'tautulli_version': common.RELEASE,
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
'tautulli_commit': plexpy.CURRENT_VERSION,

View File

@@ -1932,7 +1932,7 @@ class IFTTT(Notifier):
"""
NAME = 'IFTTT'
_DEFAULT_CONFIG = {'key': '',
'event': 'plexpy'
'event': 'tautulli'
}
def agent_notify(self, subject='', body='', action='', **kwargs):
@@ -2135,7 +2135,7 @@ class MQTT(Notifier):
'protocol': 'MQTTv311',
'username': '',
'password': '',
'clientid': 'plexpy',
'clientid': 'tautulli',
'topic': '',
'qos': 1,
'retain': 0,
@@ -2320,7 +2320,7 @@ class OSX(Notifier):
self.objc.classAddMethod(cls, SEL, new_IMP)
def _swizzled_bundleIdentifier(self, original, swizzled):
return 'ade.plexpy.osxnotify'
return 'ade.tautulli.osxnotify'
def agent_notify(self, subject='', body='', action='', **kwargs):
@@ -2691,8 +2691,10 @@ class PUSHOVER(Notifier):
_DEFAULT_CONFIG = {'api_token': '',
'key': '',
'html_support': 1,
'priority': 0,
'sound': '',
'priority': 0,
'retry': 30,
'expire': 3600,
'incl_url': 1,
'incl_subject': 1,
'incl_poster': 0,
@@ -2713,6 +2715,10 @@ class PUSHOVER(Notifier):
if self.config['incl_subject']:
data['title'] = subject.encode("utf-8")
if self.config['priority'] == 2:
data['retry'] = max(30, self.config['retry'])
data['expire'] = max(30, self.config['expire'])
headers = {'Content-type': 'application/x-www-form-urlencoded'}
files = {}
@@ -2789,6 +2795,13 @@ class PUSHOVER(Notifier):
'description': 'Your Pushover user or group key.',
'input_type': 'text'
},
{'label': 'Sound',
'value': self.config['sound'],
'name': 'pushover_sound',
'description': 'Set the notification sound. Leave blank for the default sound.',
'input_type': 'select',
'select_options': self.get_sounds()
},
{'label': 'Priority',
'value': self.config['priority'],
'name': 'pushover_priority',
@@ -2796,12 +2809,19 @@ class PUSHOVER(Notifier):
'input_type': 'select',
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
},
{'label': 'Sound',
'value': self.config['sound'],
'name': 'pushover_sound',
'description': 'Set the notification sound. Leave blank for the default sound.',
'input_type': 'select',
'select_options': self.get_sounds()
{'label': 'Retry Interval',
'value': self.config['retry'],
'name': 'pushover_retry',
'description': 'Set the interval in seconds to keep retrying the notification.<br>'
'Note: For priority 2 only. Minimum 30 seconds.',
'input_type': 'number'
},
{'label': 'Expire Duration',
'value': self.config['expire'],
'name': 'pushover_expire',
'description': 'Set the duration in seconds when the notification will stop retrying.<br>'
'Note: For priority 2 only. Minimum 30 seconds.',
'input_type': 'number'
},
{'label': 'Enable HTML Support',
'value': self.config['html_support'],

View File

@@ -666,6 +666,11 @@ class PmsConnect(object):
}
elif metadata_type == 'show':
# Workaround for for duration sometimes reported in minutes for a show
duration = helpers.get_xml_attr(metadata_main, 'duration')
if duration.isdigit() and int(duration) < 1000:
duration = unicode(int(duration) * 60 * 1000)
metadata = {'media_type': metadata_type,
'section_id': section_id,
'library_name': library_name,
@@ -685,7 +690,7 @@ class PmsConnect(object):
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'duration': duration,
'year': helpers.get_xml_attr(metadata_main, 'year'),
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
@@ -1404,6 +1409,10 @@ class PmsConnect(object):
'location': 'wan' if player_details['local'] == '0' else 'lan'
}
# Check if using Plex Relay
session_details['relay'] = int(session_details['location'] != 'lan'
and player_details['ip_address_public'] == '127.0.0.1')
# Get the transcode details
if session.getElementsByTagName('TranscodeSession'):
transcode_info = session.getElementsByTagName('TranscodeSession')[0]
@@ -2134,10 +2143,12 @@ class PmsConnect(object):
sort_type = '&type=10'
elif section_type == 'photo':
sort_type = ''
elif section_type == 'photoAlbum':
elif section_type == 'photo_album':
sort_type = '&type=14'
elif section_type == 'picture':
sort_type = '&type=13'
sort_type = '&type=13&clusterZoomLevel=1'
elif section_type == 'clip':
sort_type = '&type=12&clusterZoomLevel=1'
else:
sort_type = ''
@@ -2280,12 +2291,12 @@ class PmsConnect(object):
library_stats.update(child_stats)
if section_type == 'photo':
parent_list = self.get_library_children_details(section_id=section_id, section_type='photoAlbum', count='1')
parent_list = self.get_library_children_details(section_id=section_id, section_type='picture', count='1')
if parent_list:
parent_stats = {'parent_count': parent_list['library_count']}
library_stats.update(parent_stats)
child_list = self.get_library_children_details(section_id=section_id, section_type='picture', count='1')
child_list = self.get_library_children_details(section_id=section_id, section_type='clip', count='1')
if child_list:
child_stats = {'child_count': child_list['library_count']}
library_stats.update(child_stats)

View File

@@ -79,7 +79,7 @@ def get_session_library_filters_type(filters, media_type=None):
filters = filters.get('filter_tv', ())
elif media_type == 'artist' or media_type == 'album' or media_type == 'track':
filters = filters.get('filter_music', ())
elif media_type == 'photo' or media_type == 'photoAlbum' or media_type == 'picture':
elif media_type == 'photo' or media_type == 'photo_album' or media_type == 'picture' or media_type == 'clip':
filters = filters.get('filter_photos', ())
else:
filters = filters.get('filter_all', ())

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.0.20-beta"
PLEXPY_BRANCH = "master"
PLEXPY_RELEASE_VERSION = "v2.0.22"

View File

@@ -20,9 +20,9 @@ import subprocess
import tarfile
import plexpy
import common
import logger
import request
import version
def runGit(args):
@@ -65,7 +65,7 @@ def runGit(args):
def getVersion():
if version.PLEXPY_BRANCH.startswith('win32build'):
if common.BRANCH.startswith('win32build'):
plexpy.INSTALL_TYPE = 'win'
# Don't have a way to update exe yet, but don't want to set VERSION to None
@@ -120,15 +120,15 @@ def getVersion():
version_file = os.path.join(plexpy.PROG_DIR, 'version.txt')
if not os.path.isfile(version_file):
return None, 'origin', 'master'
return None, 'origin', common.BRANCH
with open(version_file, 'r') as f:
current_version = f.read().strip(' \n\r')
if current_version:
return current_version, plexpy.CONFIG.GIT_REMOTE, plexpy.CONFIG.GIT_BRANCH
return current_version, 'origin', common.BRANCH
else:
return None, 'origin', 'master'
return None, 'origin', common.BRANCH
def checkGithub(auto_update=False):

View File

@@ -63,7 +63,7 @@ def serve_template(templatename, **kwargs):
http_root = plexpy.HTTP_ROOT
server_name = plexpy.CONFIG.PMS_NAME
cache_param = '?' + (plexpy.CURRENT_VERSION or common.VERSION_NUMBER)
cache_param = '?' + (plexpy.CURRENT_VERSION or common.RELEASE)
_session = get_session_info()
@@ -2293,7 +2293,7 @@ class WebInterface(object):
filtered = []
fa = filt.append
if logfile == "plexpy_api":
if logfile == "tautulli_api":
filename = logger.FILENAME_API
elif logfile == "plex_websocket":
filename = logger.FILENAME_PLEX_WEBSOCKET
@@ -2496,7 +2496,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
def delete_logs(self, logfile='', **kwargs):
if logfile == "plexpy_api":
if logfile == "tautulli_api":
filename = logger.FILENAME_API
elif logfile == "plex_websocket":
filename = logger.FILENAME_PLEX_WEBSOCKET
@@ -2538,7 +2538,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth(member_of("admin"))
def logFile(self, logfile='', **kwargs):
if logfile == "plexpy_api":
if logfile == "tautulli_api":
filename = logger.FILENAME_API
elif logfile == "plex_websocket":
filename = logger.FILENAME_PLEX_WEBSOCKET
@@ -3581,11 +3581,11 @@ class WebInterface(object):
}
elif plexpy.COMMITS_BEHIND > 0 and plexpy.common.BRANCH in ('master', 'beta') and \
plexpy.common.VERSION_NUMBER != plexpy.LATEST_RELEASE:
plexpy.common.RELEASE != plexpy.LATEST_RELEASE:
return {'result': 'success',
'update': True,
'release': True,
'message': 'A new release (%) of Tautulli is available.' % plexpy.LATEST_RELEASE,
'message': 'A new release (%s) of Tautulli is available.' % plexpy.LATEST_RELEASE,
'latest_release': plexpy.LATEST_RELEASE,
'release_url': helpers.anon_url(
'https://github.com/%s/%s/releases/tag/%s'
@@ -3668,7 +3668,7 @@ class WebInterface(object):
latest_only = (latest_only == 'true')
since_prev_release = (since_prev_release == 'true')
if since_prev_release and plexpy.PREV_RELEASE == common.VERSION_NUMBER:
if since_prev_release and plexpy.PREV_RELEASE == common.RELEASE:
latest_only = True
since_prev_release = False
@@ -3912,7 +3912,7 @@ class WebInterface(object):
@addtoapi()
def download_log(self, logfile='', **kwargs):
""" Download the Tautulli log file. """
if logfile == "plexpy_api":
if logfile == "tautulli_api":
filename = logger.FILENAME_API
log = logger.logger_api
elif logfile == "plex_websocket":
@@ -4706,6 +4706,7 @@ class WebInterface(object):
"quality_profile": "Original",
"rating": "7.8",
"rating_key": "153037",
"relay": 0,
"section_id": "2",
"session_id": "helf15l3rxgw01xxe0jf3l3d",
"session_key": "27",

View File

@@ -72,7 +72,7 @@ def initialize(options):
if plexpy.CONFIG.HTTP_PLEX_ADMIN:
login_allowed.append("Plex admin")
logger.info(u"Tautulli WebStart :: Web server authentication is enabled: %s allowed", ' and '.join(login_allowed))
logger.info(u"Tautulli WebStart :: Web server authentication is enabled: %s.", ' and '.join(login_allowed))
if options['http_basic_auth']:
auth_enabled = False
@@ -122,6 +122,7 @@ def initialize(options):
'/images': {
'tools.staticdir.on': True,
'tools.staticdir.dir': "interfaces/default/images",
'tools.staticdir.content_types': {'svg': 'image/svg+xml'},
'tools.caching.on': True,
'tools.caching.force': True,
'tools.caching.delay': 0,