Compare commits

...

94 Commits

Author SHA1 Message Date
JonnyWong16
1f587ed698 v2.0.27 2018-04-02 14:17:48 -07:00
JonnyWong16
1032fdfe7a Move refresh interval setting back to the settings page 2018-04-02 14:13:52 -07:00
JonnyWong16
818e7723ff v2.0.26-beta 2018-03-30 09:29:47 -07:00
JonnyWong16
a69008e179 Send Telegram notification separately if caption is longer than 200 characters (Closes Tautulli/Tautulli-Issues#20) 2018-03-30 09:23:38 -07:00
JonnyWong16
91c647f9ae Show extra type on activity cards 2018-03-29 19:54:43 -07:00
JonnyWong16
36b80aa6d3 Make sure all datatables are using POST 2018-03-28 18:08:57 -07:00
JonnyWong16
c35fcc727c Change default refresh to 10 seconds 2018-03-27 22:08:12 -07:00
JonnyWong16
749e1fcebe Move refresh interval setting to homepage 2018-03-26 08:53:40 -07:00
JonnyWong16
084732706d Add setting to change homepage refresh interval 2018-03-25 13:25:18 -07:00
JonnyWong16
2aff7713cd Fix invalid link to playlist in sync table (Fixes Tautulli/Tautulli-Issues#34) 2018-03-25 12:39:20 -07:00
JonnyWong16
683a782723 Fix typo (Closes Tautulli/Tautulli-Issues#35) 2018-03-25 11:58:57 -07:00
JonnyWong16
5108e1bb09 Add quick websocket test when verifying server 2018-03-25 11:38:35 -07:00
JonnyWong16
d8298a12eb Clear PMS selectize when dropdown opens 2018-03-25 11:00:58 -07:00
JonnyWong16
042b48c1fd Fix repeating renaming notifiers on startup 2018-03-24 23:32:53 -07:00
JonnyWong16
8fac54aa71 Typo 2018-03-22 22:11:11 -07:00
JonnyWong16
244008d539 v2.0.25 2018-03-22 22:06:01 -07:00
JonnyWong16
502b807e45 Fix websocket not scheduling reconnect 2018-03-22 21:03:11 -07:00
JonnyWong16
35914b9a48 Remove unicode from websocket logger error 2018-03-22 20:32:37 -07:00
JonnyWong16
24ac34d5e2 Make sure user has Plex Pass if checking for synced stream 2018-03-22 19:39:46 -07:00
JonnyWong16
a5807f21b4 Flush temporary sessions automatically if failed to check sessions on startup 2018-03-19 23:24:09 -07:00
JonnyWong16
e3b71a729e Revert negative operator values to "OR" (UI change only) 2018-03-19 23:18:27 -07:00
JonnyWong16
ebb287e1ee v2.0.24 2018-03-18 17:46:17 -07:00
JonnyWong16
bd3497b2bf Rename notifiers in database 2018-03-18 17:44:24 -07:00
JonnyWong16
034f3ee308 Anon URL to FAQ for pycryptodome does not work with anchors 2018-03-18 17:33:02 -07:00
JonnyWong16
a946879fc1 Better OSX register button 2018-03-18 17:22:44 -07:00
JonnyWong16
9f964b5a87 Move notification agent instructions to wiki 2018-03-18 17:05:30 -07:00
JonnyWong16
ed0b41cd19 Add punctuation to Arnold 2018-03-17 18:44:15 -07:00
JonnyWong16
dc87591992 Show historical stream data (Fixes Tautulli/Tautulli-Issues#27) 2018-03-17 16:36:24 -07:00
JonnyWong16
d05e80e573 Make sure all exisiting environment variables are included for scripts 2018-03-17 13:30:12 -07:00
JonnyWong16
522684b2ab v2.0.23-beta 2018-03-16 19:59:06 -07:00
JonnyWong16
feab16b351 Update API docs for get_server_id 2018-03-16 19:47:49 -07:00
JonnyWong16
ee041db63d Pass common environment variable to scripts 2018-03-16 18:37:50 -07:00
JonnyWong16
2479533d07 Show Plex Server URL in settings 2018-03-16 17:43:32 -07:00
JonnyWong16
d045fd5834 Update Facebook Graph API version 2018-03-16 15:39:41 -07:00
JonnyWong16
8407f27fed Add value3 to IFTTT notifications (Closes #1279) 2018-03-16 09:45:30 -07:00
JonnyWong16
b505286caf Add season/episode/album/track count to notification parameters 2018-03-16 09:42:32 -07:00
JonnyWong16
feb762ce8b Beta/nightly update check to include non-beta releases 2018-03-16 08:37:50 -07:00
JonnyWong16
8acdb5af83 Use media stream info for transcode decision (Fixes Tautulli/Tautulli-Issues#24) 2018-03-14 19:45:47 -07:00
JonnyWong16
5af1294f71 Make websocket thread daemon 2018-03-14 16:19:22 -07:00
JonnyWong16
87d2d273d3 Attempt at fixing custom condition json error 2018-03-13 22:16:23 -07:00
JonnyWong16
b5c52ac71e Add logging for failed custom condition json 2018-03-13 20:45:41 -07:00
JonnyWong16
efe9a15f72 Cast Email username/password to string 2018-03-13 20:41:07 -07:00
JonnyWong16
525f1e4b0b Use cherrypy remote for login IP info 2018-03-13 10:00:08 -07:00
JonnyWong16
d18820b832 Use cherrypy base for login host info 2018-03-13 09:42:01 -07:00
JonnyWong16
7e024fd736 Remove test comment in c9c5989 2018-03-13 09:09:27 -07:00
JonnyWong16
c9c5989474 Fix login logs for Plex admin user 2018-03-13 09:08:09 -07:00
JonnyWong16
ce9f96d3be Exit if failed to move database instead of continuing 2018-03-12 19:43:46 -07:00
JonnyWong16
7362dd0bf4 Close websocket cleanly on shutdown 2018-03-12 19:38:19 -07:00
JonnyWong16
9905ebc144 Don't empty results if message in API response (Fixes Tautulli/Tautulli-Issues#13) 2018-03-12 08:56:43 -07:00
JonnyWong16
8f8010884b Add git pull after checkout from interface 2018-03-12 08:20:20 -07:00
JonnyWong16
37afd141be Catch invalid json for custom conditions 2018-03-11 20:59:18 -07:00
JonnyWong16
a3643b4302 Fix typos 2018-03-10 20:54:21 -08:00
JonnyWong16
02cfd8d9b7 Fix git branch select box height 2018-03-10 20:33:18 -08:00
JonnyWong16
941ce439b4 Update API message for remote app settings 2018-03-10 18:03:23 -08:00
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
64 changed files with 1436 additions and 997 deletions

4
API.md
View File

@@ -401,6 +401,7 @@ Returns:
"quality_profile": "Original", "quality_profile": "Original",
"rating": "7.8", "rating": "7.8",
"rating_key": "153037", "rating_key": "153037",
"relay": 0,
"section_id": "2", "section_id": "2",
"session_id": "helf15l3rxgw01xxe0jf3l3d", "session_id": "helf15l3rxgw01xxe0jf3l3d",
"session_key": "27", "session_key": "27",
@@ -1673,7 +1674,8 @@ Optional parameters:
remote (int): 0 or 1 remote (int): 0 or 1
Returns: Returns:
string: The unique PMS identifier json:
{'identifier': '08u2phnlkdshf890bhdlksghnljsahgleikjfg9t'}
``` ```

View File

@@ -1,5 +1,93 @@
# Changelog # Changelog
## v2.0.27 (2018-04-02)
* Monitoring:
* Change: Move activity refresh interval setting to the settings page.
## v2.0.26-beta (2018-03-30)
* Monitoring:
* New: Setting to change the refresh interval on the homepage.
* Fix: Identify extras correctly on the activity cards.
* Notifications:
* Change: Send Telegram image and text separately if the caption is longer than 200 characters.
* UI:
* Fix: Error when clicking on synced playlist links.
## v2.0.25 (2018-03-22)
* Monitoring:
* Fix: Websocket not reconnecting causing activity monitoring and notifications to not work.
* Fix: Error checking for synced streams without Plex Pass.
## v2.0.24 (2018-03-18)
* Monitoring:
* Fix: Fix stream data not showing for history recorded before v2.
* Notifications:
* Fix: Set all environment variables for scripts.
* Change: Moved all notification agent instructions to the wiki.
* Change: XBMC notification agent renamed to Kodi.
* Change: OSX Notify notification agent renamed to macOS Notification Center.
## v2.0.23-beta (2018-03-16)
* Monitoring:
* Fix: Certain transcode stream showing incorrectly as direct play in history. Fix is not retroactive.
* Notifications:
* New: Added season/episode/album/track count to notification parameters.
* New: Added "Value 3" setting for IFTTT notifications.
* New: Set PLEX_URL, PLEX_TOKEN, TAUTULLI_URL, and TAUTULLI_APIKEY environment variables for scripts.
* Fix: Notifications failing to send with invalid custom conditions json.
* Fix: Email notifications failing with unicode username/passwords.
* Change: Facebook Graph API version updated to v2.12.
* UI:
* New: Show the Plex Server URL in the settings.
* Fix: Incorrect info displayed in the Tautulli login logs.
* API:
* Fix: API returning empty data if a message was in the original data.
* Change: get_server_id command returns json instead of string.
* Other:
* Fix: Forgot git pull when changing branches in the web UI.
## 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) ## v2.0.20-beta (2018-02-24)
* Notifications: * Notifications:

235
PlexPy.py
View File

@@ -21,239 +21,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>. # along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
import os from Tautulli import main
import sys
# Ensure lib added to path, before any other imports # Call main() from Tautulli.py
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()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

267
Tautulli.py Executable file
View File

@@ -0,0 +1,267 @@
#!/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)):
try:
os.rename(os.path.join(plexpy.DATA_DIR, 'plexpy.db'), plexpy.DB_FILE)
except OSError as e:
raise SystemExit("Unable to rename plexpy.db to tautulli.db: %s", e)
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=""> <meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet"> <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/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/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet"> <link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
${next.headIncludes()} ${next.headIncludes()}
@@ -47,7 +47,7 @@
You are running an unknown version of Tautulli.<br /> You are running an unknown version of Tautulli.<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a> <a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
</div> </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;"> <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"> 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 /> new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br />
@@ -227,15 +227,23 @@ ${next.modalIncludes()}
</div> </div>
</div> </div>
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;"> <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 class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
<li><a href="#flattr-donation" role="tab" data-toggle="tab">Flattr</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="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="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="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> <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> </ul>
<div class="tab-content"> <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> <p>
Click the button below to continue to PayPal. Click the button below to continue to PayPal.
</p> </p>
@@ -243,14 +251,6 @@ ${next.modalIncludes()}
<img src="images/gold-rect-paypal-34px.png" alt="PayPal"> <img src="images/gold-rect-paypal-34px.png" alt="PayPal">
</a> </a>
</div> </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"> <div role="tabpanel" class="tab-pane" id="crypto-donation">
<label>QR Code</label> <label>QR Code</label>
<pre id="crypto_qr_code" style="text-align: center"></pre> <pre id="crypto_qr_code" style="text-align: center"></pre>

View File

@@ -66,7 +66,6 @@ div.form-control .selectize-input {
color: #fff; color: #fff;
border: 0px solid #444; border: 0px solid #444;
background: #555; background: #555;
height: 32px;
padding: 6px 12px; padding: 6px 12px;
background-color: #555; background-color: #555;
border-radius: 3px; border-radius: 3px;
@@ -92,6 +91,7 @@ div.form-control .selectize-input {
border-top-left-radius: 3px; border-top-left-radius: 3px;
border-bottom-left-radius: 3px; border-bottom-left-radius: 3px;
min-height: 32px !important; min-height: 32px !important;
height: 32px !important;
} }
.input-group .selectize-control.form-control.selectize-pms-ip .selectize-input > div { .input-group .selectize-control.form-control.selectize-pms-ip .selectize-input > div {
max-width: 450px; max-width: 450px;
@@ -134,9 +134,6 @@ div.form-control .selectize-input {
text-transform: uppercase; text-transform: uppercase;
font-size: 10px; font-size: 10px;
} }
.react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values.negative-operator .value-wrapper:not(:first-child):before {
content: "and" !important;
}
.react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values .resizable-input { .react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values .resizable-input {
padding-top: 3px !important; padding-top: 3px !important;
padding-bottom: 3px !important; padding-bottom: 3px !important;
@@ -1419,7 +1416,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
} }
.dashboard-stats-info-item .sub-count { .dashboard-stats-info-item .sub-count {
height: 100%; height: 100%;
margin-left: 10px; margin-left: 5px;
color: #f9be03; color: #f9be03;
font-size: 12px; font-size: 12px;
text-align: right; text-align: right;
@@ -1430,7 +1427,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
} }
.dashboard-stats-info-item .sub-divider { .dashboard-stats-info-item .sub-divider {
height: 100%; height: 100%;
margin-left: 10px; margin-left: 5px;
color: #aaa; color: #aaa;
font-size: 12px; font-size: 12px;
text-align: left; text-align: left;
@@ -2372,21 +2369,6 @@ a .library-user-instance-box:hover {
#watched-stats-days-selection label { #watched-stats-days-selection label {
margin-bottom: 0; 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 { .home-padded-header {
margin: 25px 0; margin: 25px 0;
height: 34px; height: 34px;
@@ -3435,22 +3417,10 @@ pre::-webkit-scrollbar-thumb {
.notification-params tr:nth-child(even) td { .notification-params tr:nth-child(even) td {
background-color: rgba(255,255,255,0.010); background-color: rgba(255,255,255,0.010);
} }
#days-selection label { #days-selection label,
margin-bottom: 0;
}
#graph-days {
margin: 0;
width: 75px;
height: 34px;
}
#months-selection label { #months-selection label {
margin-bottom: 0; margin-bottom: 0;
} }
#graph-months {
margin: 0;
width: 75px;
height: 34px;
}
.card-sortable { .card-sortable {
height: 36px; height: 36px;
padding: 0 20px 0 0; padding: 0 20px 0 0;
@@ -3721,6 +3691,7 @@ a:hover .overlay-refresh-image:hover {
} }
.git-group select.form-control { .git-group select.form-control {
width: 50%; width: 50%;
height: 32px;
} }
#changelog-modal .modal-body > h2 { #changelog-modal .modal-body > h2 {
margin-bottom: 10px; margin-bottom: 10px;
@@ -3967,3 +3938,14 @@ a:hover .overlay-refresh-image:hover {
.stream-info tr:nth-child(even) td { .stream-info tr:nth-child(even) td {
background-color: rgba(255,255,255,0.010); 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

@@ -64,7 +64,7 @@ DOCUMENTATION :: END
from collections import defaultdict from collections import defaultdict
from urllib import quote from urllib import quote
from plexpy import helpers from plexpy import helpers
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES, EXTRA_TYPES
import plexpy import plexpy
%> %>
<% <%
@@ -108,7 +108,11 @@ DOCUMENTATION :: END
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div> <div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div>
</a> </a>
% elif data['media_type'] in ('photo', 'clip'): % elif data['media_type'] in ('photo', 'clip'):
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div> % if data['extra_type']:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['art'].replace('/art', '/thumb') or data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
% else:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb'] or data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
% endif
% else: % else:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/art.png);"></div> <div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/art.png);"></div>
% endif % endif
@@ -279,16 +283,20 @@ DOCUMENTATION :: END
<span id="location-${sk}">${data['location'].upper()}</span>: <span id="location-${sk}">${data['location'].upper()}</span>:
% if data['ip_address'] != 'N/A': % if data['ip_address'] != 'N/A':
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span> <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']}"> % if data['relay']:
<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> <span data-toggle="tooltip" title="Plex Relay"><i class="fa fa-exclamation-circle"></i></span>
</a> % else:
<script> <a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
isPrivateIP("${data['ip_address']}").then(function () { <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>
$("#external_ip-${sk}").hide(); </a>
}, function () { <script>
$("#external_ip-${sk}").show(); isPrivateIP("${data['ip_address']}").then(function () {
}); $("#external_ip-${sk}").hide();
</script> }, function () {
$("#external_ip-${sk}").show();
});
</script>
% endif
% else: % else:
N/A N/A
% endif % endif
@@ -297,14 +305,13 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
<div class="sub-heading">Bandwidth</div> <div class="sub-heading">Bandwidth</div>
<div class="sub-value time-right"> <div class="sub-value time-right">
% if data['media_type'] != 'photo' and helpers.cast_to_int(data['bandwidth']): % if data['media_type'] != 'photo' and data['bandwidth'] != 'Unknown':
<% <%
bw = helpers.cast_to_int(data['bandwidth']) bw = helpers.cast_to_int(data['bandwidth'])
if bw != "Unknown": if bw > 1000:
if bw > 1000: bw = str(round(bw / 1000.0, 1)) + ' Mbps'
bw = str(round(bw / 1000.0, 1)) + ' Mbps' else:
else: bw = str(bw) + ' kbps'
bw = str(bw) + ' kbps'
%> %>
<span id="stream-bandwidth-${sk}">${bw}</span> <span id="stream-bandwidth-${sk}">${bw}</span>
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span> <span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span>
@@ -436,7 +443,12 @@ DOCUMENTATION :: END
% elif data['media_type'] == 'photo': % elif data['media_type'] == 'photo':
<span title="${data['title']}" class="sub-heading">${data['title']}</span> <span title="${data['title']}" class="sub-heading">${data['title']}</span>
% else: % else:
<span title="${data['year']}" class="sub-heading">${data['year']}</span> % if data['extra_type']:
<% extra_type = EXTRA_TYPES.get(data['extra_type'], data['sub_type'].capitalize()) %>
<span title="${data['year']} (${extra_type})" class="sub-heading">${data['year']} (${extra_type})</span>
% else:
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
% endif
% endif % endif
% elif data['channel_title']: % elif data['channel_title']:
<span title="${data['channel_title']}" class="sub-heading">${data['channel_title']}</span> <span title="${data['channel_title']}" class="sub-heading">${data['channel_title']}</span>

View File

@@ -2,7 +2,7 @@
<%def name="headIncludes()"> <%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css"> <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>
<%def name="body()"> <%def name="body()">
@@ -39,12 +39,12 @@
</div> </div>
<div class="input-group pull-right" style="width: 1px;" id="days-selection"> <div class="input-group pull-right" style="width: 1px;" id="days-selection">
<span class="input-group-addon btn-dark inactive">Last</span> <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> <span class="input-group-addon btn-dark inactive">days</span>
</div> </div>
<div class="input-group pull-right" style="width: 1px;" id="months-selection"> <div class="input-group pull-right" style="width: 1px;" id="months-selection">
<span class="input-group-addon btn-dark inactive">Last</span> <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> <span class="input-group-addon btn-dark inactive">months</span>
</div> </div>
</div> </div>

View File

@@ -3,7 +3,7 @@
<%def name="headIncludes()"> <%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css"> <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/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>
<%def name="body()"> <%def name="body()">
@@ -113,7 +113,7 @@
// Load user ids and names (for the selector) // Load user ids and names (for the selector)
$.ajax({ $.ajax({
url: 'get_user_names', url: 'get_user_names',
type: 'get', type: 'GET',
dataType: 'json', dataType: 'json',
success: function (data) { success: function (data) {
var select = $('#history-user'); var select = $('#history-user');
@@ -130,6 +130,7 @@
function loadHistoryTable(media_type, selected_user_id) { function loadHistoryTable(media_type, selected_user_id) {
history_table_options.ajax = { history_table_options.ajax = {
url: 'get_history', url: 'get_history',
type: 'POST',
data: function (d) { data: function (d) {
return { return {
json_data: JSON.stringify(d), json_data: JSON.stringify(d),
@@ -163,7 +164,7 @@
} }
var media_type = null; var media_type = null;
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"; var selected_user_id = "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}";
loadHistoryTable(media_type, selected_user_id); loadHistoryTable(media_type, selected_user_id);
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -10,7 +10,7 @@
% if section == 'current_activity': % if section == 'current_activity':
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="padded-header" id="current-activity-header"> <div class="home-padded-header padded-header" id="current-activity-header">
<h3><span id="sessions-shortcut">Activity</span> &nbsp;&nbsp; <h3><span id="sessions-shortcut">Activity</span> &nbsp;&nbsp;
<small> <small>
<span id="currentActivityHeader" style="display: none;"> <span id="currentActivityHeader" style="display: none;">
@@ -22,7 +22,16 @@
</h3> </h3>
</div> </div>
<div id="currentActivity"> <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> <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> </div>
</div> </div>
@@ -51,7 +60,7 @@
</div> </div>
<div class="input-group pull-left" style="width: 1px; margin-right: 3px" id="watched-stats-days-selection"> <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> <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> <span class="input-group-addon btn-dark inactive">days</span>
</div> </div>
</div> </div>
@@ -114,7 +123,7 @@
</label> </label>
</div> </div>
<div class="input-group pull-left" style="width: 1px;" id="recently-added-count-selection"> <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> <span class="input-group-addon btn-dark inactive">items</span>
</div> </div>
</div> </div>
@@ -137,13 +146,13 @@
<%def name="modalIncludes()"> <%def name="modalIncludes()">
% if _session['user_group'] == 'admin' and config['update_show_changelog']: % 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 id="changelog-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button> <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>
<div class="modal-body"> <div class="modal-body">
</div> </div>
@@ -241,9 +250,10 @@
}); });
} }
}); });
}; }
</script> </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> <script>
var defaultHandler = { var defaultHandler = {
get: function(target, name) { get: function(target, name) {
@@ -266,6 +276,7 @@
async: true, async: true,
error: function (xhr, status, error) { error: function (xhr, status, error) {
console.log(status + ': ' + error); console.log(status + ': ' + error);
activity_ready = true;
}, },
complete: function (xhr, status) { complete: function (xhr, status) {
$('#dashboard-checking-activity').remove(); $('#dashboard-checking-activity').remove();
@@ -280,9 +291,9 @@
if (!(current_activity)) { if (!(current_activity)) {
% if _session['user_group'] == 'admin': % 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: % else:
var msg_settings = '' var msg_settings = '';
% endif % endif
$('#currentActivityHeader').hide(); $('#currentActivityHeader').hide();
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>'); $('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');
@@ -496,17 +507,15 @@
$('#location-' + key).html(s.location.toUpperCase()); $('#location-' + key).html(s.location.toUpperCase());
if (s.media_type !== 'photo' && parseInt(s.bandwidth)) { if (s.media_type !== 'photo' && s.bandwidth !== 'Unknown') {
var bw = parseInt(s.bandwidth); var bw = parseInt(s.bandwidth) || 0;
if (bw !== "Unknown") { if (bw > 1000) {
if (bw > 1000) { bw = (bw / 1000).toFixed(1) + ' Mbps';
bw = (bw / 1000).toFixed(1) + ' Mbps'; } else {
} else { bw = bw + ' kbps'
bw = bw + ' kbps'
}
} }
$('#stream-bandwidth-' + key).html(bw); $('#stream-bandwidth-' + key).html(bw);
} };
// Update the stream progress times // Update the stream progress times
$('#stream-eta-' + key).html(moment().add(parseInt(s.duration) - parseInt(s.view_offset), 'milliseconds').format(time_format)); $('#stream-eta-' + key).html(moment().add(parseInt(s.duration) - parseInt(s.view_offset), 'milliseconds').format(time_format));
@@ -578,7 +587,7 @@
if (!(create_instances.length) && activity_ready) { if (!(create_instances.length) && activity_ready) {
getCurrentActivity(); getCurrentActivity();
} }
}, 2000); }, ${config['home_refresh_interval'] * 1000});
setInterval(function(){ setInterval(function(){
$('.progress_time_offset').each(function () { $('.progress_time_offset').each(function () {
@@ -593,7 +602,7 @@
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) { if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
var view_offset = parseInt($(this).data('view_offset')); var view_offset = parseInt($(this).data('view_offset'));
var stream_duration = parseInt($(this).data('stream_duration')); var stream_duration = parseInt($(this).data('stream_duration'));
var progress_percent = Math.min(Math.trunc(view_offset / stream_duration * 100), 100) var progress_percent = Math.min(Math.trunc(view_offset / stream_duration * 100), 100);
$(this).width(progress_percent - 3 + '%').html(progress_percent + '%') $(this).width(progress_percent - 3 + '%').html(progress_percent + '%')
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%') .attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
.data('view_offset', Math.min(view_offset + 1000, stream_duration)); .data('view_offset', Math.min(view_offset + 1000, stream_duration));

View File

@@ -64,7 +64,7 @@ DOCUMENTATION :: END
<%def name="headIncludes()"> <%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css"> <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/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>
<%def name="body()"> <%def name="body()">
@@ -547,12 +547,12 @@ DOCUMENTATION :: END
function get_history() { function get_history() {
history_table_options.ajax = { history_table_options.ajax = {
url: 'get_history', url: 'get_history',
type: 'post', type: 'POST',
data: function ( d ) { data: function ( d ) {
return { return {
json_data: JSON.stringify( d ), json_data: JSON.stringify( d ),
grandparent_rating_key: "${data['rating_key']}", grandparent_rating_key: "${data['rating_key']}",
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}" user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
}; };
} }
} }
@@ -563,12 +563,12 @@ DOCUMENTATION :: END
function get_history() { function get_history() {
history_table_options.ajax = { history_table_options.ajax = {
url: 'get_history', url: 'get_history',
type: 'post', type: 'POST',
data: function ( d ) { data: function ( d ) {
return { return {
json_data: JSON.stringify( d ), json_data: JSON.stringify( d ),
parent_rating_key: "${data['rating_key']}", parent_rating_key: "${data['rating_key']}",
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}" user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
}; };
} }
} }
@@ -579,12 +579,12 @@ DOCUMENTATION :: END
function get_history() { function get_history() {
history_table_options.ajax = { history_table_options.ajax = {
url: 'get_history', url: 'get_history',
type: 'post', type: 'POST',
data: function ( d ) { data: function ( d ) {
return { return {
json_data: JSON.stringify( d ), json_data: JSON.stringify( d ),
rating_key: "${data['rating_key']}", rating_key: "${data['rating_key']}",
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}" user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
}; };
} }
} }

View File

@@ -292,7 +292,11 @@ function millisecondsToMinutes(ms, roundToMinute) {
if (ms > 0) { if (ms > 0) {
var minutes = Math.floor(ms / 60000); var minutes = Math.floor(ms / 60000);
var seconds = ((ms % 60000) / 1000).toFixed(0); 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 { } else {
if (roundToMinute) { if (roundToMinute) {
return '0'; return '0';

View File

@@ -54,7 +54,7 @@ media_info_table_options = {
} else if (rowData['media_type'] === 'album') { } 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>'; 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>'); $(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>'; 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>'); $(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else { } else {
@@ -77,32 +77,44 @@ media_info_table_options = {
if (rowData['media_type'] === 'movie') { if (rowData['media_type'] === 'movie') {
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; } 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>'; 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>'); $(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') { } 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>'; 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>'); $(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') { } 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>'; 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>'); $(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') { } 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>'; 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>'); $(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') { } 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>'; 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>'); $(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') { } 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>'; 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>'); $(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') { } 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>'; 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>'); $(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 { } else {
$(td).html(cellData); $(td).html(cellData);
} }
@@ -335,7 +347,7 @@ function childTableOptionsMedia(rowData) {
case 'album': case 'album':
section_type = 'track'; section_type = 'track';
break; break;
case 'photo': case 'photo_album':
section_type = 'picture'; section_type = 'picture';
break; break;
} }

View File

@@ -37,7 +37,6 @@ sync_table_options = {
"data": "state", "data": "state",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === 'pending') { if (cellData === 'pending') {
$(td).addClass('currentlyWatching');
$(td).html('Pending...'); $(td).html('Pending...');
} else { } else {
$(td).html(cellData.toProperCase()); $(td).html(cellData.toProperCase());
@@ -66,7 +65,7 @@ sync_table_options = {
"data": "sync_title", "data": "sync_title",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
if (rowData['metadata_type'] !== '') { if (rowData['rating_key']) {
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>'); $(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
} else { } else {
$(td).html(cellData); $(td).html(cellData);
@@ -74,7 +73,7 @@ sync_table_options = {
} }
}, },
"className": "datatable-wrap" "className": "datatable-wrap"
}, },
{ {
"targets": [4], "targets": [4],
"data": "metadata_type", "data": "metadata_type",
@@ -150,6 +149,11 @@ sync_table_options = {
"preDrawCallback": function (settings) { "preDrawCallback": function (settings) {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows..."; var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
showMsg(msg, false, false, 0) showMsg(msg, false, false, 0)
},
"rowCallback": function (row, rowData, rowIndex) {
if (rowData['state'] === 'pending') {
$(row).addClass('current-activity-row');
}
} }
}; };

View File

@@ -3,7 +3,7 @@
<%def name="headIncludes()"> <%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css"> <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/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>
<%def name="body()"> <%def name="body()">
@@ -91,7 +91,7 @@
json_data: JSON.stringify(d) json_data: JSON.stringify(d)
}; };
} }
} };
libraries_list_table = $('#libraries_list_table').DataTable(libraries_list_table_options); libraries_list_table = $('#libraries_list_table').DataTable(libraries_list_table_options);
var colvis = new $.fn.dataTable.ColVis(libraries_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] }); var colvis = new $.fn.dataTable.ColVis(libraries_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] });

View File

@@ -30,7 +30,7 @@ DOCUMENTATION :: END
<%def name="headIncludes()"> <%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css"> <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/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>
<%def name="body()"> <%def name="body()">
@@ -374,12 +374,12 @@ DOCUMENTATION :: END
// Build watch history table // Build watch history table
history_table_options.ajax = { history_table_options.ajax = {
url: 'get_history', url: 'get_history',
type: 'post', type: 'POST',
data: function ( d ) { data: function ( d ) {
return { return {
json_data: JSON.stringify( d ), json_data: JSON.stringify( d ),
section_id: section_id, section_id: section_id,
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}" user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
}; };
} }
}; };
@@ -406,7 +406,7 @@ DOCUMENTATION :: END
// Build media info table // Build media info table
media_info_table_options.ajax = { media_info_table_options.ajax = {
url: 'get_library_media_info', url: 'get_library_media_info',
type: 'post', type: 'POST',
data: function ( d ) { data: function ( d ) {
return { return {
json_data: JSON.stringify( d ), json_data: JSON.stringify( d ),

View File

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

View File

@@ -9,7 +9,7 @@
<meta name="author" content=""> <meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet"> <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/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/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet"> <link href="${http_root}css/font-awesome.min.css" rel="stylesheet">

View File

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

View File

@@ -55,7 +55,7 @@ DOCUMENTATION :: END
}) })
} }
return deferred; return deferred;
} };
function checkQRAddress(url) { function checkQRAddress(url) {
var parser = document.createElement('a'); var parser = document.createElement('a');
@@ -82,7 +82,7 @@ DOCUMENTATION :: END
verifiedDevice = false; verifiedDevice = false;
getPlexPyURL().then(function (url) { getPlexPyURL().then(function (url) {
checkQRAddress(url) checkQRAddress(url);
$.get('generate_api_key', { device: true }).then(function (token) { $.get('generate_api_key', { device: true }).then(function (token) {
$('#api_qr_address').val(url); $('#api_qr_address').val(url);
@@ -120,7 +120,7 @@ DOCUMENTATION :: END
$('#api_qr_address').change(function () { $('#api_qr_address').change(function () {
var url = $(this).val(); var url = $(this).val();
checkQRAddress(url) checkQRAddress(url);
$('#api_qr_code').empty().qrcode({ $('#api_qr_code').empty().qrcode({
text: url + '|' + $('#api_qr_token').val() text: url + '|' + $('#api_qr_token').val()

View File

@@ -45,9 +45,6 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}> <input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}>
% if item['name'] == 'osx_notify_app':
<a href="javascript:void(0)" id="osxnotifyregister">Register</a>
% endif
</div> </div>
</div> </div>
<p class="help-block">${item['description'] | n}</p> <p class="help-block">${item['description'] | n}</p>
@@ -171,7 +168,7 @@
<div class="form-group"> <div class="form-group">
<label for="custom_conditions_logic">Condition Logic</label> <label for="custom_conditions_logic">Condition Logic</label>
<input type="text" class="form-control" name="custom_conditions_logic" id="custom_conditions_logic" value="${notifier['custom_conditions_logic']}" required /> <input type="text" class="form-control" name="custom_conditions_logic" id="custom_conditions_logic" value="${notifier['custom_conditions_logic']}" />
<div id="custom_conditions_logic_error" class="alert alert-danger" role="alert" style="padding-top: 5px; padding-bottom: 5px; margin: 0; display: none;"><i class="fa fa-exclamation-triangle" style="color: #a94442;"></i> <span></span></div> <div id="custom_conditions_logic_error" class="alert alert-danger" role="alert" style="padding-top: 5px; padding-bottom: 5px; margin: 0; display: none;"><i class="fa fa-exclamation-triangle" style="color: #a94442;"></i> <span></span></div>
<p class="help-block"> <p class="help-block">
Optional: Enter custom logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>). Optional: Enter custom logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>).
@@ -333,31 +330,16 @@
$('#notifier-config-modal').unbind('hidden.bs.modal'); $('#notifier-config-modal').unbind('hidden.bs.modal');
// Need this for setting conditions since conditions contain the character " // Need this for setting conditions since conditions contain the character "
$('#custom_conditions').val(${json.dumps(notifier["custom_conditions"]) | n}); $('#custom_conditions').val(JSON.stringify(${json.dumps(notifier["custom_conditions"]) | n}));
$('#condition-widget').filterer({ $('#condition-widget').filterer({
parameters: ${parameters | n}, parameters: ${json.dumps(parameters) | n},
conditions: ${notifier["custom_conditions"] | n}, conditions: ${json.dumps(notifier["custom_conditions"]) | n},
updateConditions: function(newConditions){ updateConditions: function(newConditions){
$('#custom_conditions').val(JSON.stringify(newConditions)); $('#custom_conditions').val(JSON.stringify(newConditions));
} }
}); });
function setNegativeOperator(select) {
if (select.val() === 'does not contain' || select.val() === 'is not') {
select.closest('.form-group').find('.react-selectize-search-field-and-selected-values').addClass('negative-operator');
} else {
select.closest('.form-group').find('.react-selectize-search-field-and-selected-values').removeClass('negative-operator');
}
}
$('#condition-widget select[name=operator]').each(function () {
setNegativeOperator($(this));
});
$('#condition-widget').on('change', 'select[name=operator]', function () {
setNegativeOperator($(this));
});
function reloadModal() { function reloadModal() {
$.ajax({ $.ajax({
url: 'get_notifier_config_modal', url: 'get_notifier_config_modal',
@@ -433,16 +415,30 @@
}); });
% if notifier['agent_name'] == 'facebook': % if notifier['agent_name'] == 'facebook':
if (location.protocol !== 'https:') {
$('#tabs-config .form-group:first').prepend(
'<div class="form-group">' +
'<label>Warning</label>' +
'<p class="help-block" style="color: #eb8600;">Facebook requires HTTPS for authorization. ' +
'Please enable HTTPS for Tautulli under <a data-tab-destination="tabs-web_interface" data-dismiss="modal" style="cursor: pointer;">Web Interface</a>.</p>' +
'</div>'
);
$('#facebook_redirect_uri').val('HTTPS not enabled');
} else {
$('#facebook_redirect_uri').val(location.href.split('/settings')[0] + '/facebook_redirect');
}
function disableFacebookRequest() { function disableFacebookRequest() {
if ($('#facebook_app_id').val() !== '' && $('#facebook_app_secret').val() !== '') { $('#facebook_facebookStep1').prop('disabled', false); } if ($('#facebook_app_id').val() !== '' && $('#facebook_app_secret').val() !== '') { $('#facebook_facebook_auth').prop('disabled', false); }
else { $('#facebook_facebookStep1').prop('disabled', true); } else { $('#facebook_facebook_auth').prop('disabled', true); }
} }
disableFacebookRequest(); disableFacebookRequest();
$('#facebook_app_id, #facebook_app_secret').on('change', function () { $('#facebook_app_id, #facebook_app_secret').on('change', function () {
disableFacebookRequest(); disableFacebookRequest();
}); });
$('#facebook_facebookStep1').click(function () { $('#facebook_facebook_auth').click(function () {
// Remove trailing '/' from Facebook redirect URI // Remove trailing '/' from Facebook redirect URI
if ($('#facebook_redirect_uri') && $('#facebook_redirect_uri').val().endsWith('/')) { if ($('#facebook_redirect_uri') && $('#facebook_redirect_uri').val().endsWith('/')) {
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1)); $('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
@@ -450,7 +446,7 @@
var facebook_token; var facebook_token;
$.ajax({ $.ajax({
url: 'facebookStep1', url: 'facebook_auth',
data: { data: {
app_id: $('#facebook_app_id').val(), app_id: $('#facebook_app_id').val(),
app_secret: $('#facebook_app_secret').val(), app_secret: $('#facebook_app_secret').val(),
@@ -508,7 +504,7 @@
}); });
% elif notifier['agent_name'] == 'osx': % elif notifier['agent_name'] == 'osx':
$('#osxnotifyregister').click(function () { $('#osx_notify_register').click(function () {
var osx_notify_app = $('#osx_notify_app').val(); var osx_notify_app = $('#osx_notify_app').val();
$.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); }); $.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
}); });
@@ -606,6 +602,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 % endif
function validateLogic() { function validateLogic() {

View File

@@ -10,7 +10,7 @@ DOCUMENTATION :: END
</%doc> </%doc>
<ul class="stacked-configs list-unstyled"> <ul class="stacked-configs list-unstyled">
% for notifier in sorted(notifiers_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])): % for notifier in sorted(notifiers_list, key=lambda k: (k['agent_label'].lower(), k['friendly_name'], k['id'])):
<li class="notification-agent" data-id="${notifier['id']}"> <li class="notification-agent" data-id="${notifier['id']}">
<span> <span>
<span class="toggle-left trigger-tooltip ${'active' if notifier['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Triggers ${'active' if notifier['active'] else 'inactive'}"><i class="fa fa-lg fa-bell"></i></span> <span class="toggle-left trigger-tooltip ${'active' if notifier['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Triggers ${'active' if notifier['active'] else 'inactive'}"><i class="fa fa-lg fa-bell"></i></span>

View File

@@ -60,9 +60,9 @@
<input type="hidden" id="show_advanced_settings" name="show_advanced_settings" value="${config['show_advanced_settings']}" required> <input type="hidden" id="show_advanced_settings" name="show_advanced_settings" value="${config['show_advanced_settings']}" required>
<div class="tab-content"> <div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-help_info"> <div role="tabpanel" class="tab-pane active" id="tabs-help_info">
% if common.VERSION_NUMBER: % if common.RELEASE:
<div class="padded-header"> <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> </div>
% endif % endif
<div class="padded-header"> <div class="padded-header">
@@ -267,6 +267,21 @@
<div role="tabpanel" class="tab-pane" id="tabs-homepage"> <div role="tabpanel" class="tab-pane" id="tabs-homepage">
<div class="padded-header">
<h3>Activity</h3>
</div>
<div class="form-group">
<label for="home_refresh_interval">Activty Refresh Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="home_refresh_interval" name="home_refresh_interval" value="${config['home_refresh_interval']}" size="5" data-parsley-min="2" data-parsley-trigger="change" data-parsley-errors-container="#home_refresh_interval_error" required>
</div>
<div id="home_refresh_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">Set the interval (in seconds) to refresh the current activity on the homepage. Minimum 2.</p>
</div>
<div class="padded-header"> <div class="padded-header">
<h3>Sections</h3> <h3>Sections</h3>
</div> </div>
@@ -642,7 +657,7 @@
<label for="pms_port">Plex Port</label> <label for="pms_port">Plex Port</label>
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-2">
<input data-parsley-type="integer" class="pms-settings form-control" type="text" id="pms_port" name="pms_port" value="${config['pms_port']}" size="30" data-parsley-trigger="change" data-parsley-errors-container="#pms_port_error" required> <input data-parsley-type="integer" class="form-control pms-settings" type="text" id="pms_port" name="pms_port" value="${config['pms_port']}" size="30" data-parsley-trigger="change" data-parsley-errors-container="#pms_port_error" required>
</div> </div>
<div id="pms_port_error" class="alert alert-danger settings-alert" role="alert"></div> <div id="pms_port_error" class="alert alert-danger settings-alert" role="alert"></div>
</div> </div>
@@ -650,29 +665,40 @@
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle" data-id="pms_is_remote" value="1" ${checked(config['pms_is_remote'])}> Remote Server <input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle pms-settings" data-id="pms_is_remote" value="1" ${checked(config['pms_is_remote'])}> Remote Server
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}"> <input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
</label> </label>
<p class="help-block">Check this if your Plex Server is not on the same local network as Tautulli.</p> <p class="help-block">Check this if your Plex Server is not on the same local network as Tautulli.</p>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle" data-id="pms_ssl" value="1" ${checked(config['pms_ssl'])}> Use SSL <input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle pms-settings" data-id="pms_ssl" value="1" ${checked(config['pms_ssl'])}> Use SSL
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}"> <input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
</label> </label>
<p class="help-block">If you have secure connections enabled on your Plex Server, communicate with it securely.</p> <p class="help-block">If you have secure connections enabled on your Plex Server, communicate with it securely.</p>
</div> </div>
<div class="form-group">
<label for="pms_url">Plex Server URL</label>
<div class="row">
<div class="col-md-9">
<input type="text" class="form-control" id="pms_url" name="pms_url" value="${config['pms_url']}" size="30" readonly>
</div>
</div>
<p class="help-block">
The server URL that Tautulli will use to connect to your Plex server. Retrieved automatically.
</p>
</div>
<div class="checkbox advanced-setting"> <div class="checkbox advanced-setting">
<label> <label>
<input type="checkbox" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection <input type="checkbox" class="pms-settings" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
</label> </label>
<span id="cloudManualConnection" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span> <span id="cloudManualConnection" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
<p class="help-block">Use the user defined connection details. Do not retrieve the server connection URL automatically.</p> <p class="help-block">Use the user defined connection details. Do not retrieve the server connection URL automatically.</p>
</div> </div>
<div class="form-group advanced-setting"> <div class="form-group advanced-setting">
<label for="pms_logs_folder">Plex Web URL</label> <label for="pms_web_url">Plex Web URL</label>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-9">
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" id="pms_web_url" name="pms_web_url" value="${config['pms_web_url']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^https?:\/\/\S+$|^https:\/\/app.plex.tv\/desktop$" data-parsley-errors-container="#pms_web_url_error" data-parsley-error-message="Invalid Plex Web URL."> <input type="text" class="form-control" id="pms_web_url" name="pms_web_url" value="${config['pms_web_url']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^https?:\/\/\S+$|^https:\/\/app.plex.tv\/desktop$" data-parsley-errors-container="#pms_web_url_error" data-parsley-error-message="Invalid Plex Web URL.">
<span class="input-group-btn"> <span class="input-group-btn">
@@ -953,6 +979,9 @@
<p class="help-block"> <p class="help-block">
Add a new notification agent, or configure an existing notification agent by clicking the settings icon on the right. Add a new notification agent, or configure an existing notification agent by clicking the settings icon on the right.
</p> </p>
<p class="help-block">
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/Notification-Agents-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>Notification Agents Guide</a> for instructions on setting up each notification agent.
</p>
<br /> <br />
<div id="plexpy-notifiers-table"> <div id="plexpy-notifiers-table">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading notification agents...</div> <div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading notification agents...</div>
@@ -967,7 +996,7 @@
<h3>Database Import</h3> <h3>Database Import</h3>
</div> </div>
<p class="help-block">Click a button below to import an exisiting database from another app.</p> <p class="help-block">Click a button below to import an existing database from another app.</p>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexwatch">PlexWatch</button> <button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexwatch">PlexWatch</button>
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexivity">Plexivity</button> <button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexivity">Plexivity</button>
@@ -1062,8 +1091,8 @@
</div> </div>
<p class="form-group"> <p class="form-group">
<label>Registered Devices</label> <label>Registered Devices</label>
<p class="help-block">Register a new device, or configure an existing device by clicking the settings icon on the right.</p> <p class="help-block">Register a new device using a QR code, or configure an existing device by clicking the settings icon on the right.</p>
<p id="app_api_msg" style="color: #eb8600;">The API must be enabled under <a data-tab-destination="tabs-access_control" style="cursor: pointer;">Access Control</a> to use the app.</p> <p id="app_api_msg" style="color: #eb8600;">The API must be enabled under <a data-tab-destination="tabs-web_interface" style="cursor: pointer;">Web Interface</a> to use the app.</p>
<div class="row"> <div class="row">
<div id="plexpy-mobile-devices-table" class="col-md-12"> <div id="plexpy-mobile-devices-table" class="col-md-12">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading registered devices...</div> <div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading registered devices...</div>
@@ -1214,7 +1243,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<ul class="stacked-configs list-unstyled"> <ul class="stacked-configs list-unstyled">
% for agent in available_notification_agents: % for agent in sorted(available_notification_agents, key=lambda k: k['label'].lower()):
<li class="new-notification-agent" data-id="${agent['id']}"> <li class="new-notification-agent" data-id="${agent['id']}">
<span>${agent['label']}</span> <span>${agent['label']}</span>
</li> </li>
@@ -1573,7 +1602,7 @@ $(document).ready(function() {
} }
function preSaveChecks(_callback) { function preSaveChecks(_callback) {
if ($("#pms_identifier").val() == "") { if (serverChanged) {
verifyServer(); verifyServer();
} }
verifyPMSWebURL(); verifyPMSWebURL();
@@ -1585,7 +1614,7 @@ $(document).ready(function() {
// Alert the user that their changes require a restart. // Alert the user that their changes require a restart.
function postSaveChecks() { function postSaveChecks() {
if (serverChanged || authChanged || httpChanged || directoryChanged) { if (authChanged || httpChanged || directoryChanged) {
$('#restart-modal').modal('show'); $('#restart-modal').modal('show');
} }
$("#http_hashed_password").val($("#http_hash_password").is(":checked") ? 1 : 0); $("#http_hashed_password").val($("#http_hash_password").is(":checked") ? 1 : 0);
@@ -1769,9 +1798,8 @@ $(document).ready(function() {
$( ".pms-settings" ).change(function() { $( ".pms-settings" ).change(function() {
serverChanged = true; serverChanged = true;
$("#pms_identifier").val("");
$("#server_changed").prop('checked', true); $("#server_changed").prop('checked', true);
verifyServer(); $("#pms_verify").hide();
}); });
$('.checkbox-toggle').click(function () { $('.checkbox-toggle').click(function () {
@@ -1841,7 +1869,11 @@ $(document).ready(function() {
$('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0); $('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0); $('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
$('#pms_url_manual').prop('checked', false); $('#pms_url_manual').prop('checked', false);
$('#pms_url').val('Please verify your server above to retrieve the URL');
PMSCloudCheck(); PMSCloudCheck();
},
onDropdownOpen: function() {
this.clear();
} }
}); });
var select_pms = $select_pms[0].selectize; var select_pms = $select_pms[0].selectize;
@@ -1906,6 +1938,7 @@ $(document).ready(function() {
var pms_identifier = $("#pms_identifier").val(); var pms_identifier = $("#pms_identifier").val();
var pms_ssl = $("#pms_ssl").val(); var pms_ssl = $("#pms_ssl").val();
var pms_is_remote = $("#pms_is_remote").val(); var pms_is_remote = $("#pms_is_remote").val();
var pms_url_manual = $("#pms_url_manual").is(':checked') ? 1 : 0;
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) { if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast'); $("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
@@ -1914,9 +1947,11 @@ $(document).ready(function() {
data: { data: {
hostname: pms_ip, hostname: pms_ip,
port: pms_port, port: pms_port,
identifier: pms_identifier,
ssl: pms_ssl, ssl: pms_ssl,
remote: pms_is_remote remote: pms_is_remote,
manual: pms_url_manual,
get_url: true,
test_websocket: true
}, },
cache: true, cache: true,
async: true, async: true,
@@ -1925,12 +1960,27 @@ $(document).ready(function() {
$("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast'); $("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
$("#pms_ip_group").addClass("has-error"); $("#pms_ip_group").addClass("has-error");
}, },
success: function (json) { success: function(xhr, status) {
var machine_identifier = json; var result = xhr;
if (machine_identifier) { var identifier = result.identifier;
$("#pms_identifier").val(machine_identifier); var url = result.url;
$("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast'); var ws = result.ws;
$("#pms_ip_group").removeClass("has-error"); if (identifier) {
$("#pms_identifier").val(identifier);
if (url) {
$("#pms_url").val(url);
}
if (ws === false) {
$("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
$("#pms_ip_group").addClass("has-error");
showMsg('<i class="fa fa-exclamation-circle"></i> Server found but unable to connect websocket.<br>Check the <a href="logs">logs</a> for errors.', false, true, 5000, true)
} else {
$("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
$("#pms_ip_group").removeClass("has-error");
serverChanged = false;
}
if (_callback) { if (_callback) {
_callback(); _callback();
@@ -1950,7 +2000,6 @@ $(document).ready(function() {
} }
$('#verify_server_button').on('click', function(){ $('#verify_server_button').on('click', function(){
$("#pms_identifier").val("");
verifyServer(); verifyServer();
}); });

View File

@@ -58,6 +58,10 @@ DOCUMENTATION :: END
<div class="col-sm-12 text-muted stream-info-current"> <div class="col-sm-12 text-muted stream-info-current">
<i class="fa fa-exclamation-circle"></i> Current session. Updated stream details below may be delayed. <i class="fa fa-exclamation-circle"></i> Current session. Updated stream details below may be delayed.
</div> </div>
% elif data['pre_tautulli']:
<div class="col-sm-12 text-muted stream-info-current">
<i class="fa fa-exclamation-circle"></i> Pre-Tautulli history. Stream details below may be incorrect.
</div>
% endif % endif
<table class="stream-info" style="margin-top: 0;"> <table class="stream-info" style="margin-top: 0;">
<thead> <thead>
@@ -84,8 +88,8 @@ DOCUMENTATION :: END
<tbody> <tbody>
<tr> <tr>
<td>Bitrate</td> <td>Bitrate</td>
<td>${data['stream_bitrate']} kbps</td> <td>${data['stream_bitrate']} ${'kbps' if data['stream_bitrate'] else ''}</td>
<td>${data['bitrate']} kbps</td> <td>${data['bitrate']} ${'kbps' if data['bitrate'] else ''}</td>
</tr> </tr>
% if data['media_type'] != 'track': % if data['media_type'] != 'track':
<tr> <tr>
@@ -154,8 +158,8 @@ DOCUMENTATION :: END
</tr> </tr>
<tr> <tr>
<td>Bitrate</td> <td>Bitrate</td>
<td>${data['stream_video_bitrate']} kbps</td> <td>${data['stream_video_bitrate']} ${'kbps' if data['stream_video_bitrate'] else ''}</td>
<td>${data['video_bitrate']} kbps</td> <td>${data['video_bitrate']} ${'kbps' if data['video_bitrate'] else ''}</td>
</tr> </tr>
<tr> <tr>
<td>Width</td> <td>Width</td>
@@ -199,8 +203,8 @@ DOCUMENTATION :: END
</tr> </tr>
<tr> <tr>
<td>Bitrate</td> <td>Bitrate</td>
<td>${data['stream_audio_bitrate']} kbps</td> <td>${data['stream_audio_bitrate']} ${'kbps' if data['stream_audio_bitrate'] else ''}</td>
<td>${data['audio_bitrate']} kbps</td> <td>${data['audio_bitrate']} ${'kbps' if data['audio_bitrate'] else ''}</td>
</tr> </tr>
<tr> <tr>
<td>Channels</td> <td>Channels</td>

View File

@@ -2,7 +2,7 @@
<%def name="headIncludes()"> <%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css"> <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"> <link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<style> <style>
td {word-wrap: break-word} td {word-wrap: break-word}
@@ -100,7 +100,7 @@
// Load user ids and names (for the selector) // Load user ids and names (for the selector)
$.ajax({ $.ajax({
url: 'get_user_names', url: 'get_user_names',
type: 'get', type: 'GET',
dataType: 'json', dataType: 'json',
success: function (data) { success: function (data) {
var select = $('#sync-user'); var select = $('#sync-user');
@@ -116,7 +116,8 @@
function loadSyncTable(selected_user_id) { function loadSyncTable(selected_user_id) {
sync_table_options.ajax = { sync_table_options.ajax = {
url: 'get_sync?user_id=' + selected_user_id url: 'get_sync?user_id=' + selected_user_id,
type: 'POST'
}; };
sync_table = $('#sync_table').DataTable(sync_table_options); sync_table = $('#sync_table').DataTable(sync_table_options);
var colvis = new $.fn.dataTable.ColVis(sync_table, { var colvis = new $.fn.dataTable.ColVis(sync_table, {
@@ -134,7 +135,7 @@
}); });
} }
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"; var selected_user_id = "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}";
loadSyncTable(selected_user_id); loadSyncTable(selected_user_id);
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':

View File

@@ -32,7 +32,7 @@ DOCUMENTATION :: END
<%def name="headIncludes()"> <%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css"> <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/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>
<%def name="body()"> <%def name="body()">
@@ -413,7 +413,7 @@ DOCUMENTATION :: END
// Build watch history table // Build watch history table
history_table_options.ajax = { history_table_options.ajax = {
url: 'get_history', url: 'get_history',
type: 'post', type: 'POST',
data: function ( d ) { data: function ( d ) {
return { return {
json_data: JSON.stringify( d ), json_data: JSON.stringify( d ),
@@ -442,7 +442,8 @@ DOCUMENTATION :: END
function loadSyncTable() { function loadSyncTable() {
// Build user sync table // Build user sync table
sync_table_options.ajax = { sync_table_options.ajax = {
url: 'get_sync?user_id=' + user_id url: 'get_sync?user_id=' + user_id,
type: 'POST'
}; };
sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options); sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options);
sync_table.column(2).visible(false); sync_table.column(2).visible(false);
@@ -457,7 +458,7 @@ DOCUMENTATION :: END
// Build user IP table // Build user IP table
user_ip_table_options.ajax = { user_ip_table_options.ajax = {
url: 'get_user_ips', url: 'get_user_ips',
type: 'post', type: 'POST',
data: function ( d ) { data: function ( d ) {
return { return {
json_data: JSON.stringify( d ), json_data: JSON.stringify( d ),
@@ -474,6 +475,7 @@ DOCUMENTATION :: END
// Build user login table // Build user login table
login_log_table_options.ajax = { login_log_table_options.ajax = {
url: 'get_user_logins', url: 'get_user_logins',
type: 'POST',
data: function(d) { data: function(d) {
return { return {
json_data: JSON.stringify(d), json_data: JSON.stringify(d),

View File

@@ -3,7 +3,7 @@
<%def name="headIncludes()"> <%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css"> <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/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>
<%def name="body()"> <%def name="body()">
@@ -94,7 +94,7 @@
json_data: JSON.stringify(d) json_data: JSON.stringify(d)
}; };
} }
} };
users_list_table = $('#users_list_table').DataTable(users_list_table_options); users_list_table = $('#users_list_table').DataTable(users_list_table_options);
var colvis = new $.fn.dataTable.ColVis(users_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] }); var colvis = new $.fn.dataTable.ColVis(users_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] });

View File

@@ -14,7 +14,7 @@
<meta name="author" content=""> <meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet"> <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/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/selectize.bootstrap3.css" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet"> <link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet"> <link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
@@ -94,7 +94,7 @@
<label for="pms_ip">Plex IP or Hostname</label> <label for="pms_ip">Plex IP or Hostname</label>
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<select class="form-control selectize-pms-ip" id="pms_ip" name="pms_ip"> <select class="form-control pms-settings selectize-pms-ip" id="pms_ip" name="pms_ip">
<option value="${config['pms_ip']}" selected>${config['pms_ip']}</option> <option value="${config['pms_ip']}" selected>${config['pms_ip']}</option>
</select> </select>
</div> </div>
@@ -104,12 +104,12 @@
<label for="pms_port">Plex Port</label> <label for="pms_port">Plex Port</label>
<div class="row"> <div class="row">
<div class="col-xs-3"> <div class="col-xs-3">
<input type="text" class="form-control pms_settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required> <input type="text" class="form-control pms-settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
</div> </div>
<div class="col-xs-4"> <div class="col-xs-4">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle" data-id="pms_ssl" value="1" ${helpers.checked(config['pms_ssl'])}> Use SSL <input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle pms-settings" data-id="pms_ssl" value="1" ${helpers.checked(config['pms_ssl'])}> Use SSL
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}"> <input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
</label> </label>
</div> </div>
@@ -117,16 +117,16 @@
<div class="col-xs-4"> <div class="col-xs-4">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle" data-id="pms_is_remote" value="1" ${helpers.checked(config['pms_is_remote'])}> Remote Server <input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle pms-settings" data-id="pms_is_remote" value="1" ${helpers.checked(config['pms_is_remote'])}> Remote Server
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}"> <input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
</label> </label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<input type="hidden" class="form-control pms-settings" id="pms_valid" data-validate="validatePMSip" value=""> <input type="hidden" id="pms_valid" data-validate="validatePMSip" value="">
<input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}"> <input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}">
<input type="hidden" class="form-control pms-settings" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}"> <input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span> <a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
</div> </div>
@@ -374,6 +374,9 @@ $(document).ready(function() {
$('#pms_is_remote_checkbox').prop('disabled', false); $('#pms_is_remote_checkbox').prop('disabled', false);
$('#pms_ssl_checkbox').prop('disabled', false); $('#pms_ssl_checkbox').prop('disabled', false);
} }
},
onDropdownOpen: function() {
this.clear();
} }
}); });
var select_pms = $select_pms[0].selectize; var select_pms = $select_pms[0].selectize;
@@ -419,7 +422,8 @@ $(document).ready(function() {
port: pms_port, port: pms_port,
identifier: pms_identifier, identifier: pms_identifier,
ssl: pms_ssl, ssl: pms_ssl,
remote: pms_is_remote }, remote: pms_is_remote
},
cache: true, cache: true,
async: true, async: true,
timeout: 5000, timeout: 5000,
@@ -427,10 +431,11 @@ $(document).ready(function() {
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!'); $("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
$('#pms-verify-status').fadeIn('fast'); $('#pms-verify-status').fadeIn('fast');
}, },
success: function (json) { success: function(xhr, status) {
var machine_identifier = json; var result = xhr;
if (machine_identifier) { var identifier = result.identifier;
$("#pms_identifier").val(machine_identifier); if (identifier) {
$("#pms_identifier").val(identifier);
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!'); $("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
$('#pms-verify-status').fadeIn('fast'); $('#pms-verify-status').fadeIn('fast');
pms_verified = true; pms_verified = true;

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,12 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>Label</key> <key>Label</key>
<string>plexpy</string> <string>tautulli</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <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>/usr/bin/python</string>
<string>/Applications/PlexPy/PlexPy.py</string> <string>/Applications/Tautulli/Tautulli.py</string>
</array> </array>
<key>RunAtLoad</key> <key>RunAtLoad</key>
<true/> <true/>

View File

@@ -2,9 +2,9 @@
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1"> <!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
<!-- <!--
Created by Manifold 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"/> <create_default_instance enabled="true"/>
@@ -19,10 +19,10 @@
</dependency> </dependency>
<method_context> <method_context>
<method_credential user="plexpy" group="nogroup"/> <method_credential user="tautulli" group="nogroup"/>
</method_context> </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"/> <exec_method type="method" name="stop" exec=":kill" timeout_seconds="60"/>
@@ -37,7 +37,7 @@
<template> <template>
<common_name> <common_name>
<loctext xml:lang="C"> <loctext xml:lang="C">
PlexPy Tautulli
</loctext> </loctext>
</common_name> </common_name>
</template> </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 # Service Unit file for systemd system manager
# #
# INSTALLATION NOTES # INSTALLATION NOTES
# #
# 1. Rename this file as you want, ensuring that it ends in .service # 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 # 2. Adjust configuration settings as required. More details in the
# "CONFIGURATION NOTES" section shown below. # "CONFIGURATION NOTES" section shown below.
@@ -15,39 +15,39 @@
# #
# 4. Enable boot-time autostart with the following commands: # 4. Enable boot-time autostart with the following commands:
# systemctl daemon-reload # systemctl daemon-reload
# systemctl enable plexpy.service # systemctl enable tautulli.service
# #
# 5. Start now with the following command: # 5. Start now with the following command:
# systemctl start plexpy.service # systemctl start tautulli.service
# #
# CONFIGURATION NOTES # CONFIGURATION NOTES
# #
# - The example settings in this file assume that you will run PlexPy as user: 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 plexpy directory: # - To create this user and give it ownership of the tautulli directory:
# sudo adduser --system --no-create-home plexpy # sudo adduser --system --no-create-home tautulli
# sudo chown plexpy:nogroup -R /opt/plexpy # sudo chown tautulli:nogroup -R /opt/Tautulli
# #
# - Option names (e.g. ExecStart=, Type=) appear to be case-sensitive) # - Option names (e.g. ExecStart=, Type=) appear to be case-sensitive)
# #
# - Adjust ExecStart= to point to: # - 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) # 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) # multi-user.target equates to runlevel 3 (multi-user text mode)
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode) # graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
[Unit] [Unit]
Description=PlexPy - Stats for Plex Media Server usage Description=Tautulli - Stats for Plex Media Server usage
[Service] [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 GuessMainPID=no
Type=forking Type=forking
User=plexpy User=tautulli
Group=nogroup Group=nogroup
[Install] [Install]

View File

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

View File

@@ -45,7 +45,8 @@ __version__ = version.__version__
FACEBOOK_GRAPH_URL = "https://graph.facebook.com/" FACEBOOK_GRAPH_URL = "https://graph.facebook.com/"
FACEBOOK_OAUTH_DIALOG_URL = "https://www.facebook.com/dialog/oauth?" FACEBOOK_OAUTH_DIALOG_URL = "https://www.facebook.com/dialog/oauth?"
VALID_API_VERSIONS = ["2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9"] VALID_API_VERSIONS = [
"2.5", "2.6", "2.7", "2.8", "2.9", "2.10", "2.11", "2.12"]
VALID_SEARCH_TYPES = ["page", "event", "group", "place", "placetopic", "user"] VALID_SEARCH_TYPES = ["page", "event", "group", "place", "placetopic", "user"]
@@ -89,7 +90,7 @@ class GraphAPI(object):
self.session = session or requests.Session() self.session = session or requests.Session()
if version: if version:
version_regex = re.compile("^\d\.\d$") version_regex = re.compile("^\d\.\d{1,2}$")
match = version_regex.search(str(version)) match = version_regex.search(str(version))
if match is not None: if match is not None:
if str(version) not in VALID_API_VERSIONS: if str(version) not in VALID_API_VERSIONS:
@@ -229,7 +230,7 @@ class GraphAPI(object):
try: try:
headers = response.headers headers = response.headers
version = headers["facebook-api-version"].replace("v", "") version = headers["facebook-api-version"].replace("v", "")
return float(version) return str(version)
except Exception: except Exception:
raise GraphAPIError("API version number not available") raise GraphAPIError("API version number not available")
@@ -369,24 +370,24 @@ class GraphAPIError(Exception):
self.code = None self.code = None
try: try:
self.type = result["error_code"] self.type = result["error_code"]
except: except (KeyError, TypeError):
self.type = "" self.type = ""
# OAuth 2.0 Draft 10 # OAuth 2.0 Draft 10
try: try:
self.message = result["error_description"] self.message = result["error_description"]
except: except (KeyError, TypeError):
# OAuth 2.0 Draft 00 # OAuth 2.0 Draft 00
try: try:
self.message = result["error"]["message"] self.message = result["error"]["message"]
self.code = result["error"].get("code") self.code = result["error"].get("code")
if not self.type: if not self.type:
self.type = result["error"].get("type", "") self.type = result["error"].get("type", "")
except: except (KeyError, TypeError):
# REST server style # REST server style
try: try:
self.message = result["error_msg"] self.message = result["error_msg"]
except: except (KeyError, TypeError):
self.message = result self.message = result
Exception.__init__(self, self.message) Exception.__init__(self, self.message)

View File

@@ -46,6 +46,7 @@ import notifiers
import plextv import plextv
import users import users
import versioncheck import versioncheck
import web_socket
import plexpy.config import plexpy.config
PROG_DIR = None PROG_DIR = None
@@ -95,6 +96,7 @@ HTTP_ROOT = None
DEV = False DEV = False
WEBSOCKET = None
WS_CONNECTED = False WS_CONNECTED = False
PLEX_SERVER_UP = None PLEX_SERVER_UP = None
@@ -240,7 +242,7 @@ def initialize(config_file):
# Get the previous release from the file # Get the previous release from the file
release_file = os.path.join(DATA_DIR, "release.lock") release_file = os.path.join(DATA_DIR, "release.lock")
PREV_RELEASE = common.VERSION_NUMBER PREV_RELEASE = common.RELEASE
if os.path.isfile(release_file): if os.path.isfile(release_file):
try: try:
with open(release_file, "r") as fp: with open(release_file, "r") as fp:
@@ -252,7 +254,7 @@ def initialize(config_file):
PREV_RELEASE = 'v1.4.25' PREV_RELEASE = 'v1.4.25'
# Check if the release was updated # Check if the release was updated
if common.VERSION_NUMBER != PREV_RELEASE: if common.RELEASE != PREV_RELEASE:
CONFIG.UPDATE_SHOW_CHANGELOG = 1 CONFIG.UPDATE_SHOW_CHANGELOG = 1
CONFIG.write() CONFIG.write()
_UPDATE = True _UPDATE = True
@@ -260,7 +262,7 @@ def initialize(config_file):
# Write current release version to file for update checking # Write current release version to file for update checking
try: try:
with open(release_file, "w") as fp: with open(release_file, "w") as fp:
fp.write(common.VERSION_NUMBER) fp.write(common.RELEASE)
except IOError as e: except IOError as e:
logger.error(u"Unable to write current release to file '%s': %s" % logger.error(u"Unable to write current release to file '%s': %s" %
(release_file, e)) (release_file, e))
@@ -1594,6 +1596,18 @@ def dbcheck():
'ALTER TABLE poster_urls ADD COLUMN delete_hash TEXT' 'ALTER TABLE poster_urls ADD COLUMN delete_hash TEXT'
) )
# Rename notifiers in the database
result = c_db.execute('SELECT agent_label FROM notifiers '
'WHERE agent_label = "XBMC" OR agent_label = "OSX Notify"').fetchone()
if result:
logger.debug(u"Altering database. Renaming notifiers.")
c_db.execute(
'UPDATE notifiers SET agent_label = "Kodi" WHERE agent_label = "XBMC"'
)
c_db.execute(
'UPDATE notifiers SET agent_label = "macOS Notification Center" WHERE agent_label = "OSX Notify"'
)
# Add "Local" user to database as default unauthenticated user. # Add "Local" user to database as default unauthenticated user.
result = c_db.execute('SELECT id FROM users WHERE username = "Local"') result = c_db.execute('SELECT id FROM users WHERE username = "Local"')
if not result.fetchone(): if not result.fetchone():
@@ -1621,8 +1635,15 @@ def upgrade():
def shutdown(restart=False, update=False, checkout=False): def shutdown(restart=False, update=False, checkout=False):
cherrypy.engine.exit() cherrypy.engine.exit()
SCHED.shutdown(wait=False)
activity_handler.ACTIVITY_SCHED.shutdown(wait=False) # Shutdown the websocket connection
if WEBSOCKET:
web_socket.shutdown()
if SCHED.running:
SCHED.shutdown(wait=False)
if activity_handler.ACTIVITY_SCHED.running:
activity_handler.ACTIVITY_SCHED.shutdown(wait=False)
# Stop the notification threads # Stop the notification threads
for i in range(CONFIG.NOTIFICATION_THREADS): for i in range(CONFIG.NOTIFICATION_THREADS):
@@ -1693,7 +1714,7 @@ def initialize_tracker():
data = { data = {
'dataSource': 'server', 'dataSource': 'server',
'appName': 'Tautulli', 'appName': 'Tautulli',
'appVersion': common.VERSION_NUMBER, 'appVersion': common.RELEASE,
'appId': plexpy.INSTALL_TYPE, 'appId': plexpy.INSTALL_TYPE,
'appInstallerId': plexpy.CONFIG.GIT_BRANCH, 'appInstallerId': plexpy.CONFIG.GIT_BRANCH,
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_VERSION), # App Platform 'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_VERSION), # App Platform
@@ -1702,7 +1723,8 @@ def initialize_tracker():
'noninteractive': True '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) tracker.set(data)
return tracker return tracker
@@ -1721,4 +1743,7 @@ def analytics_event(category, action, label=None, value=None, **kwargs):
data.update(kwargs) data.update(kwargs)
if TRACKER: 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 return None
def update_db_session(self, session=None): def update_db_session(self, session=None):
# Update our session temp table values if session is None:
monitor_proc = activity_processor.ActivityProcessor() session = self.get_live_session()
monitor_proc.write_session(session=session, notify=False)
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): def on_start(self):
if self.is_valid_session(): if self.is_valid_session():
@@ -114,10 +127,7 @@ class ActivityHandler(object):
# Update the session state and viewOffset # Update the session state and viewOffset
# Set force_stop to true to disable the state set # Set force_stop to true to disable the state set
if not force_stop: if not force_stop:
ap.set_session_state(session_key=self.get_session_key(), self.set_session_state()
state=self.timeline['state'],
view_offset=self.timeline['viewOffset'],
stopped=int(time.time()))
# Retrieve the session data from our temp table # Retrieve the session data from our temp table
db_session = ap.get_session_by_key(session_key=self.get_session_key()) 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())) ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=int(time.time()))
# Update the session state and viewOffset # Update the session state and viewOffset
ap.set_session_state(session_key=self.get_session_key(), self.update_db_session()
state=self.timeline['state'],
view_offset=self.timeline['viewOffset'],
stopped=int(time.time()))
# Retrieve the session data from our temp table # Retrieve the session data from our temp table
db_session = ap.get_session_by_key(session_key=self.get_session_key()) 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) ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=None)
# Update the session state and viewOffset # Update the session state and viewOffset
ap.set_session_state(session_key=self.get_session_key(), self.update_db_session()
state=self.timeline['state'],
view_offset=self.timeline['viewOffset'],
stopped=int(time.time()))
# Retrieve the session data from our temp table # Retrieve the session data from our temp table
db_session = ap.get_session_by_key(session_key=self.get_session_key()) 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()) buffer_last_triggered = ap.get_session_buffer_trigger_time(self.get_session_key())
# Update the session state and viewOffset # Update the session state and viewOffset
ap.set_session_state(session_key=self.get_session_key(), self.update_db_session()
state=self.timeline['state'],
view_offset=self.timeline['viewOffset'],
stopped=int(time.time()))
time_since_last_trigger = 0 time_since_last_trigger = 0
if buffer_last_triggered: if buffer_last_triggered:
@@ -243,9 +244,7 @@ class ActivityHandler(object):
# Update the session in our temp session table # Update the session in our temp session table
# if the last set temporary stopped time exceeds 15 seconds # if the last set temporary stopped time exceeds 15 seconds
if int(time.time()) - db_session['stopped'] > 60: if int(time.time()) - db_session['stopped'] > 60:
session = self.get_live_session() self.update_db_session()
if session:
self.update_db_session(session=session)
# Start our state checks # Start our state checks
if this_state != last_state: if this_state != last_state:

View File

@@ -293,8 +293,8 @@ def connect_server(log=True, startup=False):
try: try:
web_socket.start_thread() web_socket.start_thread()
except: except Exception as e:
logger.error(u"Websocket :: Unable to open connection.") logger.error(u"Websocket :: Unable to open connection: %s." % e)
def check_server_access(): def check_server_access():

View File

@@ -167,8 +167,8 @@ class API2:
""" """
logfile = os.path.join(plexpy.CONFIG.LOG_DIR, logger.FILENAME) logfile = os.path.join(plexpy.CONFIG.LOG_DIR, logger.FILENAME)
templog = [] templog = []
start = int(kwargs.get('start', 0)) start = int(start)
end = int(kwargs.get('end', 0)) end = int(end)
if regex: if regex:
logger.api_debug(u'Tautulli APIv2 :: Filtering log using regex %s' % regex) logger.api_debug(u'Tautulli APIv2 :: Filtering log using regex %s' % regex)
@@ -652,10 +652,9 @@ General optional parameters:
# {result: error, message: 'Some shit happend'} # {result: error, message: 'Some shit happend'}
if isinstance(ret, dict): if isinstance(ret, dict):
if ret.get('message'): if ret.get('message'):
self._api_msg = ret.get('message', {}) self._api_msg = ret.pop('message', None)
ret = {}
if ret.get('result'): if ret.get('result'):
self._api_result_type = ret.get('result') self._api_result_type = ret.pop('result', None)
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret)) return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))

View File

@@ -19,13 +19,12 @@ from collections import OrderedDict
import version import version
# Identify Our Application # Identify Our Application
USER_AGENT = 'Tautulli/-' + version.PLEXPY_BRANCH + ' v' + version.PLEXPY_RELEASE_VERSION + ' (' + platform.system() + \
' ' + platform.release() + ')'
PLATFORM = platform.system() PLATFORM = platform.system()
PLATFORM_VERSION = platform.release() PLATFORM_VERSION = platform.release()
BRANCH = version.PLEXPY_BRANCH 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_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png" DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
@@ -172,6 +171,16 @@ HW_ENCODERS = [
'nvenc' 'nvenc'
] ]
EXTRA_TYPES = {
'1': 'Trailer',
'2': 'Deleted Scene',
'3': 'Interview',
'5': 'Behind the Scenes',
'6': 'Scene',
'10': 'Featurette',
'11': 'Short'
}
SCHEDULER_LIST = [ SCHEDULER_LIST = [
'Check GitHub for updates', 'Check GitHub for updates',
'Check for server response', 'Check for server response',
@@ -392,6 +401,10 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Episode Number 00', 'type': 'int', 'value': 'episode_num00', 'description': 'The two digit episode number.', 'example': 'e.g. 06, or 06-10'}, {'name': 'Episode Number 00', 'type': 'int', 'value': 'episode_num00', 'description': 'The two digit episode number.', 'example': 'e.g. 06, or 06-10'},
{'name': 'Track Number', 'type': 'int', 'value': 'track_num', 'description': 'The track number.', 'example': 'e.g. 4, or 4-10'}, {'name': 'Track Number', 'type': 'int', 'value': 'track_num', 'description': 'The track number.', 'example': 'e.g. 4, or 4-10'},
{'name': 'Track Number 00', 'type': 'int', 'value': 'track_num00', 'description': 'The two digit track number.', 'example': 'e.g. 04, or 04-10'}, {'name': 'Track Number 00', 'type': 'int', 'value': 'track_num00', 'description': 'The two digit track number.', 'example': 'e.g. 04, or 04-10'},
{'name': 'Season Count', 'type': 'int', 'value': 'season_count', 'description': 'The number of seasons.'},
{'name': 'Episode Count', 'type': 'int', 'value': 'episode_count', 'description': 'The number of episodes.'},
{'name': 'Album Count', 'type': 'int', 'value': 'album_count', 'description': 'The number of albums.'},
{'name': 'Track Count', 'type': 'int', 'value': 'track_count', 'description': 'The number of tracks.'},
{'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'}, {'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'},
{'name': 'Release Date', 'type': 'int', 'value': 'release_date', 'description': 'The release date (in date format) for the item.'}, {'name': 'Release Date', 'type': 'int', 'value': 'release_date', 'description': 'The release date (in date format) for the item.'},
{'name': 'Air Date', 'type': 'str', 'value': 'air_date', 'description': 'The air date (in date format) for the item.'}, {'name': 'Air Date', 'type': 'str', 'value': 'air_date', 'description': 'The air date (in date format) for the item.'},

View File

@@ -209,6 +209,7 @@ _CONFIG_DEFINITIONS = {
'HOME_STATS_CARDS': (list, 'General', ['top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', \ 'HOME_STATS_CARDS': (list, 'General', ['top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', \
'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']), 'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']),
'HOME_STATS_RECENTLY_ADDED_COUNT': (int, 'General', 50), 'HOME_STATS_RECENTLY_ADDED_COUNT': (int, 'General', 50),
'HOME_REFRESH_INTERVAL': (int, 'General', 10),
'HTTPS_CREATE_CERT': (int, 'General', 1), 'HTTPS_CREATE_CERT': (int, 'General', 1),
'HTTPS_CERT': (str, 'General', ''), 'HTTPS_CERT': (str, 'General', ''),
'HTTPS_CERT_CHAIN': (str, 'General', ''), 'HTTPS_CERT_CHAIN': (str, 'General', ''),

View File

@@ -23,7 +23,7 @@ import time
import plexpy import plexpy
import logger import logger
FILENAME = "plexpy.db" FILENAME = "tautulli.db"
db_lock = threading.Lock() 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 """ """ Makes a backup of db, removes all but the last 5 backups """
if scheduler: 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: 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_folder = plexpy.CONFIG.BACKUP_DIR
backup_file_fp = os.path.join(backup_folder, backup_file) backup_file_fp = os.path.join(backup_folder, backup_file)

View File

@@ -885,6 +885,9 @@ class DataFactory(object):
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \ 'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \ 'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \ 'transcode_hw_decoding, transcode_hw_encoding, ' \
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
'transcode_width, transcode_height, ' \
'session_history_metadata.media_type, title, grandparent_title ' \ 'session_history_metadata.media_type, title, grandparent_title ' \
'FROM session_history_media_info ' \ 'FROM session_history_media_info ' \
'JOIN session_history ON session_history_media_info.id = session_history.id ' \ 'JOIN session_history ON session_history_media_info.id = session_history.id ' \
@@ -903,6 +906,9 @@ class DataFactory(object):
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \ 'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \ 'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \ 'transcode_hw_decoding, transcode_hw_encoding, ' \
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
'transcode_width, transcode_height, ' \
'media_type, title, grandparent_title ' \ 'media_type, title, grandparent_title ' \
'FROM sessions ' \ 'FROM sessions ' \
'WHERE session_key = ? %s' % user_cond 'WHERE session_key = ? %s' % user_cond
@@ -913,6 +919,23 @@ class DataFactory(object):
stream_output = {} stream_output = {}
for item in result: for item in result:
pre_tautulli = 0
# For backwards compatibility. Pick one new Tautulli key to check and override with old values.
if not item['stream_video_resolution']:
item['stream_video_resolution'] = item['video_resolution']
item['stream_container'] = item['transcode_container'] or item['container']
item['stream_video_decision'] = item['video_decision']
item['stream_video_codec'] = item['transcode_video_codec'] or item['video_codec']
item['stream_video_width'] = item['transcode_width'] or item['width']
item['stream_video_height'] = item['transcode_height'] or item['height']
item['stream_audio_decision'] = item['audio_decision']
item['stream_audio_codec'] = item['transcode_audio_codec'] or item['audio_codec']
item['stream_audio_channels'] = item['transcode_audio_channels'] or item['audio_channels']
item['video_width'] = item['width']
item['video_height'] = item['height']
pre_tautulli = 1
stream_output = {'bitrate': item['bitrate'], stream_output = {'bitrate': item['bitrate'],
'video_resolution': item['video_resolution'], 'video_resolution': item['video_resolution'],
'optimized_version': item['optimized_version'], 'optimized_version': item['optimized_version'],
@@ -951,10 +974,13 @@ class DataFactory(object):
'stream_subtitle_codec': item['stream_subtitle_codec'], 'stream_subtitle_codec': item['stream_subtitle_codec'],
'transcode_hw_decoding': item['transcode_hw_decoding'], 'transcode_hw_decoding': item['transcode_hw_decoding'],
'transcode_hw_encoding': item['transcode_hw_encoding'], 'transcode_hw_encoding': item['transcode_hw_encoding'],
'video_decision': item['video_decision'],
'audio_decision': item['audio_decision'],
'media_type': item['media_type'], 'media_type': item['media_type'],
'title': item['title'], 'title': item['title'],
'grandparent_title': item['grandparent_title'], 'grandparent_title': item['grandparent_title'],
'current_session': 1 if session_key else 0 'current_session': 1 if session_key else 0,
'pre_tautulli': pre_tautulli
} }
stream_output = {k: v or '' for k, v in stream_output.iteritems()} stream_output = {k: v or '' for k, v in stream_output.iteritems()}

View File

@@ -646,7 +646,7 @@ def whois_lookup(ip_address):
countries = ipwhois.utils.get_countries() countries = ipwhois.utils.get_countries()
nets = whois['nets'] nets = whois['nets']
for net in nets: for net in nets:
net['country'] = countries[net['country']] net['country'] = countries.get(net['country'])
if net['postal_code']: if net['postal_code']:
net['postal_code'] = net['postal_code'].replace('-', ' ') net['postal_code'] = net['postal_code'].replace('-', ' ')
except ValueError as e: except ValueError as e:
@@ -933,3 +933,36 @@ def eval_logic_groups_to_bool(logic_groups, eval_conds):
result = result or eval_cond result = result or eval_cond
return result return result
def get_plexpy_url(hostname=None):
if plexpy.CONFIG.ENABLE_HTTPS:
scheme = 'https'
else:
scheme = 'http'
if hostname is None and plexpy.CONFIG.HTTP_HOST == '0.0.0.0':
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.connect(('<broadcast>', 0))
hostname = s.getsockname()[0]
except socket.error:
hostname = socket.gethostbyname(socket.gethostname())
if not hostname:
hostname = 'localhost'
else:
hostname = hostname or plexpy.CONFIG.HTTP_HOST
if plexpy.CONFIG.HTTP_PORT not in (80, 443):
port = ':' + str(plexpy.CONFIG.HTTP_PORT)
else:
port = ''
if plexpy.CONFIG.HTTP_ROOT.strip('/'):
root = '/' + plexpy.CONFIG.HTTP_ROOT.strip('/')
else:
root = ''
return scheme + '://' + hostname + port + root

View File

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

View File

@@ -125,8 +125,8 @@ def update_section_ids():
library_children = pms_connect.get_library_children_details(section_id=section_id, library_children = pms_connect.get_library_children_details(section_id=section_id,
section_type=section_type) section_type=section_type)
if library_children: if library_children:
children_list = library_children['childern_list'] children_list = library_children['children_list']
key_mappings.update({child['rating_key']:child['section_id'] for child in children_list}) key_mappings.update({child['rating_key']: child['section_id'] for child in children_list})
else: else:
logger.warn(u"Tautulli Libraries :: Unable to get a list of library items for section_id %s." % section_id) logger.warn(u"Tautulli Libraries :: Unable to get a list of library items for section_id %s." % section_id)
@@ -198,7 +198,7 @@ def update_labels():
label_key=label['label_key']) label_key=label['label_key'])
if library_children: if library_children:
children_list = library_children['childern_list'] children_list = library_children['children_list']
# rating_key_list = [child['rating_key'] for child in children_list] # rating_key_list = [child['rating_key'] for child in children_list]
for rating_key in [child['rating_key'] for child in children_list]: for rating_key in [child['rating_key'] for child in children_list]:
@@ -456,7 +456,7 @@ class Libraries(object):
get_media_info=True) get_media_info=True)
if library_children: if library_children:
library_count = library_children['library_count'] library_count = library_children['library_count']
children_list = library_children['childern_list'] children_list = library_children['children_list']
else: else:
logger.warn(u"Tautulli Libraries :: Unable to get a list of library items.") logger.warn(u"Tautulli Libraries :: Unable to get a list of library items.")
return default_return return default_return
@@ -744,7 +744,7 @@ class Libraries(object):
logger.warn(u"Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh." logger.warn(u"Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh."
% section_id) % section_id)
# Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet # 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) library_details = get_library_details(section_id=section_id)

View File

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

View File

@@ -208,7 +208,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id) notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
custom_conditions_logic = notifier_config['custom_conditions_logic'] custom_conditions_logic = notifier_config['custom_conditions_logic']
custom_conditions = json.loads(notifier_config['custom_conditions']) or [] custom_conditions = notifier_config['custom_conditions']
if custom_conditions_logic or any(c for c in custom_conditions if c['value']): if custom_conditions_logic or any(c for c in custom_conditions if c['value']):
logger.debug(u"Tautulli NotificationHandler :: Checking custom notification conditions for notifier_id %s." logger.debug(u"Tautulli NotificationHandler :: Checking custom notification conditions for notifier_id %s."
@@ -507,9 +507,9 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
user_stream_count = len(user_sessions) user_stream_count = len(user_sessions)
# Generate a combined transcode decision value # Generate a combined transcode decision value
if session.get('video_decision','') == 'transcode' or session.get('audio_decision','') == 'transcode': if session.get('stream_video_decision', '') == 'transcode' or session.get('stream_audio_decision', '') == 'transcode':
transcode_decision = 'Transcode' transcode_decision = 'Transcode'
elif session.get('video_decision','') == 'copy' or session.get('audio_decision','') == 'copy': elif session.get('stream_video_decision', '') == 'copy' or session.get('stream_audio_decision', '') == 'copy':
transcode_decision = 'Direct Stream' transcode_decision = 'Direct Stream'
else: else:
transcode_decision = 'Direct Play' transcode_decision = 'Direct Play'
@@ -640,13 +640,17 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
album_name = '' album_name = ''
track_name = '' track_name = ''
num, num00 = format_group_index([helpers.cast_to_int(d['media_index']) child_num = [helpers.cast_to_int(
for d in child_metadata if d['parent_rating_key'] == rating_key]) d['media_index']) for d in child_metadata if d['parent_rating_key'] == rating_key]
num, num00 = format_group_index(child_num)
season_num, season_num00 = num, num00 season_num, season_num00 = num, num00
episode_num, episode_num00 = '', '' episode_num, episode_num00 = '', ''
track_num, track_num00 = '', '' track_num, track_num00 = '', ''
child_count = len(child_num)
grandchild_count = ''
elif ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT) elif ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT)
and notify_params['media_type'] in ('season', 'album')): and notify_params['media_type'] in ('season', 'album')):
show_name = notify_params['parent_title'] show_name = notify_params['parent_title']
@@ -654,14 +658,19 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
artist_name = notify_params['parent_title'] artist_name = notify_params['parent_title']
album_name = notify_params['title'] album_name = notify_params['title']
track_name = '' track_name = ''
season_num = str(notify_params['media_index']).zfill(1) season_num = str(notify_params['media_index']).zfill(1)
season_num00 = str(notify_params['media_index']).zfill(2) season_num00 = str(notify_params['media_index']).zfill(2)
num, num00 = format_group_index([helpers.cast_to_int(d['media_index']) grandchild_num = [helpers.cast_to_int(
for d in child_metadata if d['parent_rating_key'] == rating_key]) d['media_index']) for d in child_metadata if d['parent_rating_key'] == rating_key]
num, num00 = format_group_index(grandchild_num)
episode_num, episode_num00 = num, num00 episode_num, episode_num00 = num, num00
track_num, track_num00 = num, num00 track_num, track_num00 = num, num00
child_count = 1
grandchild_count = len(grandchild_num)
else: else:
show_name = notify_params['grandparent_title'] show_name = notify_params['grandparent_title']
episode_name = notify_params['title'] episode_name = notify_params['title']
@@ -674,10 +683,12 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
episode_num00 = str(notify_params['media_index']).zfill(2) episode_num00 = str(notify_params['media_index']).zfill(2)
track_num = str(notify_params['media_index']).zfill(1) track_num = str(notify_params['media_index']).zfill(1)
track_num00 = str(notify_params['media_index']).zfill(2) track_num00 = str(notify_params['media_index']).zfill(2)
child_count = 1
grandchild_count = 1
available_params = { available_params = {
# Global paramaters # Global paramaters
'tautulli_version': common.VERSION_NUMBER, 'tautulli_version': common.RELEASE,
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE, 'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH, 'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
'tautulli_commit': plexpy.CURRENT_VERSION, 'tautulli_commit': plexpy.CURRENT_VERSION,
@@ -783,6 +794,10 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'episode_num00': episode_num00, 'episode_num00': episode_num00,
'track_num': track_num, 'track_num': track_num,
'track_num00': track_num00, 'track_num00': track_num00,
'season_count': child_count,
'episode_count': grandchild_count,
'album_count': child_count,
'track_count': grandchild_count,
'year': notify_params['year'], 'year': notify_params['year'],
'release_date': arrow.get(notify_params['originally_available_at']).format(date_format) 'release_date': arrow.get(notify_params['originally_available_at']).format(date_format)
if notify_params['originally_available_at'] else '', if notify_params['originally_available_at'] else '',
@@ -877,7 +892,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
available_params = { available_params = {
# Global paramaters # Global paramaters
'tautulli_version': common.VERSION_NUMBER, 'tautulli_version': common.RELEASE,
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE, 'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH, 'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
'tautulli_commit': plexpy.CURRENT_VERSION, 'tautulli_commit': plexpy.CURRENT_VERSION,

View File

@@ -54,6 +54,7 @@ import twitter
import pynma import pynma
import plexpy import plexpy
import common
import database import database
import helpers import helpers
import logger import logger
@@ -94,6 +95,8 @@ AGENT_IDS = {'growl': 0,
'zapier': 24 'zapier': 24
} }
DEFAULT_CUSTOM_CONDITIONS = [{'parameter': '', 'operator': '', 'value': ''}]
def available_notification_agents(): def available_notification_agents():
agents = [{'label': 'Tautulli Remote Android App', agents = [{'label': 'Tautulli Remote Android App',
@@ -140,6 +143,10 @@ def available_notification_agents():
'name': 'join', 'name': 'join',
'id': AGENT_IDS['join'] 'id': AGENT_IDS['join']
}, },
{'label': 'Kodi',
'name': 'xbmc',
'id': AGENT_IDS['xbmc']
},
{'label': 'Notify My Android', {'label': 'Notify My Android',
'name': 'nma', 'name': 'nma',
'id': AGENT_IDS['nma'] 'id': AGENT_IDS['nma']
@@ -156,10 +163,10 @@ def available_notification_agents():
'name': 'prowl', 'name': 'prowl',
'id': AGENT_IDS['prowl'] 'id': AGENT_IDS['prowl']
}, },
{'label': 'Pushalot', # {'label': 'Pushalot',
'name': 'pushalot', # 'name': 'pushalot',
'id': AGENT_IDS['pushalot'] # 'id': AGENT_IDS['pushalot']
}, # },
{'label': 'Pushbullet', {'label': 'Pushbullet',
'name': 'pushbullet', 'name': 'pushbullet',
'id': AGENT_IDS['pushbullet'] 'id': AGENT_IDS['pushbullet']
@@ -184,10 +191,6 @@ def available_notification_agents():
'name': 'twitter', 'name': 'twitter',
'id': AGENT_IDS['twitter'] 'id': AGENT_IDS['twitter']
}, },
{'label': 'XBMC',
'name': 'xbmc',
'id': AGENT_IDS['xbmc']
},
{'label': 'Zapier', {'label': 'Zapier',
'name': 'zapier', 'name': 'zapier',
'id': AGENT_IDS['zapier'] 'id': AGENT_IDS['zapier']
@@ -196,7 +199,7 @@ def available_notification_agents():
# OSX Notifications should only be visible if it can be used # OSX Notifications should only be visible if it can be used
if OSX().validate(): if OSX().validate():
agents.append({'label': 'OSX Notify', agents.append({'label': 'macOS Notification Center',
'name': 'osx', 'name': 'osx',
'id': AGENT_IDS['osx'] 'id': AGENT_IDS['osx']
}) })
@@ -446,7 +449,6 @@ def get_notifier_config(notifier_id=None):
db = database.MonitorDatabase() db = database.MonitorDatabase()
result = db.select_single('SELECT * FROM notifiers WHERE id = ?', result = db.select_single('SELECT * FROM notifiers WHERE id = ?',
args=[notifier_id]) args=[notifier_id])
if not result: if not result:
return None return None
@@ -468,6 +470,14 @@ def get_notifier_config(notifier_id=None):
notifier_text[k] = {'subject': result.pop(k + '_subject'), notifier_text[k] = {'subject': result.pop(k + '_subject'),
'body': result.pop(k + '_body')} 'body': result.pop(k + '_body')}
try:
result['custom_conditions'] = json.loads(result['custom_conditions'])
except (ValueError, TypeError):
result['custom_conditions'] = DEFAULT_CUSTOM_CONDITIONS
if not result['custom_conditions_logic']:
result['custom_conditions_logic'] = ''
result['config'] = config result['config'] = config
result['config_options'] = notifier_config result['config_options'] = notifier_config
result['actions'] = notifier_actions result['actions'] = notifier_actions
@@ -494,7 +504,9 @@ def add_notifier_config(agent_id=None, **kwargs):
'agent_name': agent['name'], 'agent_name': agent['name'],
'agent_label': agent['label'], 'agent_label': agent['label'],
'friendly_name': '', 'friendly_name': '',
'notifier_config': json.dumps(get_agent_class(agent_id=agent['id']).config) 'notifier_config': json.dumps(get_agent_class(agent_id=agent['id']).config),
'custom_conditions': json.dumps(DEFAULT_CUSTOM_CONDITIONS),
'custom_conditions_logic': ''
} }
if agent['name'] == 'scripts': if agent['name'] == 'scripts':
for a in available_notification_actions(): for a in available_notification_actions():
@@ -549,7 +561,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
'agent_label': agent['label'], 'agent_label': agent['label'],
'friendly_name': kwargs.get('friendly_name', ''), 'friendly_name': kwargs.get('friendly_name', ''),
'notifier_config': json.dumps(notifier_config), 'notifier_config': json.dumps(notifier_config),
'custom_conditions': kwargs.get('custom_conditions', ''), 'custom_conditions': kwargs.get('custom_conditions', json.dumps(DEFAULT_CUSTOM_CONDITIONS)),
'custom_conditions_logic': kwargs.get('custom_conditions_logic', ''), 'custom_conditions_logic': kwargs.get('custom_conditions_logic', ''),
} }
values.update(actions) values.update(actions)
@@ -718,6 +730,13 @@ class PrettyMetadata(object):
def get_plex_url(self): def get_plex_url(self):
return self.parameters['plex_url'] return self.parameters['plex_url']
@staticmethod
def get_parameters():
parameters = {param['value']: param['name']
for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']}
parameters[''] = ''
return parameters
class Notifier(object): class Notifier(object):
NAME = '' NAME = ''
@@ -896,11 +915,9 @@ class ANDROIDAPP(Notifier):
'The content of your notifications will be sent unencrypted!</strong><br>' 'The content of your notifications will be sent unencrypted!</strong><br>'
'Please install the library to encrypt the notification contents. ' 'Please install the library to encrypt the notification contents. '
'Instructions can be found in the ' 'Instructions can be found in the '
'<a href="' + helpers.anon_url( '<a href="https://github.com/%s/%s-Wiki/wiki/'
'https://github.com/%s/%s-Wiki/wiki/' 'Frequently-Asked-Questions#notifications-pycryptodome'
'Frequently-Asked-Questions#notifications-pycryptodome' '" target="_blank">FAQ</a>.' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO),
% (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO)) +
'" target="_blank">FAQ</a>.',
'input_type': 'help' 'input_type': 'help'
}) })
else: else:
@@ -1290,7 +1307,7 @@ class EMAIL(Notifier):
mailserver.ehlo() mailserver.ehlo()
if self.config['smtp_user']: if self.config['smtp_user']:
mailserver.login(self.config['smtp_user'], self.config['smtp_password']) mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password']))
mailserver.sendmail(self.config['from'], recipients, msg.as_string()) mailserver.sendmail(self.config['from'], recipients, msg.as_string())
mailserver.quit() mailserver.quit()
@@ -1422,7 +1439,7 @@ class FACEBOOK(Notifier):
plexpy.CONFIG.FACEBOOK_TOKEN = 'temp' plexpy.CONFIG.FACEBOOK_TOKEN = 'temp'
return facebook.auth_url(app_id=app_id, return facebook.auth_url(app_id=app_id,
canvas_url=redirect_uri + '/facebookStep2', canvas_url=redirect_uri,
perms=['user_managed_groups','publish_actions']) perms=['user_managed_groups','publish_actions'])
def _get_credentials(self, code=''): def _get_credentials(self, code=''):
@@ -1434,15 +1451,15 @@ class FACEBOOK(Notifier):
try: try:
# Request user access token # Request user access token
api = facebook.GraphAPI(version='2.5') api = facebook.GraphAPI(version='2.12')
response = api.get_access_token_from_code(code=code, response = api.get_access_token_from_code(code=code,
redirect_uri=redirect_uri + '/facebookStep2', redirect_uri=redirect_uri,
app_id=app_id, app_id=app_id,
app_secret=app_secret) app_secret=app_secret)
access_token = response['access_token'] access_token = response['access_token']
# Request extended user access token # Request extended user access token
api = facebook.GraphAPI(access_token=access_token, version='2.5') api = facebook.GraphAPI(access_token=access_token, version='2.12')
response = api.extend_access_token(app_id=app_id, response = api.extend_access_token(app_id=app_id,
app_secret=app_secret) app_secret=app_secret)
@@ -1460,7 +1477,7 @@ class FACEBOOK(Notifier):
def _post_facebook(self, **data): def _post_facebook(self, **data):
if self.config['group_id']: if self.config['group_id']:
api = facebook.GraphAPI(access_token=self.config['access_token'], version='2.5') api = facebook.GraphAPI(access_token=self.config['access_token'], version='2.12')
try: try:
api.put_object(parent_object=self.config['group_id'], connection_name='feed', **data) api.put_object(parent_object=self.config['group_id'], connection_name='feed', **data)
@@ -1500,25 +1517,11 @@ class FACEBOOK(Notifier):
return self._post_facebook(**data) return self._post_facebook(**data)
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Instructions', config_option = [{'label': 'OAuth Redirect URI',
'description': 'Step 1: Visit <a href="' + helpers.anon_url('https://developers.facebook.com/apps') + '" target="_blank">'
'Facebook Developers</a> to add a new app using <strong>basic setup</strong>.<br>'
'Step 2: Click <strong>Add Product</strong> on the left, then <strong>Get Started</strong>'
'for <strong>Facebook Login</strong>.<br>'
'Step 3: Fill in <strong>Valid OAuth redirect URIs</strong> with your Tautulli URL (e.g. http://localhost:8181).<br>'
'Step 4: Click <strong>App Review</strong> on the left and toggle "make public" to <strong>Yes</strong>.<br>'
'Step 5: Fill in the <strong>Tautulli URL</strong> below with the exact same URL from Step 3.<br>'
'Step 6: Fill in the <strong>App ID</strong> and <strong>App Secret</strong> below.<br>'
'Step 7: Click the <strong>Request Authorization</strong> button below to retrieve your access token.<br>'
'Step 8: Fill in your <strong>Access Token</strong> below if it is not filled in automatically.<br>'
'Step 9: Fill in your <strong>Group ID</strong> number below. It can be found in the URL of your group page.',
'input_type': 'help'
},
{'label': 'Tautulli URL',
'value': self.config['redirect_uri'], 'value': self.config['redirect_uri'],
'name': 'facebook_redirect_uri', 'name': 'facebook_redirect_uri',
'description': 'Your Tautulli URL. This will tell Facebook where to redirect you after authorization.\ 'description': 'Fill in this address for the "Valid OAuth redirect URIs" '
(e.g. http://localhost:8181)', 'in your Facebook App.',
'input_type': 'text' 'input_type': 'text'
}, },
{'label': 'Facebook App ID', {'label': 'Facebook App ID',
@@ -1535,14 +1538,15 @@ class FACEBOOK(Notifier):
}, },
{'label': 'Request Authorization', {'label': 'Request Authorization',
'value': 'Request Authorization', 'value': 'Request Authorization',
'name': 'facebook_facebookStep1', 'name': 'facebook_facebook_auth',
'description': 'Request Facebook authorization. (Ensure you allow the browser pop-up).', 'description': 'Request Facebook authorization. (Ensure you allow the browser pop-up).',
'input_type': 'button' 'input_type': 'button'
}, },
{'label': 'Facebook Access Token', {'label': 'Facebook Access Token',
'value': self.config['access_token'], 'value': self.config['access_token'],
'name': 'facebook_access_token', 'name': 'facebook_access_token',
'description': 'Your Facebook access token. Automatically filled in after requesting authorization.', 'description': 'Your Facebook access token. '
'Automatically filled in after requesting authorization.',
'input_type': 'text' 'input_type': 'text'
}, },
{'label': 'Facebook Group ID', {'label': 'Facebook Group ID',
@@ -1741,7 +1745,7 @@ class GROWL(Notifier):
config_option = [{'label': 'Growl Host', config_option = [{'label': 'Growl Host',
'value': self.config['host'], 'value': self.config['host'],
'name': 'growl_host', 'name': 'growl_host',
'description': 'Your Growl hostname.', 'description': 'Your Growl hostname or IP address.',
'input_type': 'text' 'input_type': 'text'
}, },
{'label': 'Growl Password', {'label': 'Growl Password',
@@ -1843,7 +1847,7 @@ class HIPCHAT(Notifier):
return self.make_request(self.config['hook'], headers=headers, json=data) return self.make_request(self.config['hook'], headers=headers, json=data)
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Hipchat Custom Integrations Full URL', config_option = [{'label': 'Hipchat Custom Integrations URL',
'value': self.config['hook'], 'value': self.config['hook'],
'name': 'hipchat_hook', 'name': 'hipchat_hook',
'description': 'Your Hipchat BYO integration URL. You can get a key from' 'description': 'Your Hipchat BYO integration URL. You can get a key from'
@@ -1932,7 +1936,8 @@ class IFTTT(Notifier):
""" """
NAME = 'IFTTT' NAME = 'IFTTT'
_DEFAULT_CONFIG = {'key': '', _DEFAULT_CONFIG = {'key': '',
'event': 'plexpy' 'event': 'tautulli',
'value3': '',
} }
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
@@ -1941,6 +1946,10 @@ class IFTTT(Notifier):
data = {'value1': subject.encode("utf-8"), data = {'value1': subject.encode("utf-8"),
'value2': body.encode("utf-8")} 'value2': body.encode("utf-8")}
if self.config['value3']:
pretty_metadata = PrettyMetadata(kwargs['parameters'])
data['value3'] = pretty_metadata.parameters.get(self.config['value3'], '')
headers = {'Content-type': 'application/json'} headers = {'Content-type': 'application/json'}
return self.make_request('https://maker.ifttt.com/trigger/{}/with/key/{}'.format(event, self.config['key']), return self.make_request('https://maker.ifttt.com/trigger/{}/with/key/{}'.format(event, self.config['key']),
@@ -1964,6 +1973,13 @@ class IFTTT(Notifier):
' as <span class="inline-pre">value1</span>' ' as <span class="inline-pre">value1</span>'
' and <span class="inline-pre">value2</span> respectively.', ' and <span class="inline-pre">value2</span> respectively.',
'input_type': 'text' 'input_type': 'text'
},
{'label': 'Value 3',
'value': self.config['value3'],
'name': 'ifttt_value3',
'description': 'Optional: Select a parameter to send as <span class="inline-pre">value3</span>.',
'input_type': 'select',
'select_options': PrettyMetadata().get_parameters()
} }
] ]
@@ -2135,7 +2151,7 @@ class MQTT(Notifier):
'protocol': 'MQTTv311', 'protocol': 'MQTTv311',
'username': '', 'username': '',
'password': '', 'password': '',
'clientid': 'plexpy', 'clientid': 'tautulli',
'topic': '', 'topic': '',
'qos': 1, 'qos': 1,
'retain': 0, 'retain': 0,
@@ -2286,9 +2302,9 @@ class NMA(Notifier):
class OSX(Notifier): class OSX(Notifier):
""" """
OSX notifications macOS notifications
""" """
NAME = 'OSX Notify' NAME = 'macOS'
_DEFAULT_CONFIG = {'notify_app': '/Applications/Tautulli' _DEFAULT_CONFIG = {'notify_app': '/Applications/Tautulli'
} }
@@ -2320,7 +2336,7 @@ class OSX(Notifier):
self.objc.classAddMethod(cls, SEL, new_IMP) self.objc.classAddMethod(cls, SEL, new_IMP)
def _swizzled_bundleIdentifier(self, original, swizzled): def _swizzled_bundleIdentifier(self, original, swizzled):
return 'ade.plexpy.osxnotify' return 'ade.tautulli.osxnotify'
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
@@ -2371,9 +2387,15 @@ class OSX(Notifier):
config_option = [{'label': 'Register Notify App', config_option = [{'label': 'Register Notify App',
'value': self.config['notify_app'], 'value': self.config['notify_app'],
'name': 'osx_notify_app', 'name': 'osx_notify_app',
'description': 'Enter the path/application name to be registered with the ' 'description': 'Enter the path/application name to be registered with the Notification Center. '
'Notification Center, default is /Applications/Tautulli.', 'Default is <span class="inline-pre">/Applications/Tautulli</span>.',
'input_type': 'text' 'input_type': 'text'
},
{'label': 'Register App',
'value': 'Register App',
'name': 'osx_notify_register',
'description': 'Register Tautulli with the Notification Center.',
'input_type': 'button'
} }
] ]
@@ -2454,7 +2476,7 @@ class PLEX(Notifier):
return True return True
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Plex Home Theater Host:Port', config_option = [{'label': 'Plex Home Theater Host Address',
'value': self.config['hosts'], 'value': self.config['hosts'],
'name': 'plex_hosts', 'name': 'plex_hosts',
'description': 'Host running Plex Home Theater (eg. http://localhost:3005). Separate multiple hosts with commas (,).', 'description': 'Host running Plex Home Theater (eg. http://localhost:3005). Separate multiple hosts with commas (,).',
@@ -2645,10 +2667,10 @@ class PUSHBULLET(Notifier):
return {'': ''} return {'': ''}
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Pushbullet API Key', config_option = [{'label': 'Pushbullet Access Token',
'value': self.config['api_key'], 'value': self.config['api_key'],
'name': 'pushbullet_api_key', 'name': 'pushbullet_api_key',
'description': 'Your Pushbullet API key.', 'description': 'Your Pushbullet access token.',
'input_type': 'text', 'input_type': 'text',
'refresh': True 'refresh': True
}, },
@@ -2691,8 +2713,10 @@ class PUSHOVER(Notifier):
_DEFAULT_CONFIG = {'api_token': '', _DEFAULT_CONFIG = {'api_token': '',
'key': '', 'key': '',
'html_support': 1, 'html_support': 1,
'priority': 0,
'sound': '', 'sound': '',
'priority': 0,
'retry': 30,
'expire': 3600,
'incl_url': 1, 'incl_url': 1,
'incl_subject': 1, 'incl_subject': 1,
'incl_poster': 0, 'incl_poster': 0,
@@ -2713,6 +2737,10 @@ class PUSHOVER(Notifier):
if self.config['incl_subject']: if self.config['incl_subject']:
data['title'] = subject.encode("utf-8") 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'} headers = {'Content-type': 'application/x-www-form-urlencoded'}
files = {} files = {}
@@ -2789,6 +2817,13 @@ class PUSHOVER(Notifier):
'description': 'Your Pushover user or group key.', 'description': 'Your Pushover user or group key.',
'input_type': 'text' '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', {'label': 'Priority',
'value': self.config['priority'], 'value': self.config['priority'],
'name': 'pushover_priority', 'name': 'pushover_priority',
@@ -2796,12 +2831,19 @@ class PUSHOVER(Notifier):
'input_type': 'select', 'input_type': 'select',
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2} 'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
}, },
{'label': 'Sound', {'label': 'Retry Interval',
'value': self.config['sound'], 'value': self.config['retry'],
'name': 'pushover_sound', 'name': 'pushover_retry',
'description': 'Set the notification sound. Leave blank for the default sound.', 'description': 'Set the interval in seconds to keep retrying the notification.<br>'
'input_type': 'select', 'Note: For priority 2 only. Minimum 30 seconds.',
'select_options': self.get_sounds() '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', {'label': 'Enable HTML Support',
'value': self.config['html_support'], 'value': self.config['html_support'],
@@ -2903,6 +2945,14 @@ class SCRIPTS(Notifier):
process.kill() process.kill()
self.script_killed = True self.script_killed = True
# Common environment variables
env = {'PLEX_URL': plexpy.CONFIG.PMS_URL,
'PLEX_TOKEN': plexpy.CONFIG.PMS_TOKEN,
'TAUTULLI_URL': helpers.get_plexpy_url(hostname='localhost'),
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY
}
env.update(os.environ)
self.script_killed = False self.script_killed = False
output = error = '' output = error = ''
try: try:
@@ -2910,7 +2960,8 @@ class SCRIPTS(Notifier):
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=self.config['script_folder']) cwd=self.config['script_folder'],
env=env)
if self.config['timeout'] > 0: if self.config['timeout'] > 0:
timer = threading.Timer(self.config['timeout'], kill_script, (process,)) timer = threading.Timer(self.config['timeout'], kill_script, (process,))
@@ -2918,11 +2969,13 @@ class SCRIPTS(Notifier):
timer = None timer = None
try: try:
if timer: timer.start() if timer:
timer.start()
output, error = process.communicate() output, error = process.communicate()
status = process.returncode status = process.returncode
finally: finally:
if timer: timer.cancel() if timer:
timer.cancel()
except OSError as e: except OSError as e:
logger.error(u"Tautulli Notifiers :: Failed to run script: %s" % e) logger.error(u"Tautulli Notifiers :: Failed to run script: %s" % e)
@@ -3254,10 +3307,17 @@ class TELEGRAM(Notifier):
if poster_content: if poster_content:
poster_filename = 'poster_{}.jpg'.format(pretty_metadata.parameters['rating_key']) poster_filename = 'poster_{}.jpg'.format(pretty_metadata.parameters['rating_key'])
files = {'photo': (poster_filename, poster_content, 'image/jpeg')} files = {'photo': (poster_filename, poster_content, 'image/jpeg')}
data['caption'] = text
return self.make_request('https://api.telegram.org/bot{}/sendPhoto'.format(self.config['bot_token']), if len(text) > 200:
data=data, files=files) data['disable_notification'] = True
else:
data['caption'] = text
r = self.make_request('https://api.telegram.org/bot{}/sendPhoto'.format(self.config['bot_token']),
data=data, files=files)
if not data.pop('disable_notification', None):
return r
data['text'] = text data['text'] = text
@@ -3364,16 +3424,7 @@ class TWITTER(Notifier):
return self._send_tweet(body, attachment=poster_url) return self._send_tweet(body, attachment=poster_url)
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Instructions', config_option = [{'label': 'Twitter Consumer Key',
'description': 'Step 1: Visit <a href="' + helpers.anon_url('https://apps.twitter.com') + '" target="_blank">'
'Twitter Apps</a> to <strong>Create New App</strong>. A vaild "Website" is not required.<br>'
'Step 2: Go to <strong>Keys and Access Tokens</strong> and click '
'<strong>Create my access token</strong>.<br>'
'Step 3: Fill in the <strong>Consumer Key</strong>, <strong>Consumer Secret</strong>, '
'<strong>Access Token</strong>, and <strong>Access Token Secret</strong> below.',
'input_type': 'help'
},
{'label': 'Twitter Consumer Key',
'value': self.config['consumer_key'], 'value': self.config['consumer_key'],
'name': 'twitter_consumer_key', 'name': 'twitter_consumer_key',
'description': 'Your Twitter consumer key.', 'description': 'Your Twitter consumer key.',
@@ -3417,9 +3468,9 @@ class TWITTER(Notifier):
class XBMC(Notifier): class XBMC(Notifier):
""" """
XBMC notifications Kodi notifications
""" """
NAME = 'XBMC' NAME = 'Kodi'
_DEFAULT_CONFIG = {'hosts': '', _DEFAULT_CONFIG = {'hosts': '',
'username': '', 'username': '',
'password': '', 'password': '',
@@ -3489,22 +3540,22 @@ class XBMC(Notifier):
return True return True
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'XBMC Host:Port', config_option = [{'label': 'Kodi Host Address',
'value': self.config['hosts'], 'value': self.config['hosts'],
'name': 'xbmc_hosts', 'name': 'xbmc_hosts',
'description': 'Host running XBMC (e.g. http://localhost:8080). Separate multiple hosts with commas (,).', 'description': 'Host running Kodi (e.g. http://localhost:8080). Separate multiple hosts with commas (,).',
'input_type': 'text' 'input_type': 'text'
}, },
{'label': 'XBMC Username', {'label': 'Kodi Username',
'value': self.config['username'], 'value': self.config['username'],
'name': 'xbmc_username', 'name': 'xbmc_username',
'description': 'Username of your XBMC client API (blank for none).', 'description': 'Username of your Kodi client API (blank for none).',
'input_type': 'text' 'input_type': 'text'
}, },
{'label': 'XBMC Password', {'label': 'Kodi Password',
'value': self.config['password'], 'value': self.config['password'],
'name': 'xbmc_password', 'name': 'xbmc_password',
'description': 'Password of your XBMC client API (blank for none).', 'description': 'Password of your Kodi client API (blank for none).',
'input_type': 'password' 'input_type': 'password'
}, },
{'label': 'Notification Duration', {'label': 'Notification Duration',

View File

@@ -29,7 +29,7 @@ import pmsconnect
import session import session
def get_server_resources(return_presence=False): def get_server_resources(return_presence=False, return_server=False, **kwargs):
if not return_presence: if not return_presence:
logger.info(u"Tautulli PlexTV :: Requesting resources for server...") logger.info(u"Tautulli PlexTV :: Requesting resources for server...")
@@ -42,9 +42,15 @@ def get_server_resources(return_presence=False):
'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE, 'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE,
'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD, 'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD,
'pms_url': plexpy.CONFIG.PMS_URL, 'pms_url': plexpy.CONFIG.PMS_URL,
'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL 'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL,
'pms_identifier': plexpy.CONFIG.PMS_IDENTIFIER
} }
if kwargs:
server.update(kwargs)
for k in ['pms_ssl', 'pms_is_remote', 'pms_is_cloud', 'pms_url_manual']:
server[k] = int(server[k])
if server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']: if server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']:
scheme = 'https' scheme = 'https'
else: else:
@@ -55,7 +61,7 @@ def get_server_resources(return_presence=False):
port=server['pms_port']) port=server['pms_port'])
plex_tv = PlexTV() plex_tv = PlexTV()
result = plex_tv.get_server_connections(pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER, result = plex_tv.get_server_connections(pms_identifier=server['pms_identifier'],
pms_ip=server['pms_ip'], pms_ip=server['pms_ip'],
pms_port=server['pms_port'], pms_port=server['pms_port'],
include_https=server['pms_ssl']) include_https=server['pms_ssl'])
@@ -103,6 +109,9 @@ def get_server_resources(return_presence=False):
server['pms_url'] = fallback_url server['pms_url'] = fallback_url
logger.info(u"Tautulli PlexTV :: Using user-defined URL.") logger.info(u"Tautulli PlexTV :: Using user-defined URL.")
if return_server:
return server
plexpy.CONFIG.process_kwargs(server) plexpy.CONFIG.process_kwargs(server)
plexpy.CONFIG.write() plexpy.CONFIG.write()
@@ -645,6 +654,7 @@ class PlexTV(object):
'label': helpers.get_xml_attr(d, 'name'), 'label': helpers.get_xml_attr(d, 'name'),
'ip': helpers.get_xml_attr(c, 'address'), 'ip': helpers.get_xml_attr(c, 'address'),
'port': helpers.get_xml_attr(c, 'port'), 'port': helpers.get_xml_attr(c, 'port'),
'uri': helpers.get_xml_attr(c, 'uri'),
'local': helpers.get_xml_attr(c, 'local'), 'local': helpers.get_xml_attr(c, 'local'),
'value': helpers.get_xml_attr(c, 'address'), 'value': helpers.get_xml_attr(c, 'address'),
'is_cloud': is_cloud 'is_cloud': is_cloud

View File

@@ -666,6 +666,11 @@ class PmsConnect(object):
} }
elif metadata_type == 'show': 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, metadata = {'media_type': metadata_type,
'section_id': section_id, 'section_id': section_id,
'library_name': library_name, 'library_name': library_name,
@@ -685,7 +690,7 @@ class PmsConnect(object):
'rating': helpers.get_xml_attr(metadata_main, 'rating'), 'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'), 'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'), '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'), 'year': helpers.get_xml_attr(metadata_main, 'year'),
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'), 'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'), 'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
@@ -1091,7 +1096,9 @@ class PmsConnect(object):
'genres': genres, 'genres': genres,
'labels': labels, 'labels': labels,
'collections': collections, 'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title') 'full_title': helpers.get_xml_attr(metadata_main, 'title'),
'extra_type': helpers.get_xml_attr(metadata_main, 'extraType'),
'sub_type': helpers.get_xml_attr(metadata_main, 'subtype')
} }
else: else:
@@ -1404,6 +1411,10 @@ class PmsConnect(object):
'location': 'wan' if player_details['local'] == '0' else 'lan' '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 # Get the transcode details
if session.getElementsByTagName('TranscodeSession'): if session.getElementsByTagName('TranscodeSession'):
transcode_info = session.getElementsByTagName('TranscodeSession')[0] transcode_info = session.getElementsByTagName('TranscodeSession')[0]
@@ -1461,18 +1472,13 @@ class PmsConnect(object):
transcode_details['transcode_hw_decoding'] = int(transcode_details['transcode_hw_decode'].lower() in common.HW_DECODERS) transcode_details['transcode_hw_decoding'] = int(transcode_details['transcode_hw_decode'].lower() in common.HW_DECODERS)
transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS) transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS)
# Generate a combined transcode decision value
if transcode_details['video_decision'] == 'transcode' or transcode_details['audio_decision'] == 'transcode':
transcode_decision = 'transcode'
elif transcode_details['video_decision'] == 'copy' or transcode_details['audio_decision'] == 'copy':
transcode_decision = 'copy'
else:
transcode_decision = 'direct play'
# Determine if a synced version is being played # Determine if a synced version is being played
sync_id = None sync_id = None
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \ if media_type not in ('photo', 'clip') \
and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play': and not session.getElementsByTagName('Session') \
and not session.getElementsByTagName('TranscodeSession') \
and helpers.get_xml_attr(session, 'ratingKey').isdigit() \
and plexpy.CONFIG.PMS_PLEXPASS:
plex_tv = plextv.PlexTV() plex_tv = plextv.PlexTV()
parent_rating_key = helpers.get_xml_attr(session, 'parentRatingKey') parent_rating_key = helpers.get_xml_attr(session, 'parentRatingKey')
grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey') grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey')
@@ -1578,6 +1584,14 @@ class PmsConnect(object):
'stream_subtitle_decision': '' 'stream_subtitle_decision': ''
} }
# Generate a combined transcode decision value
if video_details['stream_video_decision'] == 'transcode' or audio_details['stream_audio_decision'] == 'transcode':
transcode_decision = 'transcode'
elif video_details['stream_video_decision'] == 'copy' or audio_details['stream_audio_decision'] == 'copy':
transcode_decision = 'copy'
else:
transcode_decision = 'direct play'
# Get the bif thumbnail # Get the bif thumbnail
indexes = helpers.get_xml_attr(stream_media_parts_info, 'indexes') indexes = helpers.get_xml_attr(stream_media_parts_info, 'indexes')
view_offset = helpers.get_xml_attr(session, 'viewOffset') view_offset = helpers.get_xml_attr(session, 'viewOffset')
@@ -1675,7 +1689,9 @@ class PmsConnect(object):
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels), 'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'), 'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
'channel_title': helpers.get_xml_attr(session, 'sourceTitle'), 'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
'live': int(helpers.get_xml_attr(session, 'live') == '1') 'live': int(helpers.get_xml_attr(session, 'live') == '1'),
'extra_type': helpers.get_xml_attr(session, 'extraType'),
'sub_type': helpers.get_xml_attr(session, 'subtype')
} }
else: else:
channel_stream = 0 channel_stream = 0
@@ -2134,10 +2150,12 @@ class PmsConnect(object):
sort_type = '&type=10' sort_type = '&type=10'
elif section_type == 'photo': elif section_type == 'photo':
sort_type = '' sort_type = ''
elif section_type == 'photoAlbum': elif section_type == 'photo_album':
sort_type = '&type=14' sort_type = '&type=14'
elif section_type == 'picture': 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: else:
sort_type = '' sort_type = ''
@@ -2155,16 +2173,16 @@ class PmsConnect(object):
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_library_children_details: %s." % e) logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_library_children_details: %s." % e)
return [] return []
childern_list = [] children_list = []
for a in xml_head: for a in xml_head:
if a.getAttribute('size'): if a.getAttribute('size'):
if a.getAttribute('size') == '0': if a.getAttribute('size') == '0':
logger.debug(u"Tautulli Pmsconnect :: No library data.") logger.debug(u"Tautulli Pmsconnect :: No library data.")
childern_list = {'library_count': '0', children_list = {'library_count': '0',
'childern_list': [] 'children_list': []
} }
return childern_list return children_list
if rating_key: if rating_key:
library_count = helpers.get_xml_attr(xml_head[0], 'size') library_count = helpers.get_xml_attr(xml_head[0], 'size')
@@ -2222,10 +2240,10 @@ class PmsConnect(object):
} }
item_info.update(media_info) item_info.update(media_info)
childern_list.append(item_info) children_list.append(item_info)
output = {'library_count': library_count, output = {'library_count': library_count,
'childern_list': childern_list 'children_list': children_list
} }
return output return output
@@ -2280,12 +2298,12 @@ class PmsConnect(object):
library_stats.update(child_stats) library_stats.update(child_stats)
if section_type == 'photo': 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: if parent_list:
parent_stats = {'parent_count': parent_list['library_count']} parent_stats = {'parent_count': parent_list['library_count']}
library_stats.update(parent_stats) 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: if child_list:
child_stats = {'child_count': child_list['library_count']} child_stats = {'child_count': child_list['library_count']}
library_stats.update(child_stats) library_stats.update(child_stats)

View File

@@ -38,14 +38,14 @@ def get_session_user():
Returns the user_id for the current logged in session Returns the user_id for the current logged in session
""" """
_session = get_session_info() _session = get_session_info()
return _session['user'] if _session and _session['user'] else None return _session['user'] if _session['user_group'] == 'guest' and _session['user'] else None
def get_session_user_id(): def get_session_user_id():
""" """
Returns the user_id for the current logged in session Returns the user_id for the current logged in session
""" """
_session = get_session_info() _session = get_session_info()
return str(_session['user_id']) if _session and _session['user_id'] else None return str(_session['user_id']) if _session['user_group'] == 'guest' and _session['user_id'] else None
def get_session_shared_libraries(): def get_session_shared_libraries():
""" """
@@ -79,7 +79,7 @@ def get_session_library_filters_type(filters, media_type=None):
filters = filters.get('filter_tv', ()) filters = filters.get('filter_tv', ())
elif media_type == 'artist' or media_type == 'album' or media_type == 'track': elif media_type == 'artist' or media_type == 'album' or media_type == 'track':
filters = filters.get('filter_music', ()) 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', ()) filters = filters.get('filter_photos', ())
else: else:
filters = filters.get('filter_all', ()) filters = filters.get('filter_all', ())

View File

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

View File

@@ -20,9 +20,9 @@ import subprocess
import tarfile import tarfile
import plexpy import plexpy
import common
import logger import logger
import request import request
import version
def runGit(args): def runGit(args):
@@ -65,7 +65,7 @@ def runGit(args):
def getVersion(): def getVersion():
if version.PLEXPY_BRANCH.startswith('win32build'): if common.BRANCH.startswith('win32build'):
plexpy.INSTALL_TYPE = 'win' plexpy.INSTALL_TYPE = 'win'
# Don't have a way to update exe yet, but don't want to set VERSION to None # 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') version_file = os.path.join(plexpy.PROG_DIR, 'version.txt')
if not os.path.isfile(version_file): if not os.path.isfile(version_file):
return None, 'origin', 'master' return None, 'origin', common.BRANCH
with open(version_file, 'r') as f: with open(version_file, 'r') as f:
current_version = f.read().strip(' \n\r') current_version = f.read().strip(' \n\r')
if current_version: if current_version:
return current_version, plexpy.CONFIG.GIT_REMOTE, plexpy.CONFIG.GIT_BRANCH return current_version, 'origin', common.BRANCH
else: else:
return None, 'origin', 'master' return None, 'origin', common.BRANCH
def checkGithub(auto_update=False): def checkGithub(auto_update=False):
@@ -190,9 +190,9 @@ def checkGithub(auto_update=False):
if plexpy.CONFIG.GIT_BRANCH == 'master': if plexpy.CONFIG.GIT_BRANCH == 'master':
release = next((r for r in releases if not r['prerelease']), releases[0]) release = next((r for r in releases if not r['prerelease']), releases[0])
elif plexpy.CONFIG.GIT_BRANCH == 'beta': elif plexpy.CONFIG.GIT_BRANCH == 'beta':
release = next((r for r in releases if r['prerelease'] and '-beta' in r['tag_name']), releases[0]) release = next((r for r in releases if not r['tag_name'].endswith('-nightly')), releases[0])
elif plexpy.CONFIG.GIT_BRANCH == 'nightly': elif plexpy.CONFIG.GIT_BRANCH == 'nightly':
release = next((r for r in releases if r['prerelease'] and '-nightly' in r['tag_name']), releases[0]) release = next((r for r in releases), releases[0])
else: else:
release = releases[0] release = releases[0]
@@ -292,8 +292,8 @@ def update():
def checkout_git_branch(): def checkout_git_branch():
if plexpy.INSTALL_TYPE == 'git': if plexpy.INSTALL_TYPE == 'git':
output, err = runGit('fetch ' + plexpy.CONFIG.GIT_REMOTE) output, err = runGit('fetch %s' % plexpy.CONFIG.GIT_REMOTE)
output, err = runGit('checkout ' + plexpy.CONFIG.GIT_BRANCH) output, err = runGit('checkout %s' % plexpy.CONFIG.GIT_BRANCH)
if not output: if not output:
logger.error('Unable to change git branch.') logger.error('Unable to change git branch.')
@@ -304,6 +304,8 @@ def checkout_git_branch():
logger.error('Unable to checkout from git: ' + line) logger.error('Unable to checkout from git: ' + line)
logger.info('Output: ' + str(output)) logger.info('Output: ' + str(output))
output, err = runGit('pull %s %s' % (plexpy.CONFIG.GIT_REMOTE, plexpy.CONFIG.GIT_BRANCH))
def read_changelog(latest_only=False, since_prev_release=False): def read_changelog(latest_only=False, since_prev_release=False):
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md') changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')

View File

@@ -25,18 +25,27 @@ import plexpy
import activity_handler import activity_handler
import activity_pinger import activity_pinger
import activity_processor import activity_processor
import database
import logger import logger
name = 'websocket' name = 'websocket'
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
ws_reconnect = False ws_shutdown = False
def start_thread(): def start_thread():
# Check for any existing sessions on start up try:
activity_pinger.check_active_sessions(ws_request=True) # Check for any existing sessions on start up
activity_pinger.check_active_sessions(ws_request=True)
except Exception as e:
logger.error(u"Tautulli WebSocket :: Failed to check for active sessions: %s." % e)
logger.warn(u"Tautulli WebSocket :: Attempt to fix by flushing temporary sessions...")
database.delete_sessions()
# Start the websocket listener on it's own thread # Start the websocket listener on it's own thread
threading.Thread(target=run).start() thread = threading.Thread(target=run)
thread.daemon = True
thread.start()
def on_connect(): def on_connect():
@@ -65,8 +74,21 @@ def on_disconnect():
def reconnect(): def reconnect():
global ws_reconnect close()
ws_reconnect = True logger.info(u"Tautulli WebSocket :: Reconnecting websocket...")
start_thread()
def shutdown():
global ws_shutdown
ws_shutdown = True
close()
def close():
logger.info(u"Tautulli WebSocket :: Disconnecting websocket...")
plexpy.WEBSOCKET.close()
plexpy.WS_CONNECTED = False
def run(): def run():
@@ -88,8 +110,8 @@ def run():
else: else:
header = [] header = []
global ws_reconnect global ws_shutdown
ws_reconnect = False ws_shutdown = False
reconnects = 0 reconnects = 0
# Try an open the websocket connection # Try an open the websocket connection
@@ -106,23 +128,26 @@ def run():
logger.info(u"Tautulli WebSocket :: Connection attempt %s." % str(reconnects)) logger.info(u"Tautulli WebSocket :: Connection attempt %s." % str(reconnects))
try: try:
ws = create_connection(uri, header=header) plexpy.WEBSOCKET = create_connection(uri, header=header)
logger.info(u"Tautulli WebSocket :: Ready") logger.info(u"Tautulli WebSocket :: Ready")
plexpy.WS_CONNECTED = True plexpy.WS_CONNECTED = True
except (websocket.WebSocketException, IOError, Exception) as e: except (websocket.WebSocketException, IOError, Exception) as e:
logger.error(u"Tautulli WebSocket :: %s." % e) logger.error("Tautulli WebSocket :: %s." % e)
if plexpy.WS_CONNECTED: if plexpy.WS_CONNECTED:
on_connect() on_connect()
while plexpy.WS_CONNECTED: while plexpy.WS_CONNECTED:
try: try:
process(*receive(ws)) process(*receive(plexpy.WEBSOCKET))
# successfully received data, reset reconnects counter # successfully received data, reset reconnects counter
reconnects = 0 reconnects = 0
except websocket.WebSocketConnectionClosedException: except websocket.WebSocketConnectionClosedException:
if ws_shutdown:
break
if reconnects == 0: if reconnects == 0:
logger.warn(u"Tautulli WebSocket :: Connection has closed.") logger.warn(u"Tautulli WebSocket :: Connection has closed.")
@@ -136,31 +161,25 @@ def run():
logger.warn(u"Tautulli WebSocket :: Reconnection attempt %s." % str(reconnects)) logger.warn(u"Tautulli WebSocket :: Reconnection attempt %s." % str(reconnects))
try: try:
ws = create_connection(uri, header=header) plexpy.WEBSOCKET = create_connection(uri, header=header)
logger.info(u"Tautulli WebSocket :: Ready") logger.info(u"Tautulli WebSocket :: Ready")
plexpy.WS_CONNECTED = True plexpy.WS_CONNECTED = True
except (websocket.WebSocketException, IOError, Exception) as e: except (websocket.WebSocketException, IOError, Exception) as e:
logger.error(u"Tautulli WebSocket :: %s." % e) logger.error("Tautulli WebSocket :: %s." % e)
else: else:
ws.shutdown() close()
plexpy.WS_CONNECTED = False
break break
except (websocket.WebSocketException, Exception) as e: except (websocket.WebSocketException, Exception) as e:
logger.error(u"Tautulli WebSocket :: %s." % e) if ws_shutdown:
ws.shutdown() break
plexpy.WS_CONNECTED = False
logger.error("Tautulli WebSocket :: %s." % e)
close()
break break
# Check if we recieved a restart notification and close websocket connection cleanly if not plexpy.WS_CONNECTED and not ws_shutdown:
if ws_reconnect:
logger.info(u"Tautulli WebSocket :: Reconnecting websocket...")
ws.shutdown()
plexpy.WS_CONNECTED = False
start_thread()
if not plexpy.WS_CONNECTED and not ws_reconnect:
on_disconnect() on_disconnect()
logger.debug(u"Tautulli WebSocket :: Leaving thread.") logger.debug(u"Tautulli WebSocket :: Leaving thread.")

View File

@@ -106,10 +106,10 @@ def check_credentials(username, password, admin_login='0'):
if plexpy.CONFIG.HTTP_PASSWORD: if plexpy.CONFIG.HTTP_PASSWORD:
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \ if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD): username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
return True, 'admin' return True, 'tautulli admin'
elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \ elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD: username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
return True, 'admin' return True, 'tautulli admin'
if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS): if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS):
plex_login = user_login(username, password) plex_login = user_login(username, password)
@@ -215,12 +215,12 @@ class AuthController(object):
return return
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT) raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
def on_login(self, user_id, username, user_group): def on_login(self, username, user_id=None, user_group=None, success=0):
"""Called on successful login""" """Called on successful login"""
# Save login to the database # Save login to the database
ip_address = cherrypy.request.headers.get('X-Forwarded-For', cherrypy.request.headers.get('Remote-Addr')) ip_address = cherrypy.request.remote.ip
host = cherrypy.request.headers.get('Host', cherrypy.request.headers.get('Origin')) host = cherrypy.request.base
user_agent = cherrypy.request.headers.get('User-Agent') user_agent = cherrypy.request.headers.get('User-Agent')
Users().set_user_login(user_id=user_id, Users().set_user_login(user_id=user_id,
@@ -229,28 +229,15 @@ class AuthController(object):
ip_address=ip_address, ip_address=ip_address,
host=host, host=host,
user_agent=user_agent, user_agent=user_agent,
success=1) success=success)
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged into Tautulli." % (user_group.capitalize(), username)) if success == 1:
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged into Tautulli." % (user_group.capitalize(), username))
def on_logout(self, username, user_group): def on_logout(self, username, user_group):
"""Called on logout""" """Called on logout"""
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged out of Tautulli." % (user_group.capitalize(), username)) logger.debug(u"Tautulli WebAuth :: %s user '%s' logged out of Tautulli." % (user_group.capitalize(), username))
def on_login_failed(self, username):
"""Called on failed login"""
# Save login attempt to the database
ip_address = cherrypy.request.headers.get('X-Forwarded-For', cherrypy.request.headers.get('Remote-Addr'))
host = cherrypy.request.headers.get('Origin')
user_agent = cherrypy.request.headers.get('User-Agent')
Users().set_user_login(user=username,
ip_address=ip_address,
host=host,
user_agent=user_agent,
success=0)
def get_loginform(self): def get_loginform(self):
from plexpy.webserve import serve_template from plexpy.webserve import serve_template
return serve_template(templatename="login.html", title="Login") return serve_template(templatename="login.html", title="Login")
@@ -293,15 +280,16 @@ class AuthController(object):
valid_login, user_group = check_credentials(username, password, admin_login) valid_login, user_group = check_credentials(username, password, admin_login)
if valid_login: if valid_login:
if user_group == 'guest': if user_group == 'tautulli admin':
user_group = 'admin'
user_id = None
else:
if re.match(r"[^@]+@[^@]+\.[^@]+", username): if re.match(r"[^@]+@[^@]+\.[^@]+", username):
user_details = Users().get_details(email=username) user_details = Users().get_details(email=username)
else: else:
user_details = Users().get_details(user=username) user_details = Users().get_details(user=username)
user_id = user_details['user_id'] user_id = user_details['user_id']
else:
user_id = None
time_delta = timedelta(days=30) if remember_me == '1' else timedelta(minutes=60) time_delta = timedelta(days=30) if remember_me == '1' else timedelta(minutes=60)
expiry = datetime.utcnow() + time_delta expiry = datetime.utcnow() + time_delta
@@ -315,7 +303,10 @@ class AuthController(object):
jwt_token = jwt.encode(payload, plexpy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM) jwt_token = jwt.encode(payload, plexpy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM)
self.on_login(user_id, username, user_group) self.on_login(username=username,
user_id=user_id,
user_group=user_group,
success=1)
jwt_cookie = JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID jwt_cookie = JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID
cherrypy.response.cookie[jwt_cookie] = jwt_token cherrypy.response.cookie[jwt_cookie] = jwt_token
@@ -327,13 +318,13 @@ class AuthController(object):
return {'status': 'success', 'token': jwt_token.decode('utf-8'), 'uuid': plexpy.CONFIG.PMS_UUID} return {'status': 'success', 'token': jwt_token.decode('utf-8'), 'uuid': plexpy.CONFIG.PMS_UUID}
elif admin_login == '1': elif admin_login == '1':
self.on_login_failed(username) self.on_login(username=username)
logger.debug(u"Tautulli WebAuth :: Invalid admin login attempt from '%s'." % username) logger.debug(u"Tautulli WebAuth :: Invalid admin login attempt from '%s'." % username)
cherrypy.response.status = 401 cherrypy.response.status = 401
return error_message return error_message
else: else:
self.on_login_failed(username) self.on_login(username=username)
logger.debug(u"Tautulli WebAuth :: Invalid login attempt from '%s'." % username) logger.debug(u"Tautulli WebAuth :: Invalid login attempt from '%s'." % username)
cherrypy.response.status = 401 cherrypy.response.status = 401
return error_message return error_message

View File

@@ -27,6 +27,8 @@ from hashing_passwords import make_hash
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
from mako import exceptions from mako import exceptions
import websocket
import plexpy import plexpy
import activity_pinger import activity_pinger
import common import common
@@ -63,7 +65,7 @@ def serve_template(templatename, **kwargs):
http_root = plexpy.HTTP_ROOT http_root = plexpy.HTTP_ROOT
server_name = plexpy.CONFIG.PMS_NAME 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() _session = get_session_info()
@@ -171,6 +173,7 @@ class WebInterface(object):
"home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE, "home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE,
"home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT, "home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT,
"home_stats_recently_added_count": plexpy.CONFIG.HOME_STATS_RECENTLY_ADDED_COUNT, "home_stats_recently_added_count": plexpy.CONFIG.HOME_STATS_RECENTLY_ADDED_COUNT,
"home_refresh_interval": plexpy.CONFIG.HOME_REFRESH_INTERVAL,
"pms_name": plexpy.CONFIG.PMS_NAME, "pms_name": plexpy.CONFIG.PMS_NAME,
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD, "pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
"update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG "update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG
@@ -2293,7 +2296,7 @@ class WebInterface(object):
filtered = [] filtered = []
fa = filt.append fa = filt.append
if logfile == "plexpy_api": if logfile == "tautulli_api":
filename = logger.FILENAME_API filename = logger.FILENAME_API
elif logfile == "plex_websocket": elif logfile == "plex_websocket":
filename = logger.FILENAME_PLEX_WEBSOCKET filename = logger.FILENAME_PLEX_WEBSOCKET
@@ -2496,7 +2499,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
def delete_logs(self, logfile='', **kwargs): def delete_logs(self, logfile='', **kwargs):
if logfile == "plexpy_api": if logfile == "tautulli_api":
filename = logger.FILENAME_API filename = logger.FILENAME_API
elif logfile == "plex_websocket": elif logfile == "plex_websocket":
filename = logger.FILENAME_PLEX_WEBSOCKET filename = logger.FILENAME_PLEX_WEBSOCKET
@@ -2538,7 +2541,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
def logFile(self, logfile='', **kwargs): def logFile(self, logfile='', **kwargs):
if logfile == "plexpy_api": if logfile == "tautulli_api":
filename = logger.FILENAME_API filename = logger.FILENAME_API
elif logfile == "plex_websocket": elif logfile == "plex_websocket":
filename = logger.FILENAME_PLEX_WEBSOCKET filename = logger.FILENAME_PLEX_WEBSOCKET
@@ -2611,6 +2614,7 @@ class WebInterface(object):
"pms_ssl": plexpy.CONFIG.PMS_SSL, "pms_ssl": plexpy.CONFIG.PMS_SSL,
"pms_is_remote": plexpy.CONFIG.PMS_IS_REMOTE, "pms_is_remote": plexpy.CONFIG.PMS_IS_REMOTE,
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD, "pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
"pms_url": plexpy.CONFIG.PMS_URL,
"pms_url_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL), "pms_url_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL),
"pms_uuid": plexpy.CONFIG.PMS_UUID, "pms_uuid": plexpy.CONFIG.PMS_UUID,
"pms_web_url": plexpy.CONFIG.PMS_WEB_URL, "pms_web_url": plexpy.CONFIG.PMS_WEB_URL,
@@ -2639,6 +2643,7 @@ class WebInterface(object):
"home_sections": json.dumps(plexpy.CONFIG.HOME_SECTIONS), "home_sections": json.dumps(plexpy.CONFIG.HOME_SECTIONS),
"home_stats_cards": json.dumps(plexpy.CONFIG.HOME_STATS_CARDS), "home_stats_cards": json.dumps(plexpy.CONFIG.HOME_STATS_CARDS),
"home_library_cards": json.dumps(plexpy.CONFIG.HOME_LIBRARY_CARDS), "home_library_cards": json.dumps(plexpy.CONFIG.HOME_LIBRARY_CARDS),
"home_refresh_interval": plexpy.CONFIG.HOME_REFRESH_INTERVAL,
"buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD, "buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD,
"buffer_wait": plexpy.CONFIG.BUFFER_WAIT, "buffer_wait": plexpy.CONFIG.BUFFER_WAIT,
"group_history_tables": checked(plexpy.CONFIG.GROUP_HISTORY_TABLES), "group_history_tables": checked(plexpy.CONFIG.GROUP_HISTORY_TABLES),
@@ -2812,6 +2817,12 @@ class WebInterface(object):
return {'result': 'success', 'message': 'Settings saved.'} return {'result': 'success', 'message': 'Settings saved.'}
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
def get_server_resources(self, **kwargs):
return plextv.get_server_resources(return_server=True, **kwargs)
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@@ -3004,18 +3015,12 @@ class WebInterface(object):
def get_notifier_config_modal(self, notifier_id=None, **kwargs): def get_notifier_config_modal(self, notifier_id=None, **kwargs):
result = notifiers.get_notifier_config(notifier_id=notifier_id) result = notifiers.get_notifier_config(notifier_id=notifier_id)
if not result['custom_conditions']:
result['custom_conditions'] = json.dumps([{'parameter': '', 'operator': '', 'value': ''}])
if not result['custom_conditions_logic']:
result['custom_conditions_logic'] = ''
parameters = [ parameters = [
{'name': param['name'], 'type': param['type'], 'value': param['value']} {'name': param['name'], 'type': param['type'], 'value': param['value']}
for category in common.NOTIFICATION_PARAMETERS for param in category['parameters'] for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']
] ]
return serve_template(templatename="notifier_config.html", notifier=result, parameters=json.dumps(parameters)) return serve_template(templatename="notifier_config.html", notifier=result, parameters=parameters)
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@@ -3190,7 +3195,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
def facebookStep1(self, app_id='', app_secret='', redirect_uri='', **kwargs): def facebook_auth(self, app_id='', app_secret='', redirect_uri='', **kwargs):
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
facebook_notifier = notifiers.FACEBOOK() facebook_notifier = notifiers.FACEBOOK()
@@ -3205,7 +3210,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
def facebookStep2(self, code='', **kwargs): def facebook_redirect(self, code='', **kwargs):
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
facebook = notifiers.FACEBOOK() facebook = notifiers.FACEBOOK()
@@ -3461,7 +3466,8 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@addtoapi() @addtoapi()
def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, **kwargs): def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, manual=0,
get_url=False, test_websocket=False, **kwargs):
""" Get the PMS server identifier. """ Get the PMS server identifier.
``` ```
@@ -3474,7 +3480,8 @@ class WebInterface(object):
remote (int): 0 or 1 remote (int): 0 or 1
Returns: Returns:
string: The unique PMS identifier json:
{'identifier': '08u2phnlkdshf890bhdlksghnljsahgleikjfg9t'}
``` ```
""" """
# Attempt to get the pms_identifier from plex.tv if the server is published # Attempt to get the pms_identifier from plex.tv if the server is published
@@ -3505,11 +3512,38 @@ class WebInterface(object):
xml_head = request.getElementsByTagName('MediaContainer')[0] xml_head = request.getElementsByTagName('MediaContainer')[0]
identifier = xml_head.getAttribute('machineIdentifier') identifier = xml_head.getAttribute('machineIdentifier')
result = {'identifier': identifier}
if identifier: if identifier:
return identifier if get_url == 'true':
server = self.get_server_resources(pms_ip=hostname,
pms_port=port,
pms_ssl=ssl,
pms_is_remote=remote,
pms_url_manual=manual,
pms_identifier=identifier)
result['url'] = server['pms_url']
result['ws'] = None
if test_websocket == 'true':
# Quick test websocket connection
ws_url = result['url'].replace('http', 'ws', 1) + '/:/websockets/notifications'
header = ['X-Plex-Token: %s' % plexpy.CONFIG.PMS_TOKEN]
logger.debug("Testing websocket connection...")
try:
test_ws = websocket.create_connection(ws_url, header=header)
test_ws.close()
logger.debug("Websocket connection test successful.")
result['ws'] = True
except (websocket.WebSocketException, IOError, Exception) as e:
logger.error("Websocket connection test failed: %s" % e)
result['ws'] = False
return result
else: else:
logger.warn('Unable to retrieve the PMS identifier.') logger.warn('Unable to retrieve the PMS identifier.')
return None return result
@cherrypy.expose @cherrypy.expose
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@@ -3581,11 +3615,11 @@ class WebInterface(object):
} }
elif plexpy.COMMITS_BEHIND > 0 and plexpy.common.BRANCH in ('master', 'beta') and \ 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', return {'result': 'success',
'update': True, 'update': True,
'release': 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, 'latest_release': plexpy.LATEST_RELEASE,
'release_url': helpers.anon_url( 'release_url': helpers.anon_url(
'https://github.com/%s/%s/releases/tag/%s' 'https://github.com/%s/%s/releases/tag/%s'
@@ -3668,7 +3702,7 @@ class WebInterface(object):
latest_only = (latest_only == 'true') latest_only = (latest_only == 'true')
since_prev_release = (since_prev_release == '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 latest_only = True
since_prev_release = False since_prev_release = False
@@ -3684,6 +3718,9 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@requireAuth() @requireAuth()
def info(self, rating_key=None, source=None, query=None, **kwargs): def info(self, rating_key=None, source=None, query=None, **kwargs):
if rating_key and not str(rating_key).isdigit():
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
metadata = None metadata = None
config = { config = {
@@ -3912,7 +3949,7 @@ class WebInterface(object):
@addtoapi() @addtoapi()
def download_log(self, logfile='', **kwargs): def download_log(self, logfile='', **kwargs):
""" Download the Tautulli log file. """ """ Download the Tautulli log file. """
if logfile == "plexpy_api": if logfile == "tautulli_api":
filename = logger.FILENAME_API filename = logger.FILENAME_API
log = logger.logger_api log = logger.logger_api
elif logfile == "plex_websocket": elif logfile == "plex_websocket":
@@ -4706,6 +4743,7 @@ class WebInterface(object):
"quality_profile": "Original", "quality_profile": "Original",
"rating": "7.8", "rating": "7.8",
"rating_key": "153037", "rating_key": "153037",
"relay": 0,
"section_id": "2", "section_id": "2",
"session_id": "helf15l3rxgw01xxe0jf3l3d", "session_id": "helf15l3rxgw01xxe0jf3l3d",
"session_key": "27", "session_key": "27",
@@ -5112,10 +5150,10 @@ class WebInterface(object):
quote_list = ['To crush your enemies, see them driven before you, and to hear the lamentation of their women!', quote_list = ['To crush your enemies, see them driven before you, and to hear the lamentation of their women!',
'Your clothes, give them to me, now!', 'Your clothes, give them to me, now!',
'Do it!', 'Do it!',
'If it bleeds, we can kill it', 'If it bleeds, we can kill it.',
'See you at the party Richter!', 'See you at the party Richter!',
'Let off some steam, Bennett', 'Let off some steam, Bennett.',
'I\'ll be back', 'I\'ll be back.',
'Get to the chopper!', 'Get to the chopper!',
'Hasta La Vista, Baby!', 'Hasta La Vista, Baby!',
'It\'s not a tumor!', 'It\'s not a tumor!',
@@ -5136,7 +5174,7 @@ class WebInterface(object):
'What killed the dinosaurs? The Ice Age!', 'What killed the dinosaurs? The Ice Age!',
'That\'s for sleeping with my wife!', 'That\'s for sleeping with my wife!',
'Remember when I said I\'d kill you last... I lied!', 'Remember when I said I\'d kill you last... I lied!',
'You want to be a farmer? Here\'s a couple of acres', 'You want to be a farmer? Here\'s a couple of acres.',
'Now, this is the plan. Get your ass to Mars.', 'Now, this is the plan. Get your ass to Mars.',
'I just had a terrible thought... What if this is a dream?', 'I just had a terrible thought... What if this is a dream?',
'Well, listen to this one: Rubber baby buggy bumpers!', 'Well, listen to this one: Rubber baby buggy bumpers!',
@@ -5270,34 +5308,4 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
def get_plexpy_url(self, **kwargs): def get_plexpy_url(self, **kwargs):
if plexpy.CONFIG.ENABLE_HTTPS: return helpers.get_plexpy_url()
scheme = 'https'
else:
scheme = 'http'
# Have to return some hostname if socket fails even if 127.0.0.1 won't work
hostname = '127.0.0.1'
if plexpy.CONFIG.HTTP_HOST == '0.0.0.0':
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.connect(('<broadcast>', 0))
hostname = s.getsockname()[0]
except socket.error:
hostname = socket.gethostbyname(socket.gethostname())
else:
hostname = plexpy.CONFIG.HTTP_HOST
if plexpy.CONFIG.HTTP_PORT not in (80, 443):
port = ':' + str(plexpy.CONFIG.HTTP_PORT)
else:
port = ''
if plexpy.CONFIG.HTTP_ROOT.strip('/'):
root = '/' + plexpy.CONFIG.HTTP_ROOT.strip('/')
else:
root = ''
return scheme + '://' + hostname + port + root

View File

@@ -72,7 +72,7 @@ def initialize(options):
if plexpy.CONFIG.HTTP_PLEX_ADMIN: if plexpy.CONFIG.HTTP_PLEX_ADMIN:
login_allowed.append("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']: if options['http_basic_auth']:
auth_enabled = False auth_enabled = False
@@ -122,6 +122,7 @@ def initialize(options):
'/images': { '/images': {
'tools.staticdir.on': True, 'tools.staticdir.on': True,
'tools.staticdir.dir': "interfaces/default/images", 'tools.staticdir.dir': "interfaces/default/images",
'tools.staticdir.content_types': {'svg': 'image/svg+xml'},
'tools.caching.on': True, 'tools.caching.on': True,
'tools.caching.force': True, 'tools.caching.force': True,
'tools.caching.delay': 0, 'tools.caching.delay': 0,