Compare commits

...

92 Commits

Author SHA1 Message Date
JonnyWong16
21dec5feb3 v2.5.4 2020-07-31 14:24:08 -07:00
JonnyWong16
bee4106af0 Change direct stream icon 2020-07-31 14:23:01 -07:00
JonnyWong16
bbb6e46515 Replace sys.stderr with logger 2020-07-27 18:47:08 -07:00
JonnyWong16
570ebb4f73 Add plex_id to notification parameters 2020-07-27 18:38:04 -07:00
JonnyWong16
d93204af4e Lookup TVmaze using title 2020-07-27 18:30:00 -07:00
JonnyWong16
702f116db9 Lookup The Movie Database using title and year 2020-07-27 18:20:12 -07:00
JonnyWong16
0c8607b3ec Fix typo in QR modal 2020-07-25 13:28:39 -07:00
JonnyWong16
3a2cc6efc7 Trim address when generating the QR code 2020-07-25 13:28:39 -07:00
JonnyWong16
1b37ff1655 Mobile device registration temporarily assume device_id is onesignal_id 2020-07-25 13:28:34 -07:00
JonnyWong16
769934c8a5 Add server_id to Andoird App notification data 2020-07-25 13:21:52 -07:00
JonnyWong16
7f1a4ec34a Add return PMS name and server ID from device registration 2020-07-25 13:21:52 -07:00
JonnyWong16
27438f7915 Don't allow apikey when using an app 2020-07-25 13:21:51 -07:00
JonnyWong16
8651bef9c1 Mask onesignal_id from API logs 2020-07-25 13:21:51 -07:00
JonnyWong16
36324d10dc Add onesignal_id to register device API 2020-07-25 13:21:46 -07:00
JonnyWong16
0272c35047 Fix parsing request responst message 2020-07-25 11:59:24 -07:00
JonnyWong16
70c0f912e2 Add themoviedb rating image 2020-07-24 09:12:11 -07:00
JonnyWong16
59a6acc088 Fix encoding issue with websocket logging 2020-07-23 17:44:17 -07:00
JonnyWong16
10b0726727 Remove support for .exe from script notifications 2020-07-23 17:33:43 -07:00
JonnyWong16
8f1360d7c2 Check for valid script extension when using a prefix override
* Also removes php, ruby, and perl overrides
2020-07-23 17:33:36 -07:00
JonnyWong16
e0e5ac9ecc Check for a valid script and script extension 2020-07-22 18:55:14 -07:00
JonnyWong16
c814f219a2 Prevent simultaneous importing of database/config 2020-07-22 18:33:47 -07:00
JonnyWong16
9095fc0c7a Catch config.ParseError 2020-07-22 18:27:23 -07:00
JonnyWong16
a675202537 Browse path starting from from current value 2020-07-18 15:19:42 -07:00
JonnyWong16
b52ab4885b Add browser button for script folder 2020-07-18 12:13:42 -07:00
JonnyWong16
43e26c9b56 Add Plex logs folder to config not imported note 2020-07-16 19:38:20 -07:00
JonnyWong16
703a7feed2 Update help text for SSL certificates/key in PEM format 2020-07-16 19:29:11 -07:00
JonnyWong16
7b69ed4cec Add browse function to settings with a folder or file 2020-07-16 19:27:14 -07:00
JonnyWong16
fcca7f969e Add filter extension as data property 2020-07-16 19:26:24 -07:00
JonnyWong16
ec34ea2116 Trigger change and unbind after selecting in file browser 2020-07-16 19:05:18 -07:00
JonnyWong16
3dc36c3b92 Refactor browse path function 2020-07-16 18:19:43 -07:00
JonnyWong16
f0d4fd5523 Add placeholder text for database/config import 2020-07-16 18:19:20 -07:00
JonnyWong16
7fe6c72fe2 Do not import PMS logs folder from config 2020-07-16 18:01:47 -07:00
JonnyWong16
d216d0f27f Reword import help text 2020-07-16 00:02:13 -07:00
JonnyWong16
43a7758acd Cleanup database import modal 2020-07-15 23:53:01 -07:00
JonnyWong16
3043956dec Add config import to settings page 2020-07-15 23:51:48 -07:00
JonnyWong16
06665fdd06 Add fucntion to import a config file 2020-07-15 23:26:22 -07:00
JonnyWong16
beff5caaac Clean shutdown page 2020-07-15 22:53:46 -07:00
JonnyWong16
3859412b2c Fix database import API docs 2020-07-15 22:10:17 -07:00
JonnyWong16
f7ec476fc0 Remove more unused config keys 2020-07-15 21:25:34 -07:00
JonnyWong16
b97d32671d Remove unused library update functions 2020-07-15 21:21:17 -07:00
JonnyWong16
01c56ef280 Remove helper bool check in database import status 2020-07-15 21:06:28 -07:00
JonnyWong16
b9422312f3 Remove unused check recently added pinger 2020-07-15 21:05:49 -07:00
JonnyWong16
9a0f83c3e7 Remove old config updates 2020-07-15 21:04:30 -07:00
JonnyWong16
fbfedb2e62 Remove unused config keys 2020-07-15 21:04:22 -07:00
JonnyWong16
4f8a462041 Update chown instructions in systemd script 2020-07-13 19:08:20 -07:00
JonnyWong16
141d043a6a FreeBSD/FreeNAS python is python3 2020-07-13 19:08:19 -07:00
JonnyWong16
c1266fed12 Update API docs for database import 2020-07-13 19:08:14 -07:00
JonnyWong16
4a4be9798d Adjust user IP table column widths 2020-07-12 12:54:56 -07:00
JonnyWong16
172692ccca Fix user IP table showing first played instead of last played 2020-07-12 12:54:42 -07:00
JonnyWong16
50e7c0469f Merge pull request #1374 from dotsam/ip-first-streamed
Add first_seen to user IP Table
2020-07-12 12:41:46 -07:00
JonnyWong16
44f74e3590 Mask device token and device id from API logs 2020-07-12 10:35:52 -07:00
Sam Edwards
63656b73c2 Add first_seen to user ips and add title attr with full date/time 2020-07-11 15:23:26 -07:00
JonnyWong16
40ecf56904 Fix Cloudinary upload for non-ASCII characters on Python 2 2020-07-10 21:57:53 -07:00
JonnyWong16
b4a10adec2 Merge branch 'v2.5-monitor-remote-access' into nightly 2020-07-10 17:09:58 -07:00
JonnyWong16
1698622d63 v2.5.3 2020-07-10 17:07:18 -07:00
JonnyWong16
fa27271647 Change shebang on contrib scripts 2020-07-10 17:02:23 -07:00
JonnyWong16
d837811c68 Improve start script 2020-07-09 17:13:16 -07:00
JonnyWong16
ad195f0969 Fix deleteing more than 1000 history entries at the same time 2020-07-08 12:27:20 -07:00
JonnyWong16
4a8748e322 Live TV library not being recreated after server identifier is changed
* Fixes Tautulli/Tautulli-Issues#261
2020-07-07 18:14:00 -07:00
JonnyWong16
0f016c83ea Fix ipwhois data location for macOS package
* Fixes Tautulli/Tautulli-Issues#260
2020-07-07 17:25:46 -07:00
JonnyWong16
061ae44da4 Fix indentation in macOS postinstall script 2020-07-07 17:05:15 -07:00
JonnyWong16
a8b90bf100 Reduce macOS build requirement to pyobjc-framework-Cocoa 2020-07-07 17:05:10 -07:00
JonnyWong16
eb3cd49bc4 Add hidden import pkg_resources.py2_warn to macos.spec
* Fixes build on macOS 10.13 (High Sierra)
2020-07-06 20:57:37 -07:00
JonnyWong16
416d869288 Add python version to Google Analytics 2020-07-06 18:13:33 -07:00
JonnyWong16
a116c26c25 Run python scripts with the same sys.executable as Tautulli 2020-07-06 11:32:16 -07:00
JonnyWong16
cc4ec53dac Full path to python3 interpreter in FreeBSD startup script 2020-07-06 10:08:36 -07:00
JonnyWong16
63164c7ff5 Quote command in systemd script 2020-07-06 09:37:37 -07:00
JonnyWong16
9815c014e8 Add python interpreter to init-scripts 2020-07-06 09:30:35 -07:00
JonnyWong16
69675151bf Remove monitor remote access settings
* Tautulli/Tautulli-Issues#251
2020-07-05 20:40:44 -07:00
JonnyWong16
99e395ddfa Update scheduled tasks table
* Tautulli/Tautulli-Issues#251
2020-07-05 20:39:31 -07:00
JonnyWong16
7fe1e542df Remove check remote access scheduled task
* Tautulli/Tautulli-Issues#251
2020-07-05 20:38:54 -07:00
JonnyWong16
938134081b Add remote access monitoring using websockets
* Fixes Tautulli/Tautulli-Issues#251
2020-07-05 20:36:44 -07:00
JonnyWong16
3fd2234a92 Remove refresh reachability 2020-07-05 19:20:52 -07:00
JonnyWong16
41843dc573 Rename some column headers 2020-07-04 12:22:40 -07:00
JonnyWong16
cc6bd528a5 Add architecture to release assets 2020-07-04 11:28:22 -07:00
JonnyWong16
2625ef5fb9 Use Popen to restart on macOS 2020-07-03 19:48:27 -07:00
JonnyWong16
dbd2d28877 Set macOS menu bar icon thread to daemon 2020-07-03 19:47:57 -07:00
JonnyWong16
f70f814c70 Shutdown tray icons last 2020-07-03 19:47:11 -07:00
JonnyWong16
6710e42134 Hide macOS dock icon for pkg install 2020-07-03 19:46:27 -07:00
JonnyWong16
78c5b45e43 Also fix e562ec9 for Python 2 2020-07-03 11:24:47 -07:00
JonnyWong16
e562ec96fa Fix encoding when reading a newsletter file 2020-07-02 20:46:42 -07:00
JonnyWong16
9b5e01c319 Fix logger for email notification exception 2020-07-02 12:45:45 -07:00
JonnyWong16
0097532f4a Fix startup script 2020-07-02 12:33:32 -07:00
JonnyWong16
91935c9018 Add hidden import cheroot.ssl.builtin for pyinstaller 2020-07-02 09:20:58 -07:00
JonnyWong16
83df807f7e Fix typo in eb3db20 2020-07-02 09:15:13 -07:00
JonnyWong16
eb3db20340 Add hidden import chroot.ssl for pyinstaller 2020-07-02 09:11:15 -07:00
JonnyWong16
6dab6194ea Replace which with command -v in startup script 2020-07-01 22:44:05 -07:00
JonnyWong16
356f64cac0 v2.5.2 2020-07-01 19:49:44 -07:00
JonnyWong16
f77f289125 Move GitHub sponsor first 2020-07-01 15:53:08 -07:00
JonnyWong16
280257477a Revert "Change shebang to python3"
This reverts commit cd8a899521.
2020-07-01 14:59:18 -07:00
JonnyWong16
660141cb16 Try various python versions in startup script 2020-07-01 14:43:35 -07:00
JonnyWong16
cd8a899521 Change shebang to python3 2020-07-01 14:43:04 -07:00
52 changed files with 1016 additions and 1333 deletions

View File

@@ -53,7 +53,7 @@ jobs:
uses: joncloud/makensis-action@v1.2
with:
script-file: ./package/Tautulli.nsi
arguments: /DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }} /DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
arguments: /DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }} /DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
include-more-plugins: true
include-custom-plugins-path: package/nsis-plugins
@@ -61,7 +61,7 @@ jobs:
uses: actions/upload-artifact@v2
with:
name: Tautulli-windows-installer
path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
@@ -117,13 +117,13 @@ jobs:
- name: Create Installer
run: |
sudo pkgbuild --install-location /Applications --version ${{ steps.get_version.outputs.VERSION }} --component ./dist/Tautulli.app --scripts ./package/macos-scripts Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
sudo pkgbuild --install-location /Applications --version ${{ steps.get_version.outputs.VERSION }} --component ./dist/Tautulli.app --scripts ./package/macos-scripts Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
- name: Upload Installer
uses: actions/upload-artifact@v2
with:
name: Tautulli-macos-installer
path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
@@ -188,8 +188,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
asset_path: ./Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
asset_content_type: application/vnd.microsoft.portable-executable
- name: Upload MacOS Installer
@@ -199,6 +199,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
asset_path: ./Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
asset_content_type: application/vnd.apple.installer+xml

24
API.md
View File

@@ -2267,8 +2267,8 @@ Required parameters:
user_id (str): The id of the Plex user
Optional parameters:
order_column (str): "last_seen", "ip_address", "platform", "player",
"last_played", "play_count"
order_column (str): "last_seen", "first_seen", "ip_address", "platform",
"player", "last_played", "play_count"
order_dir (str): "desc" or "asc"
start (int): Row to start from, 0
length (int): Number of items to return, 25
@@ -2286,6 +2286,7 @@ Returns:
"ip_address": "xxx.xxx.xxx.xxx",
"last_played": "Game of Thrones - The Red Woman",
"last_seen": 1462591869,
"first_seen": 1583968210,
"live": 0,
"media_index": 1,
"media_type": "episode",
@@ -2560,7 +2561,9 @@ Import a Tautulli, PlexWatch, or Plexivity database into Tautulli.
```
Required parameters:
app (str): "tautulli" or "plexwatch" or "plexivity"
database_path (str): The full path to the plexwatch database file
database_file (file): The database file to import (multipart/form-data)
or
database_path (str): The full path to the database file to import
method (str): For Tautulli only, "merge" or "overwrite"
table_name (str): For PlexWatch or Plexivity only, "processed" or "grouped"
@@ -2572,7 +2575,10 @@ Optional parameters:
of seconds for a stream to import
Returns:
None
json:
{"result": "success",
"message": "Database import has started. Check the logs to monitor any problems."
}
```
@@ -2668,14 +2674,18 @@ Registers the Tautulli Android App for notifications.
```
Required parameters:
device_name (str): The device name of the Tautulli Android App
device_id (str): The OneSignal device id of the Tautulli Android App
device_id (str): The unique device identifier for the mobile device
device_name (str): The device name of the mobile device
Optional parameters:
friendly_name (str): A friendly name to identify the mobile device
onesignal_id (str): The OneSignal id for the mobile device
Returns:
None
json:
{"pms_name": "Winterfell-Server",
"server_id": "ds48g4r354a8v9byrrtr697g3g79w"
}
```

View File

@@ -1,40 +1,68 @@
# Changelog
## v2.5.2-beta (2020-06-27)
## v2.5.4 (2020-07-31)
* Monitoring:
* Change: Montitoring remote access changed to use websockets. Refer to Tautulli/Tautulli-Issues#251 for details.
* Notifications:
* Fix: Uploading images to Cloudinary failed for titles with non-ASCII characters on Python 2.
* New: Added plex_id notification parameter.
* Remove: Running .exe files directly using script notifications is no longer supported.
* Remove: php, perl, and ruby prefix overrides for script notifications is no longer supported.
* Change: Stricter checking of file extensions for script notifications.
* Change: Fallback to The Movie Database lookup using title and year.
* Change: Fallback to TVmaze lookup using title.
* UI:
* New: Added ability to import a previous Tautullli configuration file in the settings.
* New: Added a browse button for settings which require a folder or file input.
* New: Added first streamed column to user IP addresses table. (Thanks @dotsam)
* New: Added The Movie Database rating image to media page.
* Change: Different icon to represent direct stream in the history tables.
* API:
* New: Updated API docs for importing a database and configuration file.
## v2.5.3 (2020-07-10)
* History:
* Fix: Unable to delete more than 1000 history entries at the same time.
* Notifications:
* Change: Python script notifications to run using the same Python interpreter as Tautulli.
* Newsletters:
* Fix: Unable to view newsletters with special characters.
* Other:
* Fix: Login to Tautulli not working on Python 2.
* Fix: Tautulli failing to start after enabling HTTPS when installed using the Windows / macOS installers.
* Fix: Startup script not working on macOS.
* Fix: Unable to hide dock icon on macOS with the pkg install. Refer to the FAQ regarding the Python rocket dock icon.
* Change: Added path to Python interpreter in system startup (daemon) scripts.
* Change: Added Python version to Google analytics.
## v2.5.1-beta (2020-06-26)
## v2.5.2 (2020-07-01)
* Announcements:
* Tautulli now supports Python 3!
* Python 2 is still supported for the time being, but it is recommended to upgrade to Python 3.
* Notifications:
* Fix: Error uploading images to Cloudinary on Python 2.
* Fix: Testing browser notifications alert not disappearing.
* Change: Default recently added notification delay set to 300 seconds.
* UI:
* Fix: MacOS menu bar icon causing Tautulli to fail to start.
* New: Added platform icon for LG devices.
* Mobile App:
* Fix: Improved API security and validation when registering the Android app.
* Other:
* Fix: Error creating self-signed certificates on Python 3.
* Fix: Docker container not respecting the PUID and PGID environment variables.
* Fix: Tautulli login session cookie not set on the HTTP root path.
* Remove: Ability to login to Tautulli using a Plex username and password has been removed. Login using a Plex.tv account is only supported via OAuth.
## v2.5.0-beta (2020-05-31)
* Announcements:
* Tautulli now supports Python 3!
* Python 2 is still supported for the time being, but it is recommended to upgrade to Python 3.
* UI:
* Fix: Unable to login to Tautulli on Python 2.
* New: Windows and MacOS setting to enable Tautulli to start automatically when you login.
* New: Added menu bar icon for MacOS.
* New: Ability to import a Tautulli database in the settings.
* New: Added Tautulli news area on the settings page.
* New: Added platform icon for LG devices.
* Remove: Ability to login to Tautulli using a Plex username and password has been removed. Login using a Plex.tv account is only supported via OAuth.
* Mobile App:
* Fix: Improved API security and validation when registering the Android app.
* Docker:
* Fix: Docker container not respecting the PUID and PGID environment variables.
* Other:
* Fix: Error creating self-signed certificates on Python 3.
* Fix: Tautulli login session cookie not set on the HTTP root path.
* New: Windows and MacOS app installers to install Tautulli without needing Python installed.

View File

@@ -131,8 +131,7 @@ def main():
if args.daemon:
if sys.platform == 'win32':
sys.stderr.write(
"Daemonizing not supported under Windows, starting normally\n")
logger.warn("Daemonizing not supported under Windows, starting normally")
else:
plexpy.DAEMON = True
plexpy.QUIET = True
@@ -265,7 +264,10 @@ def main():
if plexpy.CONFIG.SYS_TRAY_ICON:
# MacOS menu bar icon must be run on the main thread and is blocking
# Start the rest of Tautulli on a new thread
threading.Thread(target=wait).start()
thread = threading.Thread(target=wait)
thread.daemon = True
thread.start()
plexpy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
plexpy.MAC_SYS_TRAY_ICON.start()
else:

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Display information
echo "This script will remove *.pyc files. These files are generated by Python, but they can cause conflicts after an upgrade. It's safe to remove them, because they will be regenerated."

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Parameter check
if [ -z "$1" ]; then

View File

@@ -5,7 +5,7 @@
<h4 class="modal-title">Import ${app} Database</h4>
</div>
<div class="modal-body" id="modal-text">
<form id="import_database" enctype="multipart/form-data" method="post" name="import_database">
<form id="import_database_form" enctype="multipart/form-data" method="post" name="import_database_form">
<input type="hidden" id="import_app" name="import_app" value="${app.lower()}" />
% if app in ('PlexWatch', 'Plexivity'):
<p class="help-block">
@@ -28,11 +28,11 @@
<span class="btn btn-form">Upload</span>
<input type="file" style="display: none;" id="import_database_file" name="import_database_file" required>
</label>
<input id="import_database_file_name" type="text" class="form-control" disabled>
<input id="import_database_file_name" type="text" class="form-control" placeholder="tautulli.db" disabled>
</div>
</div>
</div>
<p class="help-block">Upload the ${app} database you wish to import.</p>
<p class="help-block">Upload the ${app} database file you wish to import.</p>
</div>
<div class="form-group">
<label for="import_database_path">Option 2: Browse for a Database File</label>
@@ -40,13 +40,13 @@
<div class="col-xs-12">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="import_database_path_browse">Browse</button>
<button class="btn btn-form" type="button" id="import_database_path_browse" data-toggle="browse" data-description="Database File" data-filter=".db" data-target="#import_database_path">Browse</button>
</span>
<input type="text" class="form-control" id="import_database_path" name="import_database_path" value="" required disabled>
<input type="text" class="form-control" id="import_database_path" name="import_database_path" value="" placeholder="tautulli.db" required disabled>
</div>
</div>
</div>
<p class="help-block">Browse for the ${app} database you wish to import.</p>
<p class="help-block">Browse for the ${app} database file you wish to import.</p>
</div>
% if app == 'Tautulli':
<div class="form-group">
@@ -64,7 +64,6 @@
<li><strong>Merge</strong> will add all history and remove any duplicates from the imported database into the current database.</li>
<li><strong>Overwrite</strong> will replace all history in the current database with the imported database.</li>
</ul>
<p class="help-block">Note: Libraries, users, notification agents, newsletter agents, and registered mobile devices will also be imported</p>
</div>
<div class="checkbox">
<label>
@@ -72,6 +71,15 @@
</label>
<p class="help-block">Automatically create a backup of the current database before importing.</p>
</div>
<div class="form-group">
<label>Import Notes</label>
<p class="help-block">The following data will also be imported:</p>
<ul class="help-block" style="padding-inline-start: 15px;">
<li>Libraries and Users</li>
<li>Notification / Newsletter Agents</li>
<li>Registered Mobile Devices</li>
</ul>
</div>
% else:
<div class="form-group">
<label for="import_table_name">Table Name</label>
@@ -106,19 +114,9 @@
</div>
</div>
<script>
$('#import_database_path_browse').click(function () {
$('#browse-path-type').text('Databse File');
$('#browse-path-modal').modal('show');
browsePath(null, null, '.db');
});
$('#select-browse-file').click(function () {
$('#browse-path-modal').modal('hide');
$("#import_database_path").val($('#browse-path').val());
});
$('#import_database_file').change(function() {
$("#import_database_file").change(function() {
if ($(this)[0].files[0]) {
$('#import_database_file_name').val($(this)[0].files[0].name);
$("#import_database_file_name").val($(this)[0].files[0].name);
}
});
@@ -126,7 +124,7 @@
$(this).prop('disabled', true);
var app = $("#import_app").val();
var database_file = $('#import_database_file')[0].files[0];
var database_file = $("#import_database_file")[0].files[0];
var database_path = $("#import_database_path").val();
var method = $("#import_method").val();
var backup = $("#import_backup_db").is(':checked');

View File

@@ -230,20 +230,12 @@ ${next.modalIncludes()}
</div>
</div>
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
<li><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li>
<li class="active"><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li>
<li><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
</ul>
<div class="tab-content">
<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/join/tautulli')}" 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="github-donation" style="text-align: center">
<div role="tabpanel" class="tab-pane active" id="github-donation" style="text-align: center">
<p>
Click the button below to continue to GitHub.
</p>
@@ -251,6 +243,14 @@ ${next.modalIncludes()}
<i class="fa fa-heart fa-sm" style="color: #ea4aaa;"></i>&nbsp; Sponsor
</a>
</div>
<div role="tabpanel" class="tab-pane" 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/join/tautulli')}" target="_blank">
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
<p>
Click the button below to continue to PayPal.

View File

@@ -0,0 +1,138 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">${title}</h4>
</div>
<div class="modal-body" id="modal-text">
<form id="import_config_form" enctype="multipart/form-data" method="post" name="import_config_form">
<div class="form-group">
<label for="import_config_file">Option 1: Upload a Configuration File</label>
<div class="row">
<div class="col-xs-12">
<div class="input-group">
<label for="import_config_file" class="input-group-btn">
<span class="btn btn-form">Upload</span>
<input type="file" style="display: none;" id="import_config_file" name="import_config_file" required>
</label>
<input id="import_config_file_name" type="text" class="form-control" placeholder="config.ini" disabled>
</div>
</div>
</div>
<p class="help-block">Upload the Tautulli configuration file you wish to import.</p>
</div>
<div class="form-group">
<label for="import_config_path">Option 2: Browse for a Configuration File</label>
<div class="row">
<div class="col-xs-12">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="import_config_path_browse" data-toggle="browse" data-description="Configuration File" data-filter=".ini" data-target="#import_config_path">Browse</button>
</span>
<input type="text" class="form-control" id="import_config_path" name="import_config_path" value="" placeholder="config.ini" required disabled>
</div>
</div>
</div>
<p class="help-block">Browse for the Tautulli configuration file you wish to import.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="import_backup_config" id="import_backup_config" value="1" checked> Backup Current Configuration
</label>
<p class="help-block">Automatically create a backup of the current configuration before importing.</p>
</div>
<div class="form-group">
<label>Import Notes</label>
<p class="help-block">The following settings will <em>not</em> be imported:</p>
<ul class="help-block" style="padding-inline-start: 15px;">
<li>Git Path, Log / Backup / Cache Directory, Plex Logs Folder</li>
<li>Custom Newsletter Templates Folder, Newsletter Output Directory</li>
<li>HTTP Host / Port / Root / Username / Password</li>
<li>Enable HTTPS, HTTPS Certificate / Certificate Chain / Key</li>
</ul>
</div>
</form>
</div>
<div class="modal-footer">
<div>
<span id="status-message" style="padding-right: 25px;"></span>
<input type="button" id="import_config" class="btn btn-bright" value="Import">
</div>
</div>
</div>
</div>
<script>
$("#import_config_file").change(function() {
if ($(this)[0].files[0]) {
$("#import_config_file_name").val($(this)[0].files[0].name);
}
});
$("#import_config").click(function() {
$(this).prop('disabled', true);
var config_file = $("#import_config_file")[0].files[0];
var config_path = $("#import_config_path").val();
var backup = $("#import_backup_config").is(':checked');
var content_type;
var process_data;
var data;
if (config_file) {
content_type = false;
process_data = false;
data = new FormData();
data.append('config_file', config_file);
data.append('backup', backup);
} else {
content_type = 'application/x-www-form-urlencoded; charset=UTF-8';
process_data = true;
data = {
config_path: config_path,
backup: backup
}
}
if (config_file) {
$("#status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>&nbsp; Uploading config file...');
} else {
$("#status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>');
}
$.ajax({
url: 'import_config',
type: 'POST',
data: data,
cache: false,
async: true,
contentType: content_type,
processData: process_data,
success: function(data) {
var msg;
if (data.result === 'success') {
msg = "<i class='fa fa-check'></i>&nbsp; " + data.message;
window.location.href = 'restart_import_config';
} else {
msg = "<i class='fa fa-exclamation-triangle'></i>&nbsp; " + data.message;
}
$("#status-message").html(msg);
$("#import_config_file").val(null);
$("#import_config_file_name").val('');
$("#import_config_path").val('');
},
error: function (xhr) {
var msg = "<i class='fa fa-exclamation-triangle'></i>&nbsp; Error (" + xhr.status + "): ";
if (xhr.status === 413) {
msg += "file is too large to upload"
} else {
msg += 'try again'
}
$("#status-message").html(msg);
},
complete: function(xhr) {
$("#import_config").prop('disabled', false);
}
});
});
</script>

View File

@@ -4069,6 +4069,11 @@ a:hover .overlay-refresh-image:hover {
width: 62px !important;
background-image: url(../images/rating/imdb.svg);
}
.rating-themoviedb {
width: 72px !important;
background-image: url(../images/rating/themoviedb.svg);
background-size: auto 16px !important;
}
.rating-rottentomatos-ripe {
background-image: url(../images/rating/tomato-ripe.svg);
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190.24 81.52"><defs><linearGradient id="a" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset=".56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><g data-name="Layer 2"><path d="M105.67 36.06h66.9a17.67 17.67 0 0017.67-17.66A17.67 17.67 0 00172.57.73h-66.9A17.67 17.67 0 0088 18.4a17.67 17.67 0 0017.67 17.66zm-88 45h76.9a17.67 17.67 0 0017.67-17.66 17.67 17.67 0 00-17.67-17.67h-76.9A17.67 17.67 0 000 63.4a17.67 17.67 0 0017.67 17.66zm-7.26-45.64h7.8V6.92h10.1V0h-28v6.9h10.1zm28.1 0h7.8V8.25h.1l9 27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2 23.1h-.1L50.31 0h-11.8zm113.92 20.25a15.07 15.07 0 00-4.52-5.52 18.57 18.57 0 00-6.68-3.08 33.54 33.54 0 00-8.07-1h-11.7v35.4h12.75a24.58 24.58 0 007.55-1.15 19.34 19.34 0 006.35-3.32 16.27 16.27 0 004.37-5.5 16.91 16.91 0 001.63-7.58 18.5 18.5 0 00-1.68-8.25zM145 68.6a8.8 8.8 0 01-2.64 3.4 10.7 10.7 0 01-4 1.82 21.57 21.57 0 01-5 .55h-4.05v-21h4.6a17 17 0 014.67.63 11.66 11.66 0 013.88 1.87A9.14 9.14 0 01145 59a9.87 9.87 0 011 4.52 11.89 11.89 0 01-1 5.08zm44.63-.13a8 8 0 00-1.58-2.62 8.38 8.38 0 00-2.42-1.85 10.31 10.31 0 00-3.17-1v-.1a9.22 9.22 0 004.42-2.82 7.43 7.43 0 001.68-5 8.42 8.42 0 00-1.15-4.65 8.09 8.09 0 00-3-2.72 12.56 12.56 0 00-4.18-1.3 32.84 32.84 0 00-4.62-.33h-13.2v35.4h14.5a22.41 22.41 0 004.72-.5 13.53 13.53 0 004.28-1.65 9.42 9.42 0 003.1-3 8.52 8.52 0 001.2-4.68 9.39 9.39 0 00-.55-3.18zm-19.42-15.75h5.3a10 10 0 011.85.18 6.18 6.18 0 011.7.57 3.39 3.39 0 011.22 1.13 3.22 3.22 0 01.48 1.82 3.63 3.63 0 01-.43 1.8 3.4 3.4 0 01-1.12 1.2 4.92 4.92 0 01-1.58.65 7.51 7.51 0 01-1.77.2h-5.65zm11.72 20a3.9 3.9 0 01-1.22 1.3 4.64 4.64 0 01-1.68.7 8.18 8.18 0 01-1.82.2h-7v-8h5.9a15.35 15.35 0 012 .15 8.47 8.47 0 012.05.55 4 4 0 011.57 1.18 3.11 3.11 0 01.63 2 3.71 3.71 0 01-.43 1.92z" fill="url(#a)" data-name="Layer 1"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -275,6 +275,11 @@ DOCUMENTATION :: END
<span class="rating-image rating-imdb"><strong>${data['rating']}</strong></span>
</div>
% endif
% if data['rating_image'].startswith('themoviedb://'):
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
<span class="rating-image rating-themoviedb"><strong>${get_percent(data['rating'], 10)}%</strong></span>
</div>
% endif
% if data['audience_rating_image'].startswith('rottentomatoes://'):
<div class="critic-rating hidden-xs hidden-sm" title="${data['audience_rating']}">
<span class="rating-image rating-rottentomatos-${data['audience_rating_image'].rsplit('.')[-1]}"><strong>${get_percent(data['audience_rating'], 10)}%</strong></span>

View File

@@ -141,7 +141,7 @@ history_table_options = {
if (rowData['transcode_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'direct play') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
}

View File

@@ -83,7 +83,7 @@ history_table_modal_options = {
if (rowData['transcode_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'direct play') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
}

View File

@@ -1,3 +1,25 @@
var date_format = 'YYYY-MM-DD';
var time_format = 'hh:mm a';
$.ajax({
url: 'get_date_formats',
type: 'GET',
success: function(data) {
date_format = data.date_format;
time_format = data.time_format;
}
});
var seenRender = function (data, type, full) {
return moment(data, "X").fromNow();
};
var seenCreatedCell = function (td, cellData, rowData, row, col) {
if (cellData !== null) {
$(td).attr('title', moment(cellData, "X").format(date_format + ' ' + time_format));
}
};
user_ip_table_options = {
"destroy": true,
"language": {
@@ -21,16 +43,24 @@ user_ip_table_options = {
"columnDefs": [
{
"targets": [0],
"data":"last_seen",
"render": function ( data, type, full ) {
return moment(data, "X").fromNow();
},
"data": "last_seen",
"render": seenRender,
"createdCell": seenCreatedCell,
"searchable": false,
"width": "15%",
"width": "12%",
"className": "no-wrap"
},
{
"targets": [1],
"data": "first_seen",
"render": seenRender,
"createdCell": seenCreatedCell,
"searchable": false,
"width": "12%",
"className": "no-wrap"
},
{
"targets": [2],
"data": "ip_address",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
@@ -44,22 +74,22 @@ user_ip_table_options = {
$(td).html('n/a');
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap modal-control-ip"
},
{
"targets": [2],
"targets": [3],
"data": "platform",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap"
},
{
"targets": [3],
"targets": [4],
"data": "player",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
@@ -67,18 +97,18 @@ user_ip_table_options = {
if (rowData['transcode_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'direct play') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
}
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp;' + cellData + '</div></a></div>');
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap modal-control"
},
{
"targets": [4],
"targets": [5],
"data": "last_played",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
@@ -119,7 +149,7 @@ user_ip_table_options = {
"className": "datatable-wrap"
},
{
"targets": [5],
"targets": [6],
"data": "play_count",
"searchable": false,
"width": "10%",

View File

@@ -142,7 +142,7 @@ users_list_table_options = {
if (rowData['transcode_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'direct play') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
}

View File

@@ -38,7 +38,7 @@
<th align="left" id="count">Total Movies / TV Shows / Artists</th>
<th align="left" id="parent_count">Total Seasons / Albums</th>
<th align="left" id="child_count">Total Episodes / Tracks</th>
<th align="left" id="last_accessed">Last Accessed</th>
<th align="left" id="last_accessed">Last Streamed</th>
<th align="left" id="last_played">Last Played</th>
<th align="left" id="total_plays">Total Plays</th>
<th align="left" id="total_duration">Total Played Duration</th>

View File

@@ -33,7 +33,7 @@
<label for="friendly_name">OneSignal Device ID</label>
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" id="device_id" value="${device['device_id']}" size="30" readonly>
<input type="text" class="form-control" id="onesignal_id" value="${device['onesignal_id'] or ''}" size="30" readonly>
</div>
</div>
<p class="help-block">Your OneSignal device ID for notifications.</p>

View File

@@ -121,6 +121,7 @@ DOCUMENTATION :: END
});
$('#api_qr_address').change(function () {
this.value = $.trim(this.value);
var url = $(this).val();
checkQRAddress(url);

View File

@@ -49,7 +49,16 @@
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
% if notifier['agent_name'] == 'scripts' and item['name'] == 'scripts_script_folder':
<div class="input-group">
<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 ''}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="${item['name']}_browse" data-toggle="browse" data-filter=".folderonly" data-target="#${item['name']}">Browse</button>
</span>
</div>
% 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 ''}>
% endif
</div>
</div>
<p class="help-block">${item['description'] | n}</p>

View File

@@ -28,7 +28,7 @@ DOCUMENTATION :: END
</tr>
</thead>
<tbody>
% for job in common.SCHEDULER_LIST:
% for job, job_type in common.SCHEDULER_LIST.items():
% if job in scheduled_jobs:
<%
sched_job = plexpy.SCHED.get_job(job)
@@ -41,7 +41,7 @@ DOCUMENTATION :: END
<td>${helpers.format_timedelta_Hms(sched_job.next_run_time - now)}</td>
<td>${sched_job.next_run_time.astimezone(plexpy.SYS_TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')}</td>
</tr>
% elif job in ('Check for server response', 'Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
% elif job_type == 'websocket' and plexpy.WS_CONNECTED:
<tr>
% if job == 'Check for active sessions':
<td><a class="queue-modal-link" href="#" data-queue="active sessions">${job}</a></td>

View File

@@ -568,29 +568,44 @@
<div class="form-group advanced-setting">
<label for="https_cert">HTTPS Certificate</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="https_cert_browse" data-toggle="browse" data-filter=".pem" data-target="#https_cert">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">The location of the SSL certificate.</p>
<p class="help-block">The location of the SSL certificate in PEM format.</p>
</div>
<div class="form-group advanced-setting">
<label for="https_cert_chain">HTTPS Certificate Chain</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_cert_chain" name="https_cert_chain" value="${config['https_cert_chain']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control http-settings" id="https_cert_chain" name="https_cert_chain" value="${config['https_cert_chain']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="https_cert_chain_browse" data-toggle="browse" data-filter=".pem" data-target="#https_cert_chain">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">The location of the SSL certificate chain.</p>
<p class="help-block">The location of the SSL certificate chain in PEM format.</p>
</div>
<div class="form-group advanced-setting">
<label for="https_key">HTTPS Key</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="https_key_browse" data-toggle="browse" data-filter=".pem" data-target="#https_key">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">The location of the SSL key.</p>
<p class="help-block">The location of the SSL key in PEM format.</p>
</div>
</div>
@@ -775,7 +790,6 @@
<label>
<input type="checkbox" class="pms-settings" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
</label>
<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>
</div>
<div class="form-group advanced-setting">
@@ -801,10 +815,15 @@
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
<div class="form-group advanced-setting">
<label for="pms_logs_folder">Logs Folder</label>
<label for="pms_logs_folder">Plex Logs Folder</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="pms_logs_folder" name="pms_logs_folder" value="${config['pms_logs_folder']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^[^\~\%]" data-parsley-errors-container="#pms_logs_folder_error" data-parsley-error-message="Shortcuts are not recognized.">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control" id="pms_logs_folder" name="pms_logs_folder" value="${config['pms_logs_folder']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^[^\~\%]" data-parsley-errors-container="#pms_logs_folder_error" data-parsley-error-message="Shortcuts are not recognized.">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="pms_logs_folder_browse" data-toggle="browse" data-filter=".folderonly" data-target="#pms_logs_folder">Browse</button>
</span>
</div>
</div>
<div id="pms_logs_folder_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
@@ -832,7 +851,6 @@
<label>
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" ${config['monitor_pms_updates']}> Monitor Plex Updates
</label>
<span id="cloudMonitorUpdates" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
<p class="help-block">Enable to have Tautulli check if updates are available for the Plex Media Server.</p>
</div>
<div id="pms_update_options">
@@ -866,36 +884,6 @@
</p>
</div>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
</label>
<span id="cloudMonitorRemoteAccess" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
<span id="remoteAccessCheck" class="settings-warning"></span>
<p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
</div>
<div id="monitor_remote_access_options">
<div class="form-group advanced-setting">
<label for="remote_access_ping_interval">Remote Access Ping Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="remote_access_ping_interval" name="remote_access_ping_interval" value="${config['remote_access_ping_interval']}" size="5" data-parsley-min="60" data-parsley-trigger="change" data-parsley-errors-container="#remote_access_ping_interval_error" required>
</div>
<div id="remote_access_ping_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in seconds) Tautulli will ping the Plex Media Server for the remote access status. Minimum 60.</p>
</div>
<div class="form-group advanced-setting">
<label for="remote_access_ping_threshold">Remote Access Ping Threshold</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="remote_access_ping_threshold" name="remote_access_ping_threshold" value="${config['remote_access_ping_threshold']}" size="5" data-parsley-min="1" data-parsley-trigger="change" data-parsley-errors-container="#remote_access_ping_threshold_error" required>
</div>
<div id="remote_access_ping_threshold_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The number of consecutive remote access status failures to consider remote access as down. Minimum 1.</p>
</div>
</div>
<div class="form-group advanced-setting">
<label for="refresh_users_interval">Users List Refresh Interval</label>
@@ -1134,10 +1122,15 @@
</p>
</div>
<div class="form-group advanced-setting">
<label for="newsletter_dir">Custom Newsletter Templates Folder</label>
<label for="newsletter_custom_dir">Custom Newsletter Templates Folder</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="newsletter_custom_dir" name="newsletter_custom_dir" value="${config['newsletter_custom_dir']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control" id="newsletter_custom_dir" name="newsletter_custom_dir" value="${config['newsletter_custom_dir']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="newsletter_custom_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#newsletter_custom_dir">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">Optional: Enter the full path to your custom newsletter templates folder. Leave blank for default.</p>
@@ -1145,8 +1138,13 @@
<div class="form-group advanced-setting">
<label for="newsletter_dir">Newsletter Output Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="newsletter_dir" name="newsletter_dir" value="${config['newsletter_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control" id="newsletter_dir" name="newsletter_dir" value="${config['newsletter_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="newsletter_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#newsletter_dir" ${docker_setting}>Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">Enter the full path to where newsletter files will be saved.</p>
@@ -1334,14 +1332,32 @@
<div role="tabpanel" class="tab-pane" id="tabs-import_backups">
<div class="padded-header">
<h3>Database Import</h3>
<h3>Import</h3>
</div>
<p class="help-block">Click a button below to import an existing database from the selected app.</p>
<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="tautulli">Tautulli</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>
<div class="form-group">
<label for="database_import">Database Import</label>
<p class="help-block">Click a button below to import an existing database from the selected app.</p>
<div class="row">
<div class="col-md-9">
<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="tautulli">Tautulli</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>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="config_import">Configuration Import</label>
<p class="help-block">Click the button below to import a previous Tautulli configuration.</p>
<div class="row">
<div class="col-md-9">
<div class="btn-group">
<button class="btn btn-form toggle-config-import-modal" type="button" data-target="#config-import-modal" data-toggle="modal">Tautulli</button>
</div>
</div>
</div>
</div>
<div class="padded-header">
@@ -1379,8 +1395,13 @@
<div class="form-group">
<label for="log_dir">Log Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control directory-settings" id="log_dir" name="log_dir" value="${config['log_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control directory-settings" id="log_dir" name="log_dir" value="${config['log_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="log_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#log_dir" ${docker_setting}>Browse</button>
</span>
</div>
<div class="btn-group">
<button class="btn btn-form" type="button" id="clear_logs">Clear Logs</button>
</div>
@@ -1390,8 +1411,13 @@
<div class="form-group">
<label for="backup_dir">Backup Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control directory-settings" id="backup_dir" name="backup_dir" value="${config['backup_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control directory-settings" id="backup_dir" name="backup_dir" value="${config['backup_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="backup_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#backup_dir" ${docker_setting}>Browse</button>
</span>
</div>
<div class="btn-group">
<button class="btn btn-form" type="button" id="backup_config">Backup Config</button>
<button class="btn btn-form" type="button" id="backup_database">Backup Database</button>
@@ -1402,8 +1428,13 @@
<div class="form-group">
<label for="cache_dir">Cache Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control directory-settings" id="cache_dir" name="cache_dir" value="${config['cache_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control directory-settings" id="cache_dir" name="cache_dir" value="${config['cache_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="cache_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#cache_dir" ${docker_setting}>Browse</button>
</span>
</div>
<div class="btn-group">
<button class="btn btn-form" type="button" id="clear_cache">Clear All Cache</button>
<button class="btn btn-form" type="button" id="clear_image_cache">Clear Image Cache</button>
@@ -1532,6 +1563,7 @@
</div>
</div>
<div id="app-import-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="app-import-modal"></div>
<div id="config-import-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="config-import-modal"></div>
<div id="add-notifier-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="add-notifier-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
@@ -1886,7 +1918,7 @@ Rating: {rating}/10 --> Rating: /10
</p>
<p class="help-block" id="api_qr_private" style="display: none;">
Note: This is a private IP address. Tautulli will not be reachable outside of your home network.
Access Tautulli via an externally address or manually enter the address above to generate the QR code for remote access.
Access Tautulli via an external address or manually enter the address above to generate the QR code for remote access.
</p>
<p class="help-block" id="api_qr_https" style="display: none;">
Note: This URL is not secure. Requests between the app and the server will not be encrypted.
@@ -2040,6 +2072,22 @@ Rating: {rating}/10 --> Rating: /10
});
}
$("#browse-path-modal").on('hidden.bs.modal', function() {
$("#select-browse-file").unbind('click');
});
function openBrowsePath(key, path, filter_ext, file_description, select_target) {
$("#browse-path-type").text(file_description);
$("#browse-path-modal").modal('show');
$("#select-browse-file").click(function () {
$("#browse-path-modal").modal('hide');
$(select_target).val($("#browse-path").val()).change();
});
browsePath(key, path, filter_ext);
}
function browsePath(key, path, filter_ext) {
$("#browse-path-status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>');
getBrowsePath(key, path, filter_ext).then(function (data) {
@@ -2158,7 +2206,6 @@ $(document).ready(function() {
initConfigCheckbox('#https_create_cert');
initConfigCheckbox('#check_github');
initConfigCheckbox('#monitor_pms_updates');
initConfigCheckbox('#monitor_remote_access');
initConfigCheckbox('#newsletter_self_hosted');
$('#menu_link_shutdown').click(function() {
@@ -2404,7 +2451,6 @@ $(document).ready(function() {
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
$('#pms_url_manual').prop('checked', false);
$('#pms_url').val('Please verify your server above to retrieve the URL');
PMSCloudCheck();
},
onDropdownOpen: function() {
this.clear();
@@ -2435,38 +2481,6 @@ $(document).ready(function() {
}
getServerOptions();
function PMSCloudCheck() {
if ($('#pms_is_cloud').val() === "1") {
$('#pms_port').val(443).prop('readonly', true);
$('#pms_is_remote_checkbox').prop('checked', true).prop('disabled', true);
$('#pms_is_remote').val(1);
$('#pms_ssl_checkbox').prop('checked', true).prop('disabled', true);
$('#pms_ssl').val(1);
$('#pms_url_manual').prop('checked', false).prop('disabled', true);
$('#monitor_pms_updates').prop('checked', false).prop('disabled', true);
$('#pms_update_options').hide();
$('#monitor_remote_access').prop('checked', false).prop('disabled', true);
$('#cloudManualConnection').show();
$('#cloudMonitorUpdates').show();
$('#cloudMonitorRemoteAccess').show();
$('#remoteAccessCheck').hide();
} else {
$('#pms_port').prop('readonly', false);
$('#pms_is_remote_checkbox').prop('disabled', false);
$('#pms_is_remote').val($('#pms_is_remote_checkbox').is(':checked') ? 1 : 0);
$('#pms_ssl_checkbox').prop('disabled', false);
$('#pms_ssl').val($('#pms_ssl_checkbox').is(':checked') ? 1 : 0);
$('#pms_url_manual').prop('disabled', false);
$('#monitor_pms_updates').prop('disabled', false);
$('#monitor_remote_access').prop('disabled', false);
$('#cloudManualConnection').hide();
$('#cloudMonitorUpdates').hide();
$('#cloudMonitorRemoteAccess').hide();
remoteAccessEnabledCheck()
}
}
PMSCloudCheck();
function verifyServer(_callback) {
var pms_ip = $("#pms_ip").val();
var pms_port = $("#pms_port").val();
@@ -2579,25 +2593,22 @@ $(document).ready(function() {
});
});
// Load config import modal
$(".toggle-config-import-modal").click(function() {
$.ajax({
url: 'import_config_tool',
cache: false,
async: true,
complete: function(xhr, status) {
$("#config-import-modal").html(xhr.responseText);
}
});
});
pms_version = false;
pms_logs_debug = false;
pms_logs = false;
function remoteAccessEnabledCheck() {
$.ajax({
url: 'get_server_pref',
data: { pref: 'PublishServerOnPlexOnlineKey' },
async: true,
success: function(data) {
if (data === 'false' || data === '0') {
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
$("#monitor_remote_access").attr("checked", false).attr("disabled", true);
}
}
});
}
remoteAccessEnabledCheck();
// Sortable home_sections
function set_home_sections() {
var home_sections = [];
@@ -3050,6 +3061,14 @@ $(document).ready(function() {
tautulli_news.html('<p class="help-block"><i class="fa fa-exclamation-triangle"></i>&nbsp; Failed to retrieve news.</p>')
}
});
$("body").on('click', '[data-toggle=browse]', function () {
var filter = $(this).data('filter');
var target = $(this).data('target');
var path = $(target).val();
var description = $(this).data('description') || $("label[for='" + target.replace('#', '') + "']").text();
openBrowsePath(null, path, filter, description, target);
});
});
</script>
</%def>

View File

@@ -22,10 +22,10 @@
<div class="modal-body" id="modal-text">
<div align="center">
% if message == "Shutting Down":
<h3><i class="fa fa-refresh fa-spin"></i> Tautulli is ${message}.</h3>
<h3><i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is ${message.lower()}</h3>
<br />
% else:
<h3><i class="fa fa-refresh fa-spin"></i> Tautulli is ${message}.</h3>
<h3><i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is ${message.lower()}</h3>
<br />
<h4>Restart in <span class="countdown"></span></h4>
% endif

View File

@@ -284,7 +284,8 @@ DOCUMENTATION :: END
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
<thead>
<tr>
<th align="left" id="last_seen">Last Seen</th>
<th align="left" id="last_seen">Last Streamed</th>
<th align="left" id="first_seen">First Streamed</th>
<th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Last Platform</th>
<th align="left" id="player">Last Player</th>

View File

@@ -34,7 +34,7 @@
<th align="left" id="edit_row">Edit</th>
<th align="right" id="avatar"></th>
<th align="left" id="friendly_name">User</th>
<th align="left" id="last_seen">Last Seen</th>
<th align="left" id="last_seen">Last Streamed</th>
<th align="left" id="last_known_ip">Last Known IP</th>
<th align="left" id="last_platform">Last Platform</th>
<th align="left" id="last_player">Last Player</th>

View File

@@ -38,6 +38,7 @@ load_rc_config ${name}
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command_interpreter="python"
command="${tautulli_dir}/Tautulli.py"
command_args="--daemon --pidfile ${tautulli_pid} --quiet --nolaunch ${tautulli_flags}"

View File

@@ -28,14 +28,16 @@
# Ubuntu/Debian: sudo addgroup tautulli && sudo adduser --system --no-create-home tautulli --ingroup tautulli
# CentOS/Fedora: sudo adduser --system --no-create-home tautulli
# 2. Give the user ownership of the Tautulli directory:
# sudo chown tautulli:tautulli -R /opt/Tautulli
# sudo chown -R tautulli:tautulli /opt/Tautulli
#
# - Adjust ExecStart= to point to:
# 1. Your Tautulli executable
# 1. Your Python interpreter (get the path with "command -v python3")
# - Default: /usr/bin/python3
# 2. Your Tautulli executable
# - Default: /opt/Tautulli/Tautulli.py
# 2. Your config file (recommended is to put it somewhere in /etc)
# 3. Your config file (recommended is to put it somewhere in /etc)
# - Default: --config /opt/Tautulli/config.ini
# 3. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
# 4. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
# - Default: --datadir /opt/Tautulli
#
# - Adjust User= and Group= to the user/group you want Tautulli to run as.
@@ -50,7 +52,7 @@ Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch
ExecStart=/usr/bin/python3 /opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch
GuessMainPID=no
Type=forking
User=tautulli

View File

@@ -16,10 +16,10 @@ analysis = Analysis(
('../CHANGELOG.md', '.'),
('../LICENSE', '.'),
('../version.txt', '.'),
('../lib/ipwhois/data', 'data')
('../lib/ipwhois/data', 'ipwhois/data')
],
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
hiddenimports=['Foundation', 'AppKit'],
hiddenimports=['pkg_resources.py2_warn', 'Foundation', 'AppKit', 'cheroot.ssl', 'cheroot.ssl.builtin'],
cipher=block_cipher
)
pyz = PYZ(
@@ -47,5 +47,9 @@ app = BUNDLE(
name='Tautulli.app',
icon='../data/interfaces/default/images/logo-circle.icns',
bundle_identifier='com.Tautulli.Tautulli',
version=VERSION
version=VERSION,
info_plist={
'LSBackgroundOnly': True,
'LSUIElement': True
}
)

View File

@@ -16,6 +16,7 @@ analysis = Analysis(
('..\\lib\\ipwhois\\data', 'data')
],
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
hiddenimports=['cheroot.ssl', 'cheroot.ssl.builtin'],
cipher=block_cipher,
)
pyz = PYZ(

View File

@@ -3,7 +3,7 @@
dialogText=`osascript -e 'set dialogText to button returned of (display dialog "Installation complete. Start Tautulli?" buttons {"Start", "Close"})'`;
if [[ $dialogText == 'Start' ]]
then
open /Applications/Tautulli.app
open /Applications/Tautulli.app
else
exit 0;
fi

View File

@@ -1,4 +1,4 @@
pyinstaller
pyopenssl
pycryptodomex
pyobjc
pyobjc-framework-Cocoa

View File

@@ -37,8 +37,7 @@ from apscheduler.triggers.interval import IntervalTrigger
from UniversalAnalytics import Tracker
import pytz
PYTHON_VERSION = sys.version_info[:3]
PYTHON2 = PYTHON_VERSION[0] == 2
PYTHON2 = sys.version_info[0] == 2
if PYTHON2:
import activity_handler
@@ -137,6 +136,7 @@ DEV = False
WEBSOCKET = None
WS_CONNECTED = False
PLEX_SERVER_UP = None
PLEX_REMOTE_ACCESS_UP = None
TRACKER = None
@@ -160,7 +160,11 @@ def initialize(config_file):
global UMASK
global _UPDATE
CONFIG = config.Config(config_file)
try:
CONFIG = config.Config(config_file)
except:
raise SystemExit("Unable to initialize Tautulli due to a corrupted config file. Exiting...")
CONFIG_FILE = config_file
assert CONFIG is not None
@@ -444,10 +448,6 @@ def initialize_scheduler():
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
hours=12 * (not bool(CONFIG.PMS_URL_MANUAL)), minutes=0, seconds=0)
pms_remote_access_seconds = CONFIG.REMOTE_ACCESS_PING_INTERVAL if 60 <= CONFIG.REMOTE_ACCESS_PING_INTERVAL else 60
schedule_job(activity_pinger.check_server_access, 'Check for Plex remote access',
hours=0, minutes=0, seconds=pms_remote_access_seconds * bool(CONFIG.MONITOR_REMOTE_ACCESS))
schedule_job(activity_pinger.check_server_updates, 'Check for Plex updates',
hours=pms_update_check_hours * bool(CONFIG.MONITOR_PMS_UPDATES), minutes=0, seconds=0)
@@ -470,8 +470,6 @@ def initialize_scheduler():
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
hours=0, minutes=0, seconds=0)
schedule_job(activity_pinger.check_server_access, 'Check for Plex remote access',
hours=0, minutes=0, seconds=0)
schedule_job(activity_pinger.check_server_updates, 'Check for Plex updates',
hours=0, minutes=0, seconds=0)
@@ -745,7 +743,7 @@ def dbcheck():
c_db.execute(
'CREATE TABLE IF NOT EXISTS mobile_devices (id INTEGER PRIMARY KEY AUTOINCREMENT, '
'device_id TEXT NOT NULL UNIQUE, device_token TEXT, device_name TEXT, friendly_name TEXT, '
'last_seen INTEGER, official INTEGER DEFAULT 0)'
'onesignal_id TEXT, last_seen INTEGER, official INTEGER DEFAULT 0)'
)
# tvmaze_lookup table :: This table keeps record of the TVmaze lookups
@@ -2014,6 +2012,15 @@ def dbcheck():
c_db.execute('UPDATE mobile_devices SET official = ? WHERE device_id = ?',
[mobile_app.validate_device_id(device_id), device_id])
# Upgrade mobile_devices table from earlier versions
try:
c_db.execute('SELECT onesignal_id FROM mobile_devices')
except sqlite3.OperationalError:
logger.debug("Altering database. Updating database table mobile_devices.")
c_db.execute(
'ALTER TABLE mobile_devices ADD COLUMN onesignal_id TEXT'
)
# Upgrade notifiers table from earlier versions
try:
c_db.execute('SELECT custom_conditions FROM notifiers')
@@ -2162,10 +2169,7 @@ def dbcheck():
def upgrade():
if CONFIG.UPDATE_NOTIFIERS_DB:
notifiers.upgrade_config_to_db()
if CONFIG.UPDATE_LIBRARIES_DB_NOTIFY:
libraries.update_libraries_db_notify()
return
def shutdown(restart=False, update=False, checkout=False, reset=False):
@@ -2211,11 +2215,6 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
logger.info("Removing pidfile %s", PIDFILE)
os.remove(PIDFILE)
if WIN_SYS_TRAY_ICON:
WIN_SYS_TRAY_ICON.shutdown()
elif MAC_SYS_TRAY_ICON:
MAC_SYS_TRAY_ICON.shutdown()
if restart:
logger.info("Tautulli is restarting...")
@@ -2238,7 +2237,7 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
# https://bugs.python.org/issue19066
if NOFORK:
pass
elif common.PLATFORM == 'Windows':
elif common.PLATFORM in ('Windows', 'Darwin'):
subprocess.Popen(args, cwd=os.getcwd())
else:
os.execv(exe, args)
@@ -2248,6 +2247,11 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
logger.shutdown()
if WIN_SYS_TRAY_ICON:
WIN_SYS_TRAY_ICON.shutdown()
elif MAC_SYS_TRAY_ICON:
MAC_SYS_TRAY_ICON.shutdown()
os._exit(0)
@@ -2264,6 +2268,7 @@ def initialize_tracker():
'appInstallerId': CONFIG.GIT_BRANCH,
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform
'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro
'dimension3': common.PYTHON_VERSION,
'userLanguage': SYS_LANGUAGE,
'documentEncoding': SYS_ENCODING,
'noninteractive': True

View File

@@ -505,6 +505,55 @@ class TimelineHandler(object):
schedule_callback('rating_key-{}'.format(rating_key), remove_job=True)
class ReachabilityHandler(object):
def __init__(self, data):
self.data = data
def is_reachable(self):
if 'reachability' in self.data:
return self.data['reachability']
return False
def remote_access_enabled(self):
pms_connect = pmsconnect.PmsConnect()
pref = pms_connect.get_server_pref(pref='PublishServerOnPlexOnlineKey')
return helpers.bool_true(pref)
def process(self):
# Check if remote access is enabled
if not self.remote_access_enabled():
return
# Do nothing if remote access is still up and hasn't changed
if self.is_reachable() and plexpy.PLEX_REMOTE_ACCESS_UP:
return
pms_connect = pmsconnect.PmsConnect()
server_response = pms_connect.get_server_response()
if server_response:
# Waiting for port mapping
if server_response['mapping_state'] == 'waiting':
logger.warn("Tautulli Monitor :: Remote access waiting for port mapping.")
elif plexpy.PLEX_REMOTE_ACCESS_UP is not False and server_response['reason']:
logger.warn("Tautulli Monitor :: Remote access failed: %s" % server_response['reason'])
logger.info("Tautulli Monitor :: Plex remote access is down.")
plexpy.PLEX_REMOTE_ACCESS_UP = False
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extdown', 'remote_access_info': server_response})
elif plexpy.PLEX_REMOTE_ACCESS_UP is False and not server_response['reason']:
logger.info("Tautulli Monitor :: Plex remote access is back up.")
plexpy.PLEX_REMOTE_ACCESS_UP = True
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extup', 'remote_access_info': server_response})
elif plexpy.PLEX_REMOTE_ACCESS_UP is None:
plexpy.PLEX_REMOTE_ACCESS_UP = self.is_reachable()
def del_keys(key):
if isinstance(key, set):
for child_key in key:

View File

@@ -216,75 +216,6 @@ def check_active_sessions(ws_request=False):
logger.debug("Tautulli Monitor :: Unable to read session list.")
def check_recently_added():
with monitor_lock:
# add delay to allow for metadata processing
delay = plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY
time_threshold = helpers.timestamp() - delay
time_interval = plexpy.CONFIG.MONITORING_INTERVAL
pms_connect = pmsconnect.PmsConnect()
recently_added_list = pms_connect.get_recently_added_details(count='10')
library_data = libraries.Libraries()
if recently_added_list:
recently_added = recently_added_list['recently_added']
for item in recently_added:
library_details = library_data.get_details(section_id=item['section_id'])
if not library_details['do_notify_created']:
continue
metadata = []
if 0 < time_threshold - int(item['added_at']) <= time_interval:
if item['media_type'] == 'movie':
metadata = pms_connect.get_metadata_details(item['rating_key'])
if metadata:
metadata = [metadata]
else:
logger.error("Tautulli Monitor :: Unable to retrieve metadata for rating_key %s" \
% str(item['rating_key']))
else:
metadata = pms_connect.get_metadata_children_details(item['rating_key'])
if not metadata:
logger.error("Tautulli Monitor :: Unable to retrieve children metadata for rating_key %s" \
% str(item['rating_key']))
if metadata:
if not plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED:
for item in metadata:
library_details = library_data.get_details(section_id=item['section_id'])
if 0 < time_threshold - int(item['added_at']) <= time_interval:
logger.debug("Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
else:
item = max(metadata, key=lambda x:x['added_at'])
if 0 < time_threshold - int(item['added_at']) <= time_interval:
if item['media_type'] == 'episode' or item['media_type'] == 'track':
metadata = pms_connect.get_metadata_details(item['grandparent_rating_key'])
if metadata:
item = metadata
else:
logger.error("Tautulli Monitor :: Unable to retrieve grandparent metadata for grandparent_rating_key %s" \
% str(item['rating_key']))
logger.debug("Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
# Check if any notification agents have notifications enabled
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
def connect_server(log=True, startup=False):
if plexpy.CONFIG.PMS_IS_CLOUD:
if log:
@@ -318,47 +249,6 @@ def connect_server(log=True, startup=False):
logger.error("Websocket :: Unable to open connection: %s." % e)
def check_server_access():
with monitor_lock:
pms_connect = pmsconnect.PmsConnect()
server_response = pms_connect.get_server_response()
global ext_ping_count
global ext_ping_error
# Check for remote access
if server_response:
log = (server_response['mapping_error'] != ext_ping_error)
if server_response['reason']:
ext_ping_count += 1
ext_ping_error = server_response['mapping_error']
if log:
logger.warn("Tautulli Monitor :: Remote access failed: %s, ping attempt %s."
% (server_response['reason'], str(ext_ping_count)))
# Waiting for port mapping
elif server_response['mapping_state'] == 'waiting':
ext_ping_error = server_response['mapping_error']
if log:
logger.warn("Tautulli Monitor :: Remote access waiting for port mapping, ping attempt %s."
% str(ext_ping_count))
# Reset external ping counter
else:
if ext_ping_count >= plexpy.CONFIG.REMOTE_ACCESS_PING_THRESHOLD:
logger.info("Tautulli Monitor :: Plex remote access is back up.")
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extup', 'remote_access_info': server_response})
ext_ping_count = 0
ext_ping_error = None
if ext_ping_count == plexpy.CONFIG.REMOTE_ACCESS_PING_THRESHOLD:
logger.info("Tautulli Monitor: Plex remote access is down.")
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extdown', 'remote_access_info': server_response})
def check_server_updates():
with monitor_lock:

View File

@@ -136,7 +136,7 @@ class API2(object):
self._api_app = True
if plexpy.CONFIG.API_ENABLED and not self._api_msg or self._api_cmd in ('get_apikey', 'docs', 'docs_md'):
if self._api_apikey == plexpy.CONFIG.API_KEY:
if not self._api_app and self._api_apikey == plexpy.CONFIG.API_KEY:
self._api_authenticated = True
elif self._api_app and self._api_apikey == mobile_app.get_temp_device_token() and \
@@ -391,19 +391,23 @@ class API2(object):
return data
def register_device(self, device_id='', device_name='', friendly_name='', **kwargs):
def register_device(self, device_id='', device_name='', friendly_name='', onesignal_id=None, **kwargs):
""" Registers the Tautulli Android App for notifications.
```
Required parameters:
device_name (str): The device name of the Tautulli Android App
device_id (str): The OneSignal device id of the Tautulli Android App
device_id (str): The unique device identifier for the mobile device
device_name (str): The device name of the mobile device
Optional parameters:
friendly_name (str): A friendly name to identify the mobile device
onesignal_id (str): The OneSignal id for the mobile device
Returns:
None
json:
{"pms_name": "Winterfell-Server",
"server_id": "ds48g4r354a8v9byrrtr697g3g79w"
}
```
"""
if not device_id:
@@ -416,15 +420,29 @@ class API2(object):
self._api_result_type = 'error'
return
## TODO: Temporary for backwards compatibility, assume device_id is onesignal_id
if device_id and onesignal_id is None:
onesignal_id = device_id
result = mobile_app.add_mobile_device(device_id=device_id,
device_name=device_name,
device_token=self._api_apikey,
friendly_name=friendly_name)
friendly_name=friendly_name,
onesignal_id=onesignal_id)
if result:
self._api_msg = 'Device registration successful.'
self._api_result_type = 'success'
mobile_app.set_temp_device_token(None)
data = {
"pms_name": plexpy.CONFIG.PMS_NAME,
"server_id": plexpy.CONFIG.PMS_UUID
}
return data
else:
self._api_msg = 'Device registration failed: database error.'
self._api_result_type = 'error'
@@ -686,6 +704,13 @@ General optional parameters:
def _api_run(self, *args, **kwargs):
""" handles the stuff from the handler """
# Make sure the device ID is not shown in the logs
if self._api_cmd == 'register_device':
if kwargs.get('device_id'):
logger._BLACKLIST_WORDS.add(kwargs['device_id'])
if kwargs.get('onesignal_id'):
logger._BLACKLIST_WORDS.add(kwargs['onesignal_id'])
result = {}
logger.api_debug('Tautulli APIv2 :: API called with kwargs: %s' % kwargs)

View File

@@ -35,6 +35,7 @@ PLATFORM_RELEASE = platform.release()
PLATFORM_VERSION = platform.version()
PLATFORM_LINUX_DISTRO = ' '.join(x for x in distro.linux_distribution() if x)
PLATFORM_DEVICE_NAME = platform.node()
PYTHON_VERSION = platform.python_version()
BRANCH = version.PLEXPY_BRANCH
RELEASE = version.PLEXPY_RELEASE_VERSION
@@ -216,18 +217,19 @@ EXTRA_TYPES = {
}
SCHEDULER_LIST = [
'Check GitHub for updates',
'Check for server response',
'Check for active sessions',
'Check for recently added items',
'Check for Plex updates',
'Check for Plex remote access',
'Refresh users list',
'Refresh libraries list',
'Refresh Plex server URLs',
'Backup Tautulli database',
'Backup Tautulli config'
('Check GitHub for updates', 'websocket'),
('Check for server response', 'websocket'),
('Check for active sessions', 'websocket'),
('Check for recently added items', 'websocket'),
('Check for server remote access', 'websocket'),
('Check for Plex updates', 'scheduled'),
('Refresh users list', 'scheduled'),
('Refresh libraries list', 'scheduled'),
('Refresh Plex server URLs', 'scheduled'),
('Backup Tautulli database', 'scheduled'),
('Backup Tautulli config', 'scheduled')
]
SCHEDULER_LIST = OrderedDict(SCHEDULER_LIST)
DATE_TIME_FORMATS = [
{
@@ -496,6 +498,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Audience Rating', 'type': 'int', 'value': 'audience_rating', 'description': 'The audience rating (%) for the item.', 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
{'name': 'Duration', 'type': 'int', 'value': 'duration', 'description': 'The duration (in minutes) for the item.'},
{'name': 'Poster URL', 'type': 'str', 'value': 'poster_url', 'description': 'A URL for the movie, TV show, or album poster.'},
{'name': 'Plex ID', 'type': 'str', 'value': 'plex_id', 'description': 'The Plex ID for the item.', 'example': 'e.g. 5d7769a9594b2b001e6a6b7e'},
{'name': 'Plex URL', 'type': 'str', 'value': 'plex_url', 'description': 'The Plex URL to your server for the item.'},
{'name': 'IMDB ID', 'type': 'str', 'value': 'imdb_id', 'description': 'The IMDB ID for the movie.', 'example': 'e.g. tt2488496'},
{'name': 'IMDB URL', 'type': 'str', 'value': 'imdb_url', 'description': 'The IMDB URL for the movie.'},

View File

@@ -22,8 +22,9 @@ import os
import re
import shutil
import time
import threading
from configobj import ConfigObj
from configobj import ConfigObj, ParseError
import plexpy
if plexpy.PYTHON2:
@@ -41,15 +42,12 @@ def bool_int(value):
value = 0
return int(bool(value))
FILENAME = "config.ini"
_CONFIG_DEFINITIONS = {
'ALLOW_GUEST_ACCESS': (int, 'General', 0),
'DATE_FORMAT': (str, 'General', 'YYYY-MM-DD'),
'GROUPING_GLOBAL_HISTORY': (int, 'PlexWatch', 0),
'GROUPING_USER_HISTORY': (int, 'PlexWatch', 0),
'GROUPING_CHARTS': (int, 'PlexWatch', 0),
'PLEXWATCH_DATABASE': (str, 'PlexWatch', ''),
'PMS_IDENTIFIER': (str, 'PMS', ''),
'PMS_IP': (str, 'PMS', '127.0.0.1'),
'PMS_IS_CLOUD': (int, 'PMS', 0),
@@ -75,44 +73,10 @@ _CONFIG_DEFINITIONS = {
'PMS_UPDATE_CHECK_INTERVAL': (int, 'Advanced', 24),
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
'TIME_FORMAT': (str, 'General', 'HH:mm'),
'ADD_LIVE_TV_LIBRARY': (int, 'Advanced', 1),
'ANON_REDIRECT': (str, 'General', 'http://www.nullrefer.com/?'),
'ANON_REDIRECT': (str, 'General', 'https://www.nullrefer.com/?'),
'API_ENABLED': (int, 'General', 1),
'API_KEY': (str, 'General', ''),
'API_SQL': (int, 'General', 0),
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
'BOXCAR_SOUND': (str, 'Boxcar', ''),
'BOXCAR_ON_PLAY': (int, 'Boxcar', 0),
'BOXCAR_ON_STOP': (int, 'Boxcar', 0),
'BOXCAR_ON_PAUSE': (int, 'Boxcar', 0),
'BOXCAR_ON_RESUME': (int, 'Boxcar', 0),
'BOXCAR_ON_BUFFER': (int, 'Boxcar', 0),
'BOXCAR_ON_WATCHED': (int, 'Boxcar', 0),
'BOXCAR_ON_CREATED': (int, 'Boxcar', 0),
'BOXCAR_ON_EXTDOWN': (int, 'Boxcar', 0),
'BOXCAR_ON_INTDOWN': (int, 'Boxcar', 0),
'BOXCAR_ON_EXTUP': (int, 'Boxcar', 0),
'BOXCAR_ON_INTUP': (int, 'Boxcar', 0),
'BOXCAR_ON_PMSUPDATE': (int, 'Boxcar', 0),
'BOXCAR_ON_CONCURRENT': (int, 'Boxcar', 0),
'BOXCAR_ON_NEWDEVICE': (int, 'Boxcar', 0),
'BROWSER_ENABLED': (int, 'Browser', 0),
'BROWSER_AUTO_HIDE_DELAY': (int, 'Browser', 5),
'BROWSER_ON_PLAY': (int, 'Browser', 0),
'BROWSER_ON_STOP': (int, 'Browser', 0),
'BROWSER_ON_PAUSE': (int, 'Browser', 0),
'BROWSER_ON_RESUME': (int, 'Browser', 0),
'BROWSER_ON_BUFFER': (int, 'Browser', 0),
'BROWSER_ON_WATCHED': (int, 'Browser', 0),
'BROWSER_ON_CREATED': (int, 'Browser', 0),
'BROWSER_ON_EXTDOWN': (int, 'Browser', 0),
'BROWSER_ON_INTDOWN': (int, 'Browser', 0),
'BROWSER_ON_EXTUP': (int, 'Browser', 0),
'BROWSER_ON_INTUP': (int, 'Browser', 0),
'BROWSER_ON_PMSUPDATE': (int, 'Browser', 0),
'BROWSER_ON_CONCURRENT': (int, 'Browser', 0),
'BROWSER_ON_NEWDEVICE': (int, 'Browser', 0),
'BUFFER_THRESHOLD': (int, 'Monitoring', 10),
'BUFFER_WAIT': (int, 'Monitoring', 900),
'BACKUP_DAYS': (int, 'General', 3),
@@ -130,56 +94,7 @@ _CONFIG_DEFINITIONS = {
'CLOUDINARY_API_SECRET': (str, 'Cloudinary', ''),
'CONFIG_VERSION': (int, 'Advanced', 0),
'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0),
'EMAIL_ENABLED': (int, 'Email', 0),
'EMAIL_FROM_NAME': (str, 'Email', 'Tautulli'),
'EMAIL_FROM': (str, 'Email', ''),
'EMAIL_TO': (str, 'Email', ''),
'EMAIL_CC': (str, 'Email', ''),
'EMAIL_BCC': (str, 'Email', ''),
'EMAIL_SMTP_SERVER': (str, 'Email', ''),
'EMAIL_SMTP_USER': (str, 'Email', ''),
'EMAIL_SMTP_PASSWORD': (str, 'Email', ''),
'EMAIL_SMTP_PORT': (int, 'Email', 25),
'EMAIL_TLS': (int, 'Email', 0),
'EMAIL_HTML_SUPPORT': (int, 'Email', 1),
'EMAIL_ON_PLAY': (int, 'Email', 0),
'EMAIL_ON_STOP': (int, 'Email', 0),
'EMAIL_ON_PAUSE': (int, 'Email', 0),
'EMAIL_ON_RESUME': (int, 'Email', 0),
'EMAIL_ON_BUFFER': (int, 'Email', 0),
'EMAIL_ON_WATCHED': (int, 'Email', 0),
'EMAIL_ON_CREATED': (int, 'Email', 0),
'EMAIL_ON_EXTDOWN': (int, 'Email', 0),
'EMAIL_ON_INTDOWN': (int, 'Email', 0),
'EMAIL_ON_EXTUP': (int, 'Email', 0),
'EMAIL_ON_INTUP': (int, 'Email', 0),
'EMAIL_ON_PMSUPDATE': (int, 'Email', 0),
'EMAIL_ON_CONCURRENT': (int, 'Email', 0),
'EMAIL_ON_NEWDEVICE': (int, 'Email', 0),
'ENABLE_HTTPS': (int, 'General', 0),
'FACEBOOK_ENABLED': (int, 'Facebook', 0),
'FACEBOOK_REDIRECT_URI': (str, 'Facebook', ''),
'FACEBOOK_APP_ID': (str, 'Facebook', ''),
'FACEBOOK_APP_SECRET': (str, 'Facebook', ''),
'FACEBOOK_TOKEN': (str, 'Facebook', ''),
'FACEBOOK_GROUP': (str, 'Facebook', ''),
'FACEBOOK_INCL_PMSLINK': (int, 'Facebook', 0),
'FACEBOOK_INCL_POSTER': (int, 'Facebook', 0),
'FACEBOOK_INCL_SUBJECT': (int, 'Facebook', 1),
'FACEBOOK_ON_PLAY': (int, 'Facebook', 0),
'FACEBOOK_ON_STOP': (int, 'Facebook', 0),
'FACEBOOK_ON_PAUSE': (int, 'Facebook', 0),
'FACEBOOK_ON_RESUME': (int, 'Facebook', 0),
'FACEBOOK_ON_BUFFER': (int, 'Facebook', 0),
'FACEBOOK_ON_WATCHED': (int, 'Facebook', 0),
'FACEBOOK_ON_CREATED': (int, 'Facebook', 0),
'FACEBOOK_ON_EXTDOWN': (int, 'Facebook', 0),
'FACEBOOK_ON_INTDOWN': (int, 'Facebook', 0),
'FACEBOOK_ON_EXTUP': (int, 'Facebook', 0),
'FACEBOOK_ON_INTUP': (int, 'Facebook', 0),
'FACEBOOK_ON_PMSUPDATE': (int, 'Facebook', 0),
'FACEBOOK_ON_CONCURRENT': (int, 'Facebook', 0),
'FACEBOOK_ON_NEWDEVICE': (int, 'Facebook', 0),
'FIRST_RUN_COMPLETE': (int, 'General', 0),
'FREEZE_DB': (int, 'General', 0),
'GET_FILE_SIZES': (int, 'General', 0),
@@ -191,27 +106,10 @@ _CONFIG_DEFINITIONS = {
'GIT_USER': (str, 'General', 'Tautulli'),
'GIT_REPO': (str, 'General', 'Tautulli'),
'GROUP_HISTORY_TABLES': (int, 'General', 1),
'GROWL_ENABLED': (int, 'Growl', 0),
'GROWL_HOST': (str, 'Growl', ''),
'GROWL_PASSWORD': (str, 'Growl', ''),
'GROWL_ON_PLAY': (int, 'Growl', 0),
'GROWL_ON_STOP': (int, 'Growl', 0),
'GROWL_ON_PAUSE': (int, 'Growl', 0),
'GROWL_ON_RESUME': (int, 'Growl', 0),
'GROWL_ON_BUFFER': (int, 'Growl', 0),
'GROWL_ON_WATCHED': (int, 'Growl', 0),
'GROWL_ON_CREATED': (int, 'Growl', 0),
'GROWL_ON_EXTDOWN': (int, 'Growl', 0),
'GROWL_ON_INTDOWN': (int, 'Growl', 0),
'GROWL_ON_EXTUP': (int, 'Growl', 0),
'GROWL_ON_INTUP': (int, 'Growl', 0),
'GROWL_ON_PMSUPDATE': (int, 'Growl', 0),
'GROWL_ON_CONCURRENT': (int, 'Growl', 0),
'GROWL_ON_NEWDEVICE': (int, 'Growl', 0),
'HISTORY_TABLE_ACTIVITY': (int, 'General', 1),
'HOME_SECTIONS': (list, 'General', ['current_activity','watch_stats','library_stats','recently_added']),
'HOME_SECTIONS': (list, 'General', ['current_activity', 'watch_stats', 'library_stats', 'recently_added']),
'HOME_LIBRARY_CARDS': (list, 'General', ['first_run']),
'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']),
'HOME_REFRESH_INTERVAL': (int, 'General', 10),
'HTTPS_CREATE_CERT': (int, 'General', 1),
@@ -232,65 +130,8 @@ _CONFIG_DEFINITIONS = {
'HTTP_USERNAME': (str, 'General', ''),
'HTTP_PLEX_ADMIN': (int, 'General', 0),
'HTTP_BASE_URL': (str, 'General', ''),
'HIPCHAT_URL': (str, 'Hipchat', ''),
'HIPCHAT_COLOR': (str, 'Hipchat', ''),
'HIPCHAT_INCL_SUBJECT': (int, 'Hipchat', 1),
'HIPCHAT_INCL_PMSLINK': (int, 'Hipchat', 0),
'HIPCHAT_INCL_POSTER': (int, 'Hipchat', 0),
'HIPCHAT_EMOTICON': (str, 'Hipchat', ''),
'HIPCHAT_ENABLED': (int, 'Hipchat', 0),
'HIPCHAT_ON_PLAY': (int, 'Hipchat', 0),
'HIPCHAT_ON_STOP': (int, 'Hipchat', 0),
'HIPCHAT_ON_PAUSE': (int, 'Hipchat', 0),
'HIPCHAT_ON_RESUME': (int, 'Hipchat', 0),
'HIPCHAT_ON_BUFFER': (int, 'Hipchat', 0),
'HIPCHAT_ON_WATCHED': (int, 'Hipchat', 0),
'HIPCHAT_ON_CREATED': (int, 'Hipchat', 0),
'HIPCHAT_ON_EXTDOWN': (int, 'Hipchat', 0),
'HIPCHAT_ON_INTDOWN': (int, 'Hipchat', 0),
'HIPCHAT_ON_EXTUP': (int, 'Hipchat', 0),
'HIPCHAT_ON_INTUP': (int, 'Hipchat', 0),
'HIPCHAT_ON_PMSUPDATE': (int, 'Hipchat', 0),
'HIPCHAT_ON_CONCURRENT': (int, 'Hipchat', 0),
'HIPCHAT_ON_NEWDEVICE': (int, 'Hipchat', 0),
'INTERFACE': (str, 'General', 'default'),
'IP_LOGGING_ENABLE': (int, 'General', 0),
'IFTTT_KEY': (str, 'IFTTT', ''),
'IFTTT_EVENT': (str, 'IFTTT', 'tautulli'),
'IFTTT_ENABLED': (int, 'IFTTT', 0),
'IFTTT_ON_PLAY': (int, 'IFTTT', 0),
'IFTTT_ON_STOP': (int, 'IFTTT', 0),
'IFTTT_ON_PAUSE': (int, 'IFTTT', 0),
'IFTTT_ON_RESUME': (int, 'IFTTT', 0),
'IFTTT_ON_BUFFER': (int, 'IFTTT', 0),
'IFTTT_ON_WATCHED': (int, 'IFTTT', 0),
'IFTTT_ON_CREATED': (int, 'IFTTT', 0),
'IFTTT_ON_EXTDOWN': (int, 'IFTTT', 0),
'IFTTT_ON_INTDOWN': (int, 'IFTTT', 0),
'IFTTT_ON_EXTUP': (int, 'IFTTT', 0),
'IFTTT_ON_INTUP': (int, 'IFTTT', 0),
'IFTTT_ON_PMSUPDATE': (int, 'IFTTT', 0),
'IFTTT_ON_CONCURRENT': (int, 'IFTTT', 0),
'IFTTT_ON_NEWDEVICE': (int, 'IFTTT', 0),
'IMGUR_CLIENT_ID': (str, 'Monitoring', ''),
'JOIN_APIKEY': (str, 'Join', ''),
'JOIN_DEVICEID': (str, 'Join', ''),
'JOIN_ENABLED': (int, 'Join', 0),
'JOIN_INCL_SUBJECT': (int, 'Join', 1),
'JOIN_ON_PLAY': (int, 'Join', 0),
'JOIN_ON_STOP': (int, 'Join', 0),
'JOIN_ON_PAUSE': (int, 'Join', 0),
'JOIN_ON_RESUME': (int, 'Join', 0),
'JOIN_ON_BUFFER': (int, 'Join', 0),
'JOIN_ON_WATCHED': (int, 'Join', 0),
'JOIN_ON_CREATED': (int, 'Join', 0),
'JOIN_ON_EXTDOWN': (int, 'Join', 0),
'JOIN_ON_INTDOWN': (int, 'Join', 0),
'JOIN_ON_EXTUP': (int, 'Join', 0),
'JOIN_ON_INTUP': (int, 'Join', 0),
'JOIN_ON_PMSUPDATE': (int, 'Join', 0),
'JOIN_ON_CONCURRENT': (int, 'Join', 0),
'JOIN_ON_NEWDEVICE': (int, 'Join', 0),
'JOURNAL_MODE': (str, 'Advanced', 'WAL'),
'LAUNCH_BROWSER': (int, 'General', 1),
'LAUNCH_STARTUP': (int, 'General', 1),
@@ -298,23 +139,11 @@ _CONFIG_DEFINITIONS = {
'LOG_DIR': (str, 'General', ''),
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
'METADATA_CACHE_SECONDS': (int, 'Advanced', 1800),
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1),
'MOVIE_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
'MOVIE_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
'MOVIE_WATCHED_PERCENT': (int, 'Monitoring', 85),
'MUSIC_LOGGING_ENABLE': (int, 'Monitoring', 1),
'MUSIC_NOTIFY_ENABLE': (int, 'Monitoring', 0),
'MUSIC_NOTIFY_ON_START': (int, 'Monitoring', 1),
'MUSIC_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
'MUSIC_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
'MUSIC_WATCHED_PERCENT': (int, 'Monitoring', 85),
'MUSICBRAINZ_LOOKUP': (int, 'General', 0),
'MONITOR_PMS_UPDATES': (int, 'Monitoring', 0),
'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0),
'MONITORING_INTERVAL': (int, 'Monitoring', 60),
'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0),
'NEWSLETTER_AUTH': (int, 'Newsletter', 0),
'NEWSLETTER_PASSWORD': (str, 'Newsletter', ''),
'NEWSLETTER_CUSTOM_DIR': (str, 'Newsletter', ''),
@@ -322,319 +151,37 @@ _CONFIG_DEFINITIONS = {
'NEWSLETTER_TEMPLATES': (str, 'Newsletter', 'newsletters'),
'NEWSLETTER_DIR': (str, 'Newsletter', ''),
'NEWSLETTER_SELF_HOSTED': (int, 'Newsletter', 0),
'NEWSLETTER_STATIC_URL': (int, 'Newsletter', 0),
'NMA_APIKEY': (str, 'NMA', ''),
'NMA_ENABLED': (int, 'NMA', 0),
'NMA_PRIORITY': (int, 'NMA', 0),
'NMA_ON_PLAY': (int, 'NMA', 0),
'NMA_ON_STOP': (int, 'NMA', 0),
'NMA_ON_PAUSE': (int, 'NMA', 0),
'NMA_ON_RESUME': (int, 'NMA', 0),
'NMA_ON_BUFFER': (int, 'NMA', 0),
'NMA_ON_WATCHED': (int, 'NMA', 0),
'NMA_ON_CREATED': (int, 'NMA', 0),
'NMA_ON_EXTDOWN': (int, 'NMA', 0),
'NMA_ON_INTDOWN': (int, 'NMA', 0),
'NMA_ON_EXTUP': (int, 'NMA', 0),
'NMA_ON_INTUP': (int, 'NMA', 0),
'NMA_ON_PMSUPDATE': (int, 'NMA', 0),
'NMA_ON_CONCURRENT': (int, 'NMA', 0),
'NMA_ON_NEWDEVICE': (int, 'NMA', 0),
'NOTIFICATION_THREADS': (int, 'Advanced', 2),
'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1),
'NOTIFY_CONTINUED_SESSION_THRESHOLD': (int, 'Monitoring', 15),
'NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 1),
'NOTIFY_GROUP_RECENTLY_ADDED_PARENT': (int, 'Monitoring', 1),
'NOTIFY_GROUP_RECENTLY_ADDED': (int, 'Monitoring', 1),
'NOTIFY_UPLOAD_POSTERS': (int, 'Monitoring', 0),
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 300),
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
'NOTIFY_RECENTLY_ADDED_UPGRADE': (int, 'Monitoring', 0),
'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0),
'NOTIFY_CONCURRENT_THRESHOLD': (int, 'Monitoring', 2),
'NOTIFY_WATCHED_PERCENT': (int, 'Monitoring', 85),
'NOTIFY_ON_START_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_START_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) started playing {title}.'),
'NOTIFY_ON_STOP_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_STOP_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has stopped {title}.'),
'NOTIFY_ON_PAUSE_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_PAUSE_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has paused {title}.'),
'NOTIFY_ON_RESUME_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_RESUME_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has resumed {title}.'),
'NOTIFY_ON_BUFFER_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_BUFFER_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) is buffering {title}.'),
'NOTIFY_ON_WATCHED_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_WATCHED_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has watched {title}.'),
'NOTIFY_ON_CREATED_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_CREATED_BODY_TEXT': (str, 'Monitoring', '{title} was recently added to Plex.'),
'NOTIFY_ON_EXTDOWN_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_EXTDOWN_BODY_TEXT': (str, 'Monitoring', 'The Plex Media Server remote access is down.'),
'NOTIFY_ON_INTDOWN_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_INTDOWN_BODY_TEXT': (str, 'Monitoring', 'The Plex Media Server is down.'),
'NOTIFY_ON_EXTUP_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_EXTUP_BODY_TEXT': (str, 'Monitoring', 'The Plex Media Server remote access is back up.'),
'NOTIFY_ON_INTUP_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_INTUP_BODY_TEXT': (str, 'Monitoring', 'The Plex Media Server is back up.'),
'NOTIFY_ON_PMSUPDATE_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_PMSUPDATE_BODY_TEXT': (str, 'Monitoring', 'An update is available for the Plex Media Server (version {update_version}).'),
'NOTIFY_ON_CONCURRENT_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_CONCURRENT_BODY_TEXT': (str, 'Monitoring', '{user} has {user_streams} concurrent streams.'),
'NOTIFY_ON_NEWDEVICE_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
'NOTIFY_ON_NEWDEVICE_BODY_TEXT': (str, 'Monitoring', '{user} is streaming from a new device: {player}.'),
'NOTIFY_SCRIPTS_ARGS_TEXT': (str, 'Monitoring', ''),
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/Tautulli'),
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_STOP': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_PAUSE': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_RESUME': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_BUFFER': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_WATCHED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_CREATED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_EXTDOWN': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_INTDOWN': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_EXTUP': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_INTUP': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_PMSUPDATE': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_CONCURRENT': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_NEWDEVICE': (int, 'OSX_Notify', 0),
'PLEX_CLIENT_HOST': (str, 'Plex', ''),
'PLEX_ENABLED': (int, 'Plex', 0),
'PLEX_PASSWORD': (str, 'Plex', ''),
'PLEX_USERNAME': (str, 'Plex', ''),
'PLEX_ON_PLAY': (int, 'Plex', 0),
'PLEX_ON_STOP': (int, 'Plex', 0),
'PLEX_ON_PAUSE': (int, 'Plex', 0),
'PLEX_ON_RESUME': (int, 'Plex', 0),
'PLEX_ON_BUFFER': (int, 'Plex', 0),
'PLEX_ON_WATCHED': (int, 'Plex', 0),
'PLEX_ON_CREATED': (int, 'Plex', 0),
'PLEX_ON_EXTDOWN': (int, 'Plex', 0),
'PLEX_ON_INTDOWN': (int, 'Plex', 0),
'PLEX_ON_EXTUP': (int, 'Plex', 0),
'PLEX_ON_INTUP': (int, 'Plex', 0),
'PLEX_ON_PMSUPDATE': (int, 'Plex', 0),
'PLEX_ON_CONCURRENT': (int, 'Plex', 0),
'PLEX_ON_NEWDEVICE': (int, 'Plex', 0),
'PLEXPY_AUTO_UPDATE': (int, 'General', 0),
'PROWL_ENABLED': (int, 'Prowl', 0),
'PROWL_KEYS': (str, 'Prowl', ''),
'PROWL_PRIORITY': (int, 'Prowl', 0),
'PROWL_ON_PLAY': (int, 'Prowl', 0),
'PROWL_ON_STOP': (int, 'Prowl', 0),
'PROWL_ON_PAUSE': (int, 'Prowl', 0),
'PROWL_ON_RESUME': (int, 'Prowl', 0),
'PROWL_ON_BUFFER': (int, 'Prowl', 0),
'PROWL_ON_WATCHED': (int, 'Prowl', 0),
'PROWL_ON_CREATED': (int, 'Prowl', 0),
'PROWL_ON_EXTDOWN': (int, 'Prowl', 0),
'PROWL_ON_INTDOWN': (int, 'Prowl', 0),
'PROWL_ON_EXTUP': (int, 'Prowl', 0),
'PROWL_ON_INTUP': (int, 'Prowl', 0),
'PROWL_ON_PMSUPDATE': (int, 'Prowl', 0),
'PROWL_ON_CONCURRENT': (int, 'Prowl', 0),
'PROWL_ON_NEWDEVICE': (int, 'Prowl', 0),
'PUSHALOT_APIKEY': (str, 'Pushalot', ''),
'PUSHALOT_ENABLED': (int, 'Pushalot', 0),
'PUSHALOT_ON_PLAY': (int, 'Pushalot', 0),
'PUSHALOT_ON_STOP': (int, 'Pushalot', 0),
'PUSHALOT_ON_PAUSE': (int, 'Pushalot', 0),
'PUSHALOT_ON_RESUME': (int, 'Pushalot', 0),
'PUSHALOT_ON_BUFFER': (int, 'Pushalot', 0),
'PUSHALOT_ON_WATCHED': (int, 'Pushalot', 0),
'PUSHALOT_ON_CREATED': (int, 'Pushalot', 0),
'PUSHALOT_ON_EXTDOWN': (int, 'Pushalot', 0),
'PUSHALOT_ON_INTDOWN': (int, 'Pushalot', 0),
'PUSHALOT_ON_EXTUP': (int, 'Pushalot', 0),
'PUSHALOT_ON_INTUP': (int, 'Pushalot', 0),
'PUSHALOT_ON_PMSUPDATE': (int, 'Pushalot', 0),
'PUSHALOT_ON_CONCURRENT': (int, 'Pushalot', 0),
'PUSHALOT_ON_NEWDEVICE': (int, 'Pushalot', 0),
'PUSHBULLET_APIKEY': (str, 'PushBullet', ''),
'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''),
'PUSHBULLET_CHANNEL_TAG': (str, 'PushBullet', ''),
'PUSHBULLET_ENABLED': (int, 'PushBullet', 0),
'PUSHBULLET_ON_PLAY': (int, 'PushBullet', 0),
'PUSHBULLET_ON_STOP': (int, 'PushBullet', 0),
'PUSHBULLET_ON_PAUSE': (int, 'PushBullet', 0),
'PUSHBULLET_ON_RESUME': (int, 'PushBullet', 0),
'PUSHBULLET_ON_BUFFER': (int, 'PushBullet', 0),
'PUSHBULLET_ON_WATCHED': (int, 'PushBullet', 0),
'PUSHBULLET_ON_CREATED': (int, 'PushBullet', 0),
'PUSHBULLET_ON_EXTDOWN': (int, 'PushBullet', 0),
'PUSHBULLET_ON_INTDOWN': (int, 'PushBullet', 0),
'PUSHBULLET_ON_EXTUP': (int, 'PushBullet', 0),
'PUSHBULLET_ON_INTUP': (int, 'PushBullet', 0),
'PUSHBULLET_ON_PMSUPDATE': (int, 'PushBullet', 0),
'PUSHBULLET_ON_CONCURRENT': (int, 'PushBullet', 0),
'PUSHBULLET_ON_NEWDEVICE': (int, 'PushBullet', 0),
'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
'PUSHOVER_ENABLED': (int, 'Pushover', 0),
'PUSHOVER_HTML_SUPPORT': (int, 'Pushover', 1),
'PUSHOVER_INCL_PMSLINK': (int, 'Pushover', 0),
'PUSHOVER_INCL_URL': (int, 'Pushover', 1),
'PUSHOVER_KEYS': (str, 'Pushover', ''),
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
'PUSHOVER_SOUND': (str, 'Pushover', ''),
'PUSHOVER_ON_PLAY': (int, 'Pushover', 0),
'PUSHOVER_ON_STOP': (int, 'Pushover', 0),
'PUSHOVER_ON_PAUSE': (int, 'Pushover', 0),
'PUSHOVER_ON_RESUME': (int, 'Pushover', 0),
'PUSHOVER_ON_BUFFER': (int, 'Pushover', 0),
'PUSHOVER_ON_WATCHED': (int, 'Pushover', 0),
'PUSHOVER_ON_CREATED': (int, 'Pushover', 0),
'PUSHOVER_ON_EXTDOWN': (int, 'Pushover', 0),
'PUSHOVER_ON_INTDOWN': (int, 'Pushover', 0),
'PUSHOVER_ON_EXTUP': (int, 'Pushover', 0),
'PUSHOVER_ON_INTUP': (int, 'Pushover', 0),
'PUSHOVER_ON_PMSUPDATE': (int, 'Pushover', 0),
'PUSHOVER_ON_CONCURRENT': (int, 'Pushover', 0),
'PUSHOVER_ON_NEWDEVICE': (int, 'Pushover', 0),
'REFRESH_LIBRARIES_INTERVAL': (int, 'Monitoring', 12),
'REFRESH_LIBRARIES_ON_STARTUP': (int, 'Monitoring', 1),
'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12),
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
'REMOTE_ACCESS_PING_INTERVAL': (int, 'Advanced', 60),
'REMOTE_ACCESS_PING_THRESHOLD': (int, 'Advanced', 3),
'SESSION_DB_WRITE_ATTEMPTS': (int, 'Advanced', 5),
'SHOW_ADVANCED_SETTINGS': (int, 'General', 0),
'SLACK_ENABLED': (int, 'Slack', 0),
'SLACK_HOOK': (str, 'Slack', ''),
'SLACK_CHANNEL': (str, 'Slack', ''),
'SLACK_ICON_EMOJI': (str, 'Slack', ''),
'SLACK_INCL_PMSLINK': (int, 'Slack', 0),
'SLACK_INCL_POSTER': (int, 'Slack', 0),
'SLACK_INCL_SUBJECT': (int, 'Slack', 1),
'SLACK_USERNAME': (str, 'Slack', ''),
'SLACK_ON_PLAY': (int, 'Slack', 0),
'SLACK_ON_STOP': (int, 'Slack', 0),
'SLACK_ON_PAUSE': (int, 'Slack', 0),
'SLACK_ON_RESUME': (int, 'Slack', 0),
'SLACK_ON_BUFFER': (int, 'Slack', 0),
'SLACK_ON_WATCHED': (int, 'Slack', 0),
'SLACK_ON_CREATED': (int, 'Slack', 0),
'SLACK_ON_EXTDOWN': (int, 'Slack', 0),
'SLACK_ON_INTDOWN': (int, 'Slack', 0),
'SLACK_ON_EXTUP': (int, 'Slack', 0),
'SLACK_ON_INTUP': (int, 'Slack', 0),
'SLACK_ON_PMSUPDATE': (int, 'Slack', 0),
'SLACK_ON_CONCURRENT': (int, 'Slack', 0),
'SLACK_ON_NEWDEVICE': (int, 'Slack', 0),
'SCRIPTS_ENABLED': (int, 'Scripts', 0),
'SCRIPTS_FOLDER': (str, 'Scripts', ''),
'SCRIPTS_TIMEOUT': (int, 'Scripts', 30),
'SCRIPTS_ON_PLAY': (int, 'Scripts', 0),
'SCRIPTS_ON_STOP': (int, 'Scripts', 0),
'SCRIPTS_ON_PAUSE': (int, 'Scripts', 0),
'SCRIPTS_ON_RESUME': (int, 'Scripts', 0),
'SCRIPTS_ON_BUFFER': (int, 'Scripts', 0),
'SCRIPTS_ON_WATCHED': (int, 'Scripts', 0),
'SCRIPTS_ON_CREATED': (int, 'Scripts', 0),
'SCRIPTS_ON_EXTDOWN': (int, 'Scripts', 0),
'SCRIPTS_ON_EXTUP': (int, 'Scripts', 0),
'SCRIPTS_ON_INTDOWN': (int, 'Scripts', 0),
'SCRIPTS_ON_INTUP': (int, 'Scripts', 0),
'SCRIPTS_ON_PMSUPDATE': (int, 'Scripts', 0),
'SCRIPTS_ON_CONCURRENT': (int, 'Scripts', 0),
'SCRIPTS_ON_NEWDEVICE': (int, 'Scripts', 0),
'SCRIPTS_ON_PLAY_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_STOP_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_PAUSE_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_RESUME_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_BUFFER_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_WATCHED_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_CREATED_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_EXTDOWN_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_EXTUP_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_INTDOWN_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_INTUP_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_PMSUPDATE_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_CONCURRENT_SCRIPT': (str, 'Scripts', ''),
'SCRIPTS_ON_NEWDEVICE_SCRIPT': (str, 'Scripts', ''),
'SYNCHRONOUS_MODE': (str, 'Advanced', 'NORMAL'),
'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''),
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
'TELEGRAM_CHAT_ID': (str, 'Telegram', ''),
'TELEGRAM_DISABLE_WEB_PREVIEW': (int, 'Telegram', 0),
'TELEGRAM_HTML_SUPPORT': (int, 'Telegram', 1),
'TELEGRAM_INCL_POSTER': (int, 'Telegram', 0),
'TELEGRAM_INCL_SUBJECT': (int, 'Telegram', 1),
'TELEGRAM_ON_PLAY': (int, 'Telegram', 0),
'TELEGRAM_ON_STOP': (int, 'Telegram', 0),
'TELEGRAM_ON_PAUSE': (int, 'Telegram', 0),
'TELEGRAM_ON_RESUME': (int, 'Telegram', 0),
'TELEGRAM_ON_BUFFER': (int, 'Telegram', 0),
'TELEGRAM_ON_WATCHED': (int, 'Telegram', 0),
'TELEGRAM_ON_CREATED': (int, 'Telegram', 0),
'TELEGRAM_ON_EXTDOWN': (int, 'Telegram', 0),
'TELEGRAM_ON_INTDOWN': (int, 'Telegram', 0),
'TELEGRAM_ON_EXTUP': (int, 'Telegram', 0),
'TELEGRAM_ON_INTUP': (int, 'Telegram', 0),
'TELEGRAM_ON_PMSUPDATE': (int, 'Telegram', 0),
'TELEGRAM_ON_CONCURRENT': (int, 'Telegram', 0),
'TELEGRAM_ON_NEWDEVICE': (int, 'Telegram', 0),
'THEMOVIEDB_APIKEY': (str, 'General', 'e9a6655bae34bf694a0f3e33338dc28e'),
'THEMOVIEDB_LOOKUP': (int, 'General', 0),
'TVMAZE_LOOKUP': (int, 'General', 0),
'TV_LOGGING_ENABLE': (int, 'Monitoring', 1),
'TV_NOTIFY_ENABLE': (int, 'Monitoring', 0),
'TV_NOTIFY_ON_START': (int, 'Monitoring', 1),
'TV_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
'TV_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
'TV_WATCHED_PERCENT': (int, 'Monitoring', 85),
'TWITTER_ENABLED': (int, 'Twitter', 0),
'TWITTER_ACCESS_TOKEN': (str, 'Twitter', ''),
'TWITTER_ACCESS_TOKEN_SECRET': (str, 'Twitter', ''),
'TWITTER_CONSUMER_KEY': (str, 'Twitter', ''),
'TWITTER_CONSUMER_SECRET': (str, 'Twitter', ''),
'TWITTER_INCL_POSTER': (int, 'Twitter', 0),
'TWITTER_INCL_SUBJECT': (int, 'Twitter', 1),
'TWITTER_ON_PLAY': (int, 'Twitter', 0),
'TWITTER_ON_STOP': (int, 'Twitter', 0),
'TWITTER_ON_PAUSE': (int, 'Twitter', 0),
'TWITTER_ON_RESUME': (int, 'Twitter', 0),
'TWITTER_ON_BUFFER': (int, 'Twitter', 0),
'TWITTER_ON_WATCHED': (int, 'Twitter', 0),
'TWITTER_ON_CREATED': (int, 'Twitter', 0),
'TWITTER_ON_EXTDOWN': (int, 'Twitter', 0),
'TWITTER_ON_INTDOWN': (int, 'Twitter', 0),
'TWITTER_ON_EXTUP': (int, 'Twitter', 0),
'TWITTER_ON_INTUP': (int, 'Twitter', 0),
'TWITTER_ON_PMSUPDATE': (int, 'Twitter', 0),
'TWITTER_ON_CONCURRENT': (int, 'Twitter', 0),
'TWITTER_ON_NEWDEVICE': (int, 'Twitter', 0),
'UPDATE_DB_INTERVAL': (int, 'General', 24),
'UPDATE_SECTION_IDS': (int, 'General', 1),
'UPDATE_SHOW_CHANGELOG': (int, 'General', 1),
'UPDATE_LABELS': (int, 'General', 1),
'UPDATE_LIBRARIES_DB_NOTIFY': (int, 'General', 1),
'UPDATE_NOTIFIERS_DB': (int, 'General', 1),
'VERBOSE_LOGS': (int, 'Advanced', 1),
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 1),
'WEBSOCKET_MONITOR_PING_PONG': (int, 'Advanced', 0),
'WEBSOCKET_CONNECTION_ATTEMPTS': (int, 'Advanced', 5),
'WEBSOCKET_CONNECTION_TIMEOUT': (int, 'Advanced', 5),
'WEEK_START_MONDAY': (int, 'General', 0),
'XBMC_ENABLED': (int, 'XBMC', 0),
'XBMC_HOST': (str, 'XBMC', ''),
'XBMC_PASSWORD': (str, 'XBMC', ''),
'XBMC_USERNAME': (str, 'XBMC', ''),
'XBMC_ON_PLAY': (int, 'XBMC', 0),
'XBMC_ON_STOP': (int, 'XBMC', 0),
'XBMC_ON_PAUSE': (int, 'XBMC', 0),
'XBMC_ON_RESUME': (int, 'XBMC', 0),
'XBMC_ON_BUFFER': (int, 'XBMC', 0),
'XBMC_ON_WATCHED': (int, 'XBMC', 0),
'XBMC_ON_CREATED': (int, 'XBMC', 0),
'XBMC_ON_EXTDOWN': (int, 'XBMC', 0),
'XBMC_ON_INTDOWN': (int, 'XBMC', 0),
'XBMC_ON_EXTUP': (int, 'XBMC', 0),
'XBMC_ON_INTUP': (int, 'XBMC', 0),
'XBMC_ON_PMSUPDATE': (int, 'XBMC', 0),
'XBMC_ON_CONCURRENT': (int, 'XBMC', 0),
'XBMC_ON_NEWDEVICE': (int, 'XBMC', 0),
'JWT_SECRET': (str, 'Advanced', ''),
'JWT_UPDATE_SECRET': (bool_int, 'Advanced', 0),
'SYSTEM_ANALYTICS': (int, 'Advanced', 1),
@@ -642,7 +189,79 @@ _CONFIG_DEFINITIONS = {
}
_BLACKLIST_KEYS = ['_APITOKEN', '_TOKEN', '_KEY', '_SECRET', '_PASSWORD', '_APIKEY', '_ID', '_HOOK']
_WHITELIST_KEYS = ['HTTPS_KEY', 'UPDATE_SECTION_IDS']
_WHITELIST_KEYS = ['HTTPS_KEY']
_DO_NOT_IMPORT_KEYS = [
'FIRST_RUN_COMPLETE', 'GET_FILE_SIZES_HOLD', 'GIT_PATH', 'PMS_LOGS_FOLDER',
'BACKUP_DIR', 'CACHE_DIR', 'LOG_DIR', 'NEWSLETTER_DIR', 'NEWSLETTER_CUSTOM_DIR',
'HTTP_HOST', 'HTTP_PORT', 'HTTP_ROOT',
'HTTP_USERNAME', 'HTTP_PASSWORD', 'HTTP_HASH_PASSWORD', 'HTTP_HASHED_PASSWORD',
'ENABLE_HTTPS', 'HTTPS_CREATE_CERT', 'HTTPS_CERT', 'HTTPS_CERT_CHAIN', 'HTTPS_KEY'
]
_DO_NOT_IMPORT_KEYS_DOCKER = [
'PLEXPY_AUTO_UPDATE', 'GIT_REMOTE', 'GIT_BRANCH'
]
IS_IMPORTING = False
IMPORT_THREAD = None
def set_is_importing(value):
global IS_IMPORTING
IS_IMPORTING = value
def set_import_thread(config=None, backup=False):
global IMPORT_THREAD
if config:
if IMPORT_THREAD:
return
IMPORT_THREAD = threading.Thread(target=import_tautulli_config,
kwargs={'config': config, 'backup': backup})
else:
IMPORT_THREAD = None
def import_tautulli_config(config=None, backup=False):
if IS_IMPORTING:
logger.warn("Tautulli Config :: Another Tautulli config is currently being imported. "
"Please wait until it is complete before importing another config.")
return False
if backup:
# Make a backup of the current config first
logger.info("Tautulli Config :: Creating a config backup before importing.")
if not make_backup():
logger.error("Tautulli Config :: Failed to import Tautulli config: failed to create config backup")
return False
# Create a new Config object with the imported config file
try:
imported_config = Config(config, is_import=True)
except:
logger.error("Tautulli Config :: Failed to import Tautulli config: error reading imported config file")
return False
logger.info("Tautulli Config :: Importing Tautulli config '%s'...", config)
set_is_importing(True)
# Remove keys that should not be imported
for key in _DO_NOT_IMPORT_KEYS:
delattr(imported_config, key)
if plexpy.DOCKER:
for key in _DO_NOT_IMPORT_KEYS_DOCKER:
delattr(imported_config, key)
# Merge the imported config file into the current config file
plexpy.CONFIG._config.merge(imported_config._config)
plexpy.CONFIG.write()
logger.info("Tautulli Config :: Tautulli config import complete.")
set_import_thread(None)
set_is_importing(False)
# Restart to apply changes
plexpy.SIGNAL = 'restart'
def make_backup(cleanup=False, scheduler=False):
@@ -687,14 +306,20 @@ def make_backup(cleanup=False, scheduler=False):
class Config(object):
""" Wraps access to particular values in a config file """
def __init__(self, config_file):
def __init__(self, config_file, is_import=False):
""" Initialize the config with values from a file """
self._config_file = config_file
self._config = ConfigObj(self._config_file, encoding='utf-8')
try:
self._config = ConfigObj(self._config_file, encoding='utf-8')
except ParseError as e:
logger.error("Tautulli Config :: Error reading configuration file: %s", e)
raise
for key in _CONFIG_DEFINITIONS:
self.check_setting(key)
self._upgrade()
self._blacklist()
if not is_import:
self._upgrade()
self._blacklist()
def _blacklist(self):
""" Add tokens and passwords to blacklisted words in logger """
@@ -791,6 +416,16 @@ class Config(object):
self._config[section][ini_key] = definition_type(value)
return self._config[section][ini_key]
def __delattr__(self, name):
"""
Deletes a key from the configuration object.
"""
if not re.match(r'[A-Z_]+$', name):
return super(Config, self).__delattr__(name)
else:
key, definition_type, section, ini_key, default = self._define(name)
del self._config[section][ini_key]
def process_kwargs(self, kwargs):
"""
Given a big bunch of key value pairs, apply them to the ini.
@@ -804,14 +439,6 @@ class Config(object):
Upgrades config file from previous verisions and bumps up config version
"""
if self.CONFIG_VERSION == 0:
# Separate out movie and tv notifications
if self.MOVIE_NOTIFY_ENABLE == 1:
self.TV_NOTIFY_ENABLE = 1
# Separate out movie and tv logging
if self.VIDEO_LOGGING_ENABLE == 0:
self.MOVIE_LOGGING_ENABLE = 0
self.TV_LOGGING_ENABLE = 0
self.CONFIG_VERSION = 1
if self.CONFIG_VERSION == 1:
@@ -831,23 +458,6 @@ class Config(object):
self.CONFIG_VERSION = 2
if self.CONFIG_VERSION == 2:
def rep(s):
return s.replace('{progress}', '{progress_duration}')
self.NOTIFY_ON_START_SUBJECT_TEXT = rep(self.NOTIFY_ON_START_SUBJECT_TEXT)
self.NOTIFY_ON_START_BODY_TEXT = rep(self.NOTIFY_ON_START_BODY_TEXT)
self.NOTIFY_ON_STOP_SUBJECT_TEXT = rep(self.NOTIFY_ON_STOP_SUBJECT_TEXT)
self.NOTIFY_ON_STOP_BODY_TEXT = rep(self.NOTIFY_ON_STOP_BODY_TEXT)
self.NOTIFY_ON_PAUSE_SUBJECT_TEXT = rep(self.NOTIFY_ON_PAUSE_SUBJECT_TEXT)
self.NOTIFY_ON_PAUSE_BODY_TEXT = rep(self.NOTIFY_ON_PAUSE_BODY_TEXT)
self.NOTIFY_ON_RESUME_SUBJECT_TEXT = rep(self.NOTIFY_ON_RESUME_SUBJECT_TEXT)
self.NOTIFY_ON_RESUME_BODY_TEXT = rep(self.NOTIFY_ON_RESUME_BODY_TEXT)
self.NOTIFY_ON_BUFFER_SUBJECT_TEXT = rep(self.NOTIFY_ON_BUFFER_SUBJECT_TEXT)
self.NOTIFY_ON_BUFFER_BODY_TEXT = rep(self.NOTIFY_ON_BUFFER_BODY_TEXT)
self.NOTIFY_ON_WATCHED_SUBJECT_TEXT = rep(self.NOTIFY_ON_WATCHED_SUBJECT_TEXT)
self.NOTIFY_ON_WATCHED_BODY_TEXT = rep(self.NOTIFY_ON_WATCHED_BODY_TEXT)
self.NOTIFY_SCRIPTS_ARGS_TEXT = rep(self.NOTIFY_SCRIPTS_ARGS_TEXT)
self.CONFIG_VERSION = 3
if self.CONFIG_VERSION == 3:
@@ -880,37 +490,9 @@ class Config(object):
self.CONFIG_VERSION = 7
if self.CONFIG_VERSION == 7:
def rep(s):
return s.replace('<tv>', '<episode>') \
.replace('</tv>', '</episode>') \
.replace('<music>', '<track>') \
.replace('</music>', '</track>')
self.NOTIFY_ON_START_SUBJECT_TEXT = rep(self.NOTIFY_ON_START_SUBJECT_TEXT)
self.NOTIFY_ON_START_BODY_TEXT = rep(self.NOTIFY_ON_START_BODY_TEXT)
self.NOTIFY_ON_STOP_SUBJECT_TEXT = rep(self.NOTIFY_ON_STOP_SUBJECT_TEXT)
self.NOTIFY_ON_STOP_BODY_TEXT = rep(self.NOTIFY_ON_STOP_BODY_TEXT)
self.NOTIFY_ON_PAUSE_SUBJECT_TEXT = rep(self.NOTIFY_ON_PAUSE_SUBJECT_TEXT)
self.NOTIFY_ON_PAUSE_BODY_TEXT = rep(self.NOTIFY_ON_PAUSE_BODY_TEXT)
self.NOTIFY_ON_RESUME_SUBJECT_TEXT = rep(self.NOTIFY_ON_RESUME_SUBJECT_TEXT)
self.NOTIFY_ON_RESUME_BODY_TEXT = rep(self.NOTIFY_ON_RESUME_BODY_TEXT)
self.NOTIFY_ON_BUFFER_SUBJECT_TEXT = rep(self.NOTIFY_ON_BUFFER_SUBJECT_TEXT)
self.NOTIFY_ON_BUFFER_BODY_TEXT = rep(self.NOTIFY_ON_BUFFER_BODY_TEXT)
self.NOTIFY_ON_WATCHED_SUBJECT_TEXT = rep(self.NOTIFY_ON_WATCHED_SUBJECT_TEXT)
self.NOTIFY_ON_WATCHED_BODY_TEXT = rep(self.NOTIFY_ON_WATCHED_BODY_TEXT)
self.NOTIFY_SCRIPTS_ARGS_TEXT = rep(self.NOTIFY_SCRIPTS_ARGS_TEXT)
self.NOTIFY_GROUP_RECENTLY_ADDED_PARENT = self.NOTIFY_GROUP_RECENTLY_ADDED
self.MONITORING_USE_WEBSOCKET = 1
self.CONFIG_VERSION = 8
if self.CONFIG_VERSION == 8:
self.MOVIE_WATCHED_PERCENT = self.NOTIFY_WATCHED_PERCENT
self.TV_WATCHED_PERCENT = self.NOTIFY_WATCHED_PERCENT
self.MUSIC_WATCHED_PERCENT = self.NOTIFY_WATCHED_PERCENT
self.CONFIG_VERSION = 9
if self.CONFIG_VERSION == 9:
@@ -936,7 +518,6 @@ class Config(object):
self.CONFIG_VERSION = 13
if self.CONFIG_VERSION == 13:
self.CONFIG_VERSION = 14
if self.CONFIG_VERSION == 14:

View File

@@ -27,10 +27,10 @@ import time
import plexpy
if plexpy.PYTHON2:
import logger
from helpers import cast_to_int, bool_true
from helpers import cast_to_int, chunk
else:
from plexpy import logger
from plexpy.helpers import cast_to_int, bool_true
from plexpy.helpers import cast_to_int, chunk
FILENAME = "tautulli.db"
@@ -41,7 +41,7 @@ IS_IMPORTING = False
def set_is_importing(value):
global IS_IMPORTING
IS_IMPORTING = bool_true(value)
IS_IMPORTING = value
def validate_database(database=None):
@@ -68,6 +68,11 @@ def validate_database(database=None):
def import_tautulli_db(database=None, method=None, backup=False):
if IS_IMPORTING:
logger.warn("Tautulli Database :: Another Tautulli database is currently being imported. "
"Please wait until it is complete before importing another database.")
return False
db_validate = validate_database(database=database)
if not db_validate == 'success':
logger.error("Tautulli Database :: Failed to import Tautulli database: %s", db_validate)
@@ -218,12 +223,16 @@ def delete_rows_from_table(table, row_ids):
if row_ids:
logger.info("Tautulli Database :: Deleting row ids %s from %s database table", row_ids, table)
query = "DELETE FROM " + table + " WHERE id IN (%s) " % ','.join(['?'] * len(row_ids))
monitor_db = MonitorDatabase()
# SQlite verions prior to 3.32.0 (2020-05-22) have maximum variable limit of 999
# https://sqlite.org/limits.html
sqlite_max_variable_number = 999
monitor_db = MonitorDatabase()
try:
monitor_db.action(query, row_ids)
return True
for row_ids_group in chunk(row_ids, sqlite_max_variable_number):
query = "DELETE FROM " + table + " WHERE id IN (%s) " % ','.join(['?'] * len(row_ids_group))
monitor_db.action(query, row_ids_group)
except Exception as e:
logger.error("Tautulli Database :: Failed to delete rows from %s database table: %s" % (table, e))
return False

View File

@@ -31,7 +31,7 @@ import datetime
from functools import wraps
import hashlib
import imghdr
from future.moves.itertools import zip_longest
from future.moves.itertools import islice, zip_longest
import ipwhois
import ipwhois.exceptions
import ipwhois.utils
@@ -112,7 +112,7 @@ def radio(variable, pos):
return ''
def latinToAscii(unicrap):
def latinToAscii(unicrap, replace=False):
"""
From couch potato
"""
@@ -150,7 +150,8 @@ def latinToAscii(unicrap):
if ord(i) in xlate:
r += xlate[ord(i)]
elif ord(i) >= 0x80:
pass
if replace:
r += '?'
else:
r += str(i)
@@ -736,11 +737,17 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
api_secret=plexpy.CONFIG.CLOUDINARY_API_SECRET
)
# Cloudinary library has very poor support for non-ASCII characters on Python 2
if plexpy.PYTHON2:
_img_title = latinToAscii(img_title, replace=True)
else:
_img_title = img_title
try:
response = upload((img_title, img_data),
public_id='{}_{}'.format(fallback, rating_key),
tags=['tautulli', fallback, str(rating_key)],
context={'title': img_title, 'rating_key': str(rating_key), 'fallback': fallback})
context={'title': _img_title, 'rating_key': str(rating_key), 'fallback': fallback})
logger.debug("Tautulli Helpers :: Image '{}' ({}) uploaded to Cloudinary.".format(img_title, fallback))
img_url = response.get('url', '')
except Exception as e:
@@ -1068,6 +1075,11 @@ def grouper(iterable, n, fillvalue=None):
return zip_longest(fillvalue=fillvalue, *args)
def chunk(it, size):
it = iter(it)
return iter(lambda: tuple(islice(it, size)), ())
def traverse_map(obj, func):
if isinstance(obj, list):
new_obj = []
@@ -1220,6 +1232,9 @@ def browse_path(path=None, include_hidden=False, filter_ext=''):
}
output.append(out)
if os.path.isfile(path):
path = os.path.dirname(path)
if not os.path.isdir(path):
return output
@@ -1257,6 +1272,10 @@ def browse_path(path=None, include_hidden=False, filter_ext=''):
'icon': 'folder'
}
output.append(out)
if filter_ext == '.folderonly':
break
for f in sorted(files):
if not include_hidden and f.startswith('.'):
continue

View File

@@ -88,6 +88,8 @@ def refresh_libraries():
if result == 'insert':
new_keys.append(section['section_id'])
add_live_tv_library(refresh=True)
query = 'UPDATE library_sections SET is_active = 0 WHERE server_id != ? OR ' \
'section_id NOT IN ({})'.format(', '.join(['?'] * len(section_ids)))
monitor_db.action(query=query, args=[plexpy.CONFIG.PMS_IDENTIFIER] + section_ids)
@@ -100,14 +102,6 @@ def refresh_libraries():
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', new_keys)
plexpy.CONFIG.write()
#if plexpy.CONFIG.UPDATE_SECTION_IDS == 1 or plexpy.CONFIG.UPDATE_SECTION_IDS == -1:
# # Start library section_id update on it's own thread
# threading.Thread(target=libraries.update_section_ids).start()
#if plexpy.CONFIG.UPDATE_LABELS == 1 or plexpy.CONFIG.UPDATE_LABELS == -1:
# # Start library labels update on it's own thread
# threading.Thread(target=libraries.update_labels).start()
logger.info("Tautulli Libraries :: Libraries list refreshed.")
return True
else:
@@ -115,28 +109,28 @@ def refresh_libraries():
return False
def add_live_tv_library():
if not plexpy.CONFIG.ADD_LIVE_TV_LIBRARY:
def add_live_tv_library(refresh=False):
monitor_db = database.MonitorDatabase()
result = monitor_db.select_single('SELECT * FROM library_sections '
'WHERE section_id = ? and server_id = ?',
[common.LIVE_TV_SECTION_ID, plexpy.CONFIG.PMS_IDENTIFIER])
if result and not refresh or not result and refresh:
return
logger.info("Tautulli Libraries :: Adding Live TV library to the database.")
monitor_db = database.MonitorDatabase()
section_keys = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
'section_id': common.LIVE_TV_SECTION_ID}
section_values = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
'section_id': common.LIVE_TV_SECTION_ID,
'section_name': common.LIVE_TV_SECTION_NAME,
'section_type': 'live'
'section_type': 'live',
'is_active': 1
}
result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values)
if result == 'insert':
plexpy.CONFIG.__setattr__('ADD_LIVE_TV_LIBRARY', 0)
plexpy.CONFIG.write()
def has_library_type(section_type):
monitor_db = database.MonitorDatabase()
@@ -146,152 +140,6 @@ def has_library_type(section_type):
return bool(result)
def update_section_ids():
plexpy.CONFIG.UPDATE_SECTION_IDS = -1
monitor_db = database.MonitorDatabase()
try:
query = 'SELECT id, rating_key, grandparent_rating_key, media_type ' \
'FROM session_history_metadata WHERE section_id IS NULL'
history_results = monitor_db.select(query=query)
query = 'SELECT section_id, section_type FROM library_sections'
library_results = monitor_db.select(query=query)
except Exception as e:
logger.warn("Tautulli Libraries :: Unable to execute database query for update_section_ids: %s." % e)
logger.warn("Tautulli Libraries :: Unable to update section_id's in database.")
plexpy.CONFIG.UPDATE_SECTION_IDS = 1
plexpy.CONFIG.write()
return None
if not history_results:
plexpy.CONFIG.UPDATE_SECTION_IDS = 0
plexpy.CONFIG.write()
return None
logger.debug("Tautulli Libraries :: Updating section_id's in database.")
# Get rating_key: section_id mapping pairs
key_mappings = {}
pms_connect = pmsconnect.PmsConnect()
for library in library_results:
section_id = library['section_id']
section_type = library['section_type']
if section_type != 'photo':
library_children = pms_connect.get_library_children_details(section_id=section_id,
section_type=section_type)
if library_children:
children_list = library_children['children_list']
key_mappings.update({child['rating_key']: child['section_id'] for child in children_list})
else:
logger.warn("Tautulli Libraries :: Unable to get a list of library items for section_id %s." % section_id)
error_keys = set()
for item in history_results:
rating_key = item['grandparent_rating_key'] if item['media_type'] != 'movie' else item['rating_key']
section_id = key_mappings.get(str(rating_key), None)
if section_id:
try:
section_keys = {'id': item['id']}
section_values = {'section_id': section_id}
monitor_db.upsert('session_history_metadata', key_dict=section_keys, value_dict=section_values)
except:
error_keys.add(item['rating_key'])
else:
error_keys.add(item['rating_key'])
if error_keys:
logger.info("Tautulli Libraries :: Updated all section_id's in database except for rating_keys: %s." %
', '.join(str(key) for key in error_keys))
else:
logger.info("Tautulli Libraries :: Updated all section_id's in database.")
plexpy.CONFIG.UPDATE_SECTION_IDS = 0
plexpy.CONFIG.write()
return True
def update_labels():
plexpy.CONFIG.UPDATE_LABELS = -1
monitor_db = database.MonitorDatabase()
try:
query = 'SELECT section_id, section_type FROM library_sections'
library_results = monitor_db.select(query=query)
except Exception as e:
logger.warn("Tautulli Libraries :: Unable to execute database query for update_labels: %s." % e)
logger.warn("Tautulli Libraries :: Unable to update labels in database.")
plexpy.CONFIG.UPDATE_LABELS = 1
plexpy.CONFIG.write()
return None
if not library_results:
plexpy.CONFIG.UPDATE_LABELS = 0
plexpy.CONFIG.write()
return None
logger.debug("Tautulli Libraries :: Updating labels in database.")
# Get rating_key: section_id mapping pairs
key_mappings = {}
pms_connect = pmsconnect.PmsConnect()
for library in library_results:
section_id = library['section_id']
section_type = library['section_type']
if section_type != 'photo':
library_children = []
library_labels = pms_connect.get_library_label_details(section_id=section_id)
if library_labels:
for label in library_labels:
library_children = pms_connect.get_library_children_details(section_id=section_id,
section_type=section_type,
label_key=label['label_key'])
if library_children:
children_list = library_children['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]:
if key_mappings.get(rating_key):
key_mappings[rating_key].append(label['label_title'])
else:
key_mappings[rating_key] = [label['label_title']]
else:
logger.warn("Tautulli Libraries :: Unable to get a list of library items for section_id %s."
% section_id)
error_keys = set()
for rating_key, labels in key_mappings.items():
try:
labels = ';'.join(labels)
monitor_db.action('UPDATE session_history_metadata SET labels = ? '
'WHERE rating_key = ? OR parent_rating_key = ? OR grandparent_rating_key = ? ',
args=[labels, rating_key, rating_key, rating_key])
except:
error_keys.add(rating_key)
if error_keys:
logger.info("Tautulli Libraries :: Updated all labels in database except for rating_keys: %s." %
', '.join(str(key) for key in error_keys))
else:
logger.info("Tautulli Libraries :: Updated all labels in database.")
plexpy.CONFIG.UPDATE_LABELS = 0
plexpy.CONFIG.write()
return True
class Libraries(object):
def __init__(self):
@@ -1149,39 +997,3 @@ class Libraries(object):
return 'Deleted duplicate libraries from the database.'
except Exception as e:
logger.warn("Tautulli Libraries :: Unable to delete duplicate libraries: %s." % e)
def update_libraries_db_notify():
logger.info("Tautulli Libraries :: Upgrading library notification toggles...")
# Set flag first in case something fails we don't want to keep re-adding the notifiers
plexpy.CONFIG.__setattr__('UPDATE_LIBRARIES_DB_NOTIFY', 0)
plexpy.CONFIG.write()
libraries = Libraries()
sections = libraries.get_sections()
for section in sections:
section_details = libraries.get_details(section['section_id'])
if (section_details['do_notify'] == 1 and
(section_details['section_type'] == 'movie' and not plexpy.CONFIG.MOVIE_NOTIFY_ENABLE) or
(section_details['section_type'] == 'show' and not plexpy.CONFIG.TV_NOTIFY_ENABLE) or
(section_details['section_type'] == 'artist' and not plexpy.CONFIG.MUSIC_NOTIFY_ENABLE)):
do_notify = 0
else:
do_notify = section_details['do_notify']
if (section_details['keep_history'] == 1 and
(section_details['section_type'] == 'movie' and not plexpy.CONFIG.MOVIE_LOGGING_ENABLE) or
(section_details['section_type'] == 'show' and not plexpy.CONFIG.TV_LOGGING_ENABLE) or
(section_details['section_type'] == 'artist' and not plexpy.CONFIG.MUSIC_LOGGING_ENABLE)):
keep_history = 0
else:
keep_history = section_details['keep_history']
libraries.set_config(section_id=section_details['section_id'],
custom_thumb=section_details['library_thumb'],
do_notify=do_notify,
keep_history=keep_history,
do_notify_created=section_details['do_notify_created'])

View File

@@ -42,6 +42,11 @@ def set_temp_device_token(token=None):
global TEMP_DEVICE_TOKEN
TEMP_DEVICE_TOKEN = token
if TEMP_DEVICE_TOKEN:
logger._BLACKLIST_WORDS.add(TEMP_DEVICE_TOKEN)
else:
logger._BLACKLIST_WORDS.discard(TEMP_DEVICE_TOKEN)
if TEMP_DEVICE_TOKEN is not None:
global INVALIDATE_TIMER
if INVALIDATE_TIMER:
@@ -82,19 +87,21 @@ def get_mobile_device_by_token(device_token=None):
return get_mobile_devices(device_token=device_token)
def add_mobile_device(device_id=None, device_name=None, device_token=None, friendly_name=None):
def add_mobile_device(device_id=None, device_name=None, device_token=None, friendly_name=None, onesignal_id=None):
db = database.MonitorDatabase()
keys = {'device_id': device_id}
values = {'device_name': device_name,
'device_token': device_token,
'official': validate_device_id(device_id=device_id)}
'onesignal_id': onesignal_id,
'official': validate_onesignal_id(onesignal_id=onesignal_id)}
if friendly_name:
values['friendly_name'] = friendly_name
try:
result = db.upsert(table_name='mobile_devices', key_dict=keys, value_dict=values)
blacklist_logger()
except Exception as e:
logger.warn("Tautulli MobileApp :: Failed to register mobile device in the database: %s." % e)
return
@@ -135,6 +142,7 @@ def set_mobile_device_config(mobile_device_id=None, **kwargs):
try:
db.upsert(table_name='mobile_devices', key_dict=keys, value_dict=values)
logger.info("Tautulli MobileApp :: Updated mobile device agent: mobile_device_id %s." % mobile_device_id)
blacklist_logger()
return True
except Exception as e:
logger.warn("Tautulli MobileApp :: Unable to update mobile device: %s." % e)
@@ -165,11 +173,14 @@ def set_last_seen(device_token=None):
return
def validate_device_id(device_id):
def validate_onesignal_id(onesignal_id):
if onesignal_id is None:
return False
headers = {'Content-Type': 'application/json'}
payload = {'app_id': _ONESIGNAL_APP_ID}
r = requests.get('https://onesignal.com/api/v1/players/{}'.format(device_id), headers=headers, json=payload)
r = requests.get('https://onesignal.com/api/v1/players/{}'.format(onesignal_id), headers=headers, json=payload)
return r.status_code == 200

View File

@@ -17,6 +17,7 @@
from __future__ import unicode_literals
from io import open
import os
from apscheduler.triggers.cron import CronTrigger
@@ -214,7 +215,7 @@ def get_newsletter(newsletter_uuid=None, newsletter_id_name=None):
if newsletter_file in os.listdir(newsletter_folder):
try:
with open(newsletter_file_fp, 'r') as n_file:
with open(newsletter_file_fp, 'r', encoding='utf-8') as n_file:
newsletter = n_file.read()
return newsletter
except OSError as e:

View File

@@ -602,6 +602,9 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
rating_key=plex_web_rating_key)
# Get media IDs from guid and build URLs
if 'plex://' in notify_params['guid']:
notify_params['plex_id'] = notify_params['guid'].split('plex://')[1].split('/')[1]
if 'imdb://' in notify_params['guid']:
notify_params['imdb_id'] = notify_params['guid'].split('imdb://')[1].split('?')[0]
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + notify_params['imdb_id']
@@ -610,30 +613,30 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
if 'thetvdb://' in notify_params['guid']:
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdb://')[1].split('/')[0].split('?')[0]
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?type=show'
elif 'thetvdbdvdorder://' in notify_params['guid']:
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdbdvdorder://')[1].split('/')[0].split('?')[0]
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?type=show'
if 'themoviedb://' in notify_params['guid']:
if notify_params['media_type'] == 'movie':
notify_params['themoviedb_id'] = notify_params['guid'].split('themoviedb://')[1].split('?')[0]
notify_params['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + notify_params['themoviedb_id']
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=movie'
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?type=movie'
elif notify_params['media_type'] in ('show', 'season', 'episode'):
notify_params['themoviedb_id'] = notify_params['guid'].split('themoviedb://')[1].split('/')[0].split('?')[0]
notify_params['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + notify_params['themoviedb_id']
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=show'
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?type=show'
if 'lastfm://' in notify_params['guid']:
notify_params['lastfm_id'] = '/'.join(notify_params['guid'].split('lastfm://')[1].split('?')[0].split('/')[:2])
notify_params['lastfm_url'] = 'https://www.last.fm/music/' + notify_params['lastfm_id']
# Get TheMovieDB info
if plexpy.CONFIG.THEMOVIEDB_LOOKUP:
# Get TheMovieDB info (for movies and tv only)
if plexpy.CONFIG.THEMOVIEDB_LOOKUP and notify_params['media_type'] in ('movie', 'show', 'season', 'episode'):
if notify_params.get('themoviedb_id'):
themoveidb_json = get_themoviedb_info(rating_key=rating_key,
media_type=notify_params['media_type'],
@@ -643,40 +646,64 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
notify_params['imdb_id'] = themoveidb_json['imdb_id']
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + themoveidb_json['imdb_id']
elif notify_params.get('thetvdb_id') or notify_params.get('imdb_id'):
if notify_params['media_type'] in ('episode', 'track'):
elif notify_params.get('thetvdb_id') or notify_params.get('imdb_id') or notify_params.get('plex_id'):
if notify_params['media_type'] == 'episode':
lookup_key = notify_params['grandparent_rating_key']
elif notify_params['media_type'] in ('season', 'album'):
lookup_title = notify_params['grandparent_title']
lookup_year = notify_params['year']
lookup_media_type = 'tv'
elif notify_params['media_type'] == 'season':
lookup_key = notify_params['parent_rating_key']
lookup_title = notify_params['parent_title']
lookup_year = notify_params['year']
lookup_media_type = 'tv'
else:
lookup_key = rating_key
lookup_title = notify_params['title']
lookup_year = notify_params['year']
lookup_media_type = 'tv' if notify_params['media_type'] == 'show' else 'movie'
themoviedb_info = lookup_themoviedb_by_id(rating_key=lookup_key,
thetvdb_id=notify_params.get('thetvdb_id'),
imdb_id=notify_params.get('imdb_id'))
imdb_id=notify_params.get('imdb_id'),
title=lookup_title,
year=lookup_year,
media_type=lookup_media_type)
themoviedb_info.pop('rating_key', None)
notify_params.update(themoviedb_info)
if themoviedb_info.get('imdb_id'):
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + themoviedb_info['imdb_id']
if themoviedb_info.get('themoviedb_id'):
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/{}?type={}'.format(
notify_params['themoviedb_id'], 'show' if lookup_media_type == 'tv' else 'movie')
# Get TVmaze info (for tv shows only)
if plexpy.CONFIG.TVMAZE_LOOKUP:
if notify_params['media_type'] in ('show', 'season', 'episode') and (notify_params.get('thetvdb_id') or notify_params.get('imdb_id')):
if notify_params['media_type'] in ('episode', 'track'):
if plexpy.CONFIG.TVMAZE_LOOKUP and notify_params['media_type'] in ('show', 'season', 'episode'):
if notify_params.get('thetvdb_id') or notify_params.get('imdb_id') or notify_params.get('plex_id'):
if notify_params['media_type'] == 'episode':
lookup_key = notify_params['grandparent_rating_key']
elif notify_params['media_type'] in ('season', 'album'):
lookup_title = notify_params['grandparent_title']
elif notify_params['media_type'] == 'season':
lookup_key = notify_params['parent_rating_key']
lookup_title = notify_params['parent_title']
else:
lookup_key = rating_key
lookup_title = notify_params['title']
tvmaze_info = lookup_tvmaze_by_id(rating_key=lookup_key,
thetvdb_id=notify_params.get('thetvdb_id'),
imdb_id=notify_params.get('imdb_id'))
imdb_id=notify_params.get('imdb_id'),
title=lookup_title)
tvmaze_info.pop('rating_key', None)
notify_params.update(tvmaze_info)
if tvmaze_info.get('thetvdb_id'):
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + str(tvmaze_info['thetvdb_id'])
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/{}' + str(notify_params['thetvdb_id']) + '?type=show'
if tvmaze_info.get('imdb_id'):
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + tvmaze_info['imdb_id']
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
# Get MusicBrainz info (for music only)
if plexpy.CONFIG.MUSICBRAINZ_LOOKUP and notify_params['media_type'] in ('artist', 'album', 'track'):
@@ -982,6 +1009,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'duration': duration,
'poster_title': notify_params['poster_title'],
'poster_url': notify_params['poster_url'],
'plex_id': notify_params['plex_id'],
'plex_url': notify_params['plex_url'],
'imdb_id': notify_params['imdb_id'],
'imdb_url': notify_params['imdb_url'],
@@ -1447,7 +1475,7 @@ def get_hash_image_info(img_hash=None):
return result
def lookup_tvmaze_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
def lookup_tvmaze_by_id(rating_key=None, thetvdb_id=None, imdb_id=None, title=None):
db = database.MonitorDatabase()
try:
@@ -1463,11 +1491,21 @@ def lookup_tvmaze_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
if thetvdb_id:
logger.debug("Tautulli NotificationHandler :: Looking up TVmaze info for thetvdb_id '{}'.".format(thetvdb_id))
else:
elif imdb_id:
logger.debug("Tautulli NotificationHandler :: Looking up TVmaze info for imdb_id '{}'.".format(imdb_id))
else:
logger.debug("Tautulli NotificationHandler :: Looking up TVmaze info for '{}'.".format(title))
params = {'thetvdb': thetvdb_id} if thetvdb_id else {'imdb': imdb_id}
response, err_msg, req_msg = request.request_response2('http://api.tvmaze.com/lookup/shows', params=params)
if thetvdb_id or imdb_id:
params = {'thetvdb': thetvdb_id} if thetvdb_id else {'imdb': imdb_id}
response, err_msg, req_msg = request.request_response2(
'http://api.tvmaze.com/lookup/shows', params=params)
elif title:
params = {'q': title}
response, err_msg, req_msg = request.request_response2(
'https://api.tvmaze.com/singlesearch/shows', params=params)
else:
return tvmaze_info
if response and not err_msg:
tvmaze_json = response.json()
@@ -1497,7 +1535,7 @@ def lookup_tvmaze_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
return tvmaze_info
def lookup_themoviedb_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
def lookup_themoviedb_by_id(rating_key=None, thetvdb_id=None, imdb_id=None, title=None, year=None, media_type=None):
db = database.MonitorDatabase()
try:
@@ -1513,13 +1551,24 @@ def lookup_themoviedb_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
if thetvdb_id:
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for thetvdb_id '{}'.".format(thetvdb_id))
else:
elif imdb_id:
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for imdb_id '{}'.".format(imdb_id))
else:
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for '{} ({})'.".format(title, year))
params = {'api_key': plexpy.CONFIG.THEMOVIEDB_APIKEY,
'external_source': 'tvdb_id' if thetvdb_id else 'imdb_id'
}
response, err_msg, req_msg = request.request_response2('https://api.themoviedb.org/3/find/{}'.format(thetvdb_id or imdb_id), params=params)
params = {'api_key': plexpy.CONFIG.THEMOVIEDB_APIKEY}
if thetvdb_id or imdb_id:
params['external_source'] = 'tvdb_id' if thetvdb_id else 'imdb_id'
response, err_msg, req_msg = request.request_response2(
'https://api.themoviedb.org/3/find/{}'.format(thetvdb_id or imdb_id), params=params)
elif title and year and media_type:
params['query'] = title
params['year'] = year
response, err_msg, req_msg = request.request_response2(
'https://api.themoviedb.org/3/search/{}'.format(media_type), params=params)
else:
return themoviedb_info
if response and not err_msg:
themoviedb_find_json = response.json()
@@ -1527,11 +1576,12 @@ def lookup_themoviedb_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
themoviedb_id = themoviedb_find_json['tv_results'][0]['id']
elif themoviedb_find_json.get('movie_results'):
themoviedb_id = themoviedb_find_json['movie_results'][0]['id']
elif themoviedb_find_json.get('results'):
themoviedb_id = themoviedb_find_json['results'][0]['id']
else:
themoviedb_id = ''
if themoviedb_id:
media_type = 'tv' if thetvdb_id else 'movie'
themoviedb_url = 'https://www.themoviedb.org/{}/{}'.format(media_type, themoviedb_id)
themoviedb_json = get_themoviedb_info(rating_key=rating_key,
media_type=media_type,

View File

@@ -929,12 +929,13 @@ class ANDROIDAPP(Notifier):
#logger.debug("Salt (base64): {}".format(base64.b64encode(salt)))
payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
'include_player_ids': [self.config['device_id']],
'include_player_ids': [device['onesignal_id']],
'contents': {'en': 'Tautulli Notification'},
'data': {'encrypted': True,
'cipher_text': base64.b64encode(encrypted_data),
'nonce': base64.b64encode(nonce),
'salt': base64.b64encode(salt)}
'salt': base64.b64encode(salt),
'server_id': plexpy.CONFIG.PMS_UUID}
}
else:
logger.warn("Tautulli Notifiers :: PyCryptodome library is missing. "
@@ -942,10 +943,11 @@ class ANDROIDAPP(Notifier):
"Install the library to encrypt the notifications.")
payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
'include_player_ids': [self.config['device_id']],
'include_player_ids': [device['onesignal_id']],
'contents': {'en': 'Tautulli Notification'},
'data': {'encrypted': False,
'plain_text': plaintext_data}
'plain_text': plaintext_data,
'server_id': plexpy.CONFIG.PMS_UUID}
}
#logger.debug("OneSignal payload: {}".format(payload))
@@ -958,7 +960,8 @@ class ANDROIDAPP(Notifier):
db = database.MonitorDatabase()
try:
query = 'SELECT * FROM mobile_devices WHERE official = 1'
query = 'SELECT * FROM mobile_devices WHERE official = 1 ' \
'AND onesignal_id IS NOT NULL AND onesignal_id != ""'
result = db.select(query=query)
except Exception as e:
logger.warn("Tautulli Notifiers :: Unable to retrieve Android app devices list: %s." % e)
@@ -983,9 +986,9 @@ class ANDROIDAPP(Notifier):
'The content of your notifications will be sent unencrypted!</strong><br>'
'Please install the library to encrypt the notification contents. '
'Instructions can be found in the '
'<a href="https://github.com/%s/%s-Wiki/wiki/'
'Frequently-Asked-Questions#notifications-pycryptodome'
'" target="_blank">FAQ</a>.' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO),
'<a href="' + helpers.anon_url(
'https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions#notifications-pycryptodome'
% (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO)) + '" target="_blank">FAQ</a>.' ,
'input_type': 'help'
})
else:
@@ -998,7 +1001,7 @@ class ANDROIDAPP(Notifier):
config_option[-1]['description'] += '<br><br>Notifications are sent using the ' \
'<a href="' + helpers.anon_url('https://onesignal.com') + '" target="_blank">' \
'OneSignal</a> API. Some user data is collected and cannot be encrypted. ' \
'OneSignal</a>. Some user data is collected and cannot be encrypted. ' \
'Please read the <a href="' + helpers.anon_url(
'https://onesignal.com/privacy_policy') + '" target="_blank">' \
'OneSignal Privacy Policy</a> for more details.'
@@ -1008,7 +1011,7 @@ class ANDROIDAPP(Notifier):
if not devices:
config_option.append({
'label': 'Device',
'description': 'No devices registered. '
'description': 'No mobile devices registered with OneSignal. '
'<a data-tab-destination="android_app" data-toggle="tab" data-dismiss="modal">'
'Get the Android App</a> and register a device.',
'input_type': 'help'
@@ -1018,7 +1021,7 @@ class ANDROIDAPP(Notifier):
'label': 'Device',
'value': self.config['device_id'],
'name': 'androidapp_device_id',
'description': 'Set your Android app device or '
'description': 'Set your mobile device or '
'<a data-tab-destination="android_app" data-toggle="tab" data-dismiss="modal">'
'register a new device</a> with Tautulli.',
'input_type': 'select',
@@ -1399,8 +1402,7 @@ class EMAIL(Notifier):
success = True
except Exception as e:
logger.error("Tautulli Notifiers :: {name} notification failed: {e}".format(
name=self.NAME, e=str(e).decode('utf-8')))
logger.error("Tautulli Notifiers :: %s notification failed: %s", self.NAME, e)
finally:
if mailserver:
@@ -2964,21 +2966,26 @@ class SCRIPTS(Notifier):
def __init__(self, config=None):
super(SCRIPTS, self).__init__(config=config)
self.script_exts = {'.bat': '',
'.cmd': '',
'.exe': '',
'.php': 'php',
'.pl': 'perl',
'.ps1': 'powershell -executionPolicy bypass -file',
'.py': 'python',
'.pyw': 'pythonw',
'.rb': 'ruby',
'.sh': ''
}
self.script_exts = {
'.bat': '',
'.cmd': '',
'.php': 'php',
'.pl': 'perl',
'.ps1': 'powershell -executionPolicy bypass -file',
'.py': 'python' if plexpy.FROZEN else sys.executable,
'.pyw': 'pythonw',
'.rb': 'ruby',
'.sh': ''
}
self.pythonpath_override = 'nopythonpath'
self.pythonpath = True
self.prefix_overrides = ('python2', 'python3', 'python', 'pythonw', 'php', 'ruby', 'perl')
self.prefix_overrides = {
'python': ['.py'],
'python2': ['.py'],
'python3': ['.py'],
'pythonw': ['.py', '.pyw']
}
self.script_killed = False
def list_scripts(self):
@@ -3008,7 +3015,7 @@ class SCRIPTS(Notifier):
'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT,
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY,
'TAUTULLI_ENCODING': plexpy.SYS_ENCODING,
'TAUTULLI_PYTHON_VERSION': '.'.join(map(str, plexpy.PYTHON_VERSION))
'TAUTULLI_PYTHON_VERSION': common.PYTHON_VERSION
}
if user_id:
@@ -3081,21 +3088,24 @@ class SCRIPTS(Notifier):
logger.error("Tautulli Notifiers :: No script folder specified.")
return
script_args = helpers.split_args(kwargs.get('script_args', subject))
logger.debug("Tautulli Notifiers :: Trying to run notify script, action: %s, arguments: %s"
% (action, script_args))
script = kwargs.get('script', self.config.get('script', ''))
script_args = helpers.split_args(kwargs.get('script_args', subject))
user_id = kwargs.get('parameters', {}).get('user_id')
logger.debug("Tautulli Notifiers :: Trying to run notify script: %s, arguments: %s, action: %s"
% (script, script_args, action))
# Don't try to run the script if the action does not have one
if action and not script:
logger.debug("Tautulli Notifiers :: No script selected for action %s, exiting..." % action)
logger.debug("Tautulli Notifiers :: No script selected for action '%s', exiting..." % action)
return
elif not script:
logger.debug("Tautulli Notifiers :: No script selected, exiting...")
return
# Check for a valid script file
elif not os.path.isfile(script) or not script.endswith(tuple(self.script_exts)):
logger.error("Tautulli Notifiers :: Invalid script file '%s' specified, exiting..." % script)
return
name, ext = os.path.splitext(script)
prefix = self.script_exts.get(ext, '')
@@ -3112,10 +3122,14 @@ class SCRIPTS(Notifier):
del script_args[0]
# Allow overrides for shitty systems
if prefix and script_args:
if script_args[0] in self.prefix_overrides:
if prefix and script_args and script_args[0] in self.prefix_overrides:
if ext in self.prefix_overrides[script_args[0]]:
script[0] = script_args[0]
del script_args[0]
else:
logger.error("Tautulli Notifiers :: Invalid prefix override '%s' for '%s' script, exiting..."
% (script_args[0], ext))
return
script.extend(script_args)
@@ -3817,129 +3831,6 @@ class ZAPIER(Notifier):
return config_option
def upgrade_config_to_db():
logger.info("Tautulli Notifiers :: Upgrading to new notification system...")
# Set flag first in case something fails we don't want to keep re-adding the notifiers
plexpy.CONFIG.__setattr__('UPDATE_NOTIFIERS_DB', 0)
plexpy.CONFIG.write()
# Config section names from the {new: old} config
section_overrides = {'xbmc': 'XBMC',
'nma': 'NMA',
'pushbullet': 'PushBullet',
'osx': 'OSX_Notify',
'ifttt': 'IFTTT'
}
# Config keys from the {new: old} config
config_key_overrides = {'plex': {'hosts': 'client_host'},
'facebook': {'access_token': 'token',
'group_id': 'group',
'incl_poster': 'incl_card'},
'join': {'api_key': 'apikey',
'device_id': 'deviceid'},
'hipchat': {'hook': 'url',
'incl_poster': 'incl_card'},
'nma': {'api_key': 'apikey'},
'osx': {'notify_app': 'app'},
'prowl': {'key': 'keys'},
'pushalot': {'api_key': 'apikey'},
'pushbullet': {'api_key': 'apikey',
'device_id': 'deviceid'},
'pushover': {'api_token': 'apitoken',
'key': 'keys'},
'scripts': {'script_folder': 'folder'},
'slack': {'incl_poster': 'incl_card'}
}
# Get Monitoring config section
monitoring = plexpy.CONFIG._config['Monitoring']
# Get the new default notification subject and body text
defualt_subject_text = {a['name']: a['subject'] for a in available_notification_actions()}
defualt_body_text = {a['name']: a['body'] for a in available_notification_actions()}
# Get the old notification subject and body text
notify_text = {}
for action in get_notify_actions():
subject_key = 'notify_' + action + '_subject_text'
body_key = 'notify_' + action + '_body_text'
notify_text[action + '_subject'] = monitoring.get(subject_key, defualt_subject_text[action])
notify_text[action + '_body'] = monitoring.get(body_key, defualt_body_text[action])
# Check through each notification agent
for agent in get_notify_agents():
agent_id = AGENT_IDS[agent]
# Get the old config section for the agent
agent_section = section_overrides.get(agent, agent.capitalize())
agent_config = plexpy.CONFIG._config.get(agent_section)
agent_config_key = agent_section.lower()
# Make sure there is an existing config section (to prevent adding v2 agents)
if not agent_config:
continue
# Get all the actions for the agent
agent_actions = {}
for action in get_notify_actions():
a_key = agent_config_key + '_' + action
agent_actions[action] = helpers.cast_to_int(agent_config.get(a_key, 0))
# Check if any of the actions were enabled
# If so, the agent will be added to the database
if any(agent_actions.values()):
# Get the new default config for the agent
notifier_default_config = get_agent_class(agent_id).config
# Update the new config with the old config values
notifier_config = {}
for conf, val in notifier_default_config.items():
c_key = agent_config_key + '_' + config_key_overrides.get(agent, {}).get(conf, conf)
notifier_config[agent + '_' + conf] = agent_config.get(c_key, val)
# Special handling for scripts - one script with multiple actions
if agent == 'scripts':
# Get the old script arguments
script_args = monitoring.get('notify_scripts_args_text', '')
# Get the old scripts for each action
action_scripts = {}
for action in get_notify_actions():
s_key = agent + '_' + action + '_script'
action_scripts[action] = agent_config.get(s_key, '')
# Reverse the dict to {script: [actions]}
script_actions = {}
for k, v in action_scripts.items():
if v: script_actions.setdefault(v, set()).add(k)
# Add a new script notifier for each script if the action was enabled
for script, actions in script_actions.items():
if any(agent_actions[a] for a in actions):
temp_config = notifier_config
temp_config.update({a: 0 for a in agent_actions})
temp_config.update({a + '_subject': '' for a in agent_actions})
for a in actions:
if agent_actions[a]:
temp_config[a] = agent_actions[a]
temp_config[a + '_subject'] = script_args
temp_config[agent + '_script'] = script
# Add a new notifier and update the config
notifier_id = add_notifier_config(agent_id=agent_id)
set_notifier_config(notifier_id=notifier_id, agent_id=agent_id, **temp_config)
else:
notifier_config.update(agent_actions)
notifier_config.update(notify_text)
# Add a new notifier and update the config
notifier_id = add_notifier_config(agent_id=agent_id)
set_notifier_config(notifier_id=notifier_id, agent_id=agent_id, **notifier_config)
def check_browser_enabled():
global BROWSER_NOTIFIERS
BROWSER_NOTIFIERS = {}

View File

@@ -2971,8 +2971,6 @@ class PmsConnect(object):
return key_list
def get_server_response(self):
# Refresh Plex remote access port mapping first
self.put_refresh_reachability()
account_data = self.get_account(output_format='xml')
try:

View File

@@ -295,20 +295,20 @@ def server_message(response, return_msg=False):
try:
soup = BeautifulSoup(response.content, "html5lib")
except Exception:
pass
soup = None
# Find body and cleanup common tags to grab content, which probably
# contains the message.
message = soup.find("body")
elements = ("header", "script", "footer", "nav", "input", "textarea")
if soup:
# Find body and cleanup common tags to grab content, which probably
# contains the message.
message = soup.find("body")
elements = ("header", "script", "footer", "nav", "input", "textarea")
for element in elements:
for element in elements:
for tag in soup.find_all(element):
tag.replaceWith("")
for tag in soup.find_all(element):
tag.replaceWith("")
message = message.text if message else soup.text
message = message.strip()
message = message.text if message else soup.text
message = message.strip()
# Second attempt is to just take the response
if message is None:

View File

@@ -245,6 +245,7 @@ class Users(object):
custom_where = ['users.user_id', user_id]
columns = ['session_history.id AS history_row_id',
'MIN(session_history.started) AS first_seen',
'MAX(session_history.started) AS last_seen',
'session_history.ip_address',
'COUNT(session_history.id) AS play_count',
@@ -306,6 +307,7 @@ class Users(object):
row = {'history_row_id': item['history_row_id'],
'last_seen': item['last_seen'],
'first_seen': item['first_seen'],
'ip_address': item['ip_address'],
'play_count': item['play_count'],
'platform': platform,

View File

@@ -17,5 +17,5 @@
from __future__ import unicode_literals
PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.5.2-beta"
PLEXPY_BRANCH = "master"
PLEXPY_RELEASE_VERSION = "v2.5.4"

View File

@@ -176,7 +176,7 @@ def run():
logger.info("Tautulli WebSocket :: Ready")
plexpy.WS_CONNECTED = True
except (websocket.WebSocketException, IOError, Exception) as e:
logger.error("Tautulli WebSocket :: %s." % e)
logger.error("Tautulli WebSocket :: %s.", e)
if plexpy.WS_CONNECTED:
on_connect()
@@ -209,7 +209,7 @@ def run():
logger.info("Tautulli WebSocket :: Ready")
plexpy.WS_CONNECTED = True
except (websocket.WebSocketException, IOError, Exception) as e:
logger.error("Tautulli WebSocket :: %s." % e)
logger.error("Tautulli WebSocket :: %s.", e)
else:
close()
@@ -219,7 +219,7 @@ def run():
if ws_shutdown:
break
logger.error("Tautulli WebSocket :: %s." % e)
logger.error("Tautulli WebSocket :: %s.", e)
close()
break
@@ -255,42 +255,55 @@ def process(opcode, data):
try:
data = data.decode('utf-8')
logger.websocket_debug(data)
info = json.loads(data)
event = json.loads(data)
except Exception as e:
logger.warn("Tautulli WebSocket :: Error decoding message from websocket: %s" % e)
logger.websocket_error(data)
return False
info = info.get('NotificationContainer', info)
info_type = info.get('type')
event = event.get('NotificationContainer', event)
event_type = event.get('type')
if not info_type:
if not event_type:
return False
if info_type == 'playing':
time_line = info.get('PlaySessionStateNotification', info.get('_children', {}))
if event_type == 'playing':
event_data = event.get('PlaySessionStateNotification', event.get('_children', {}))
if not time_line:
logger.debug("Tautulli WebSocket :: Session found but unable to get timeline data.")
if not event_data:
logger.debug("Tautulli WebSocket :: Session event found but unable to get websocket data.")
return False
try:
activity = activity_handler.ActivityHandler(timeline=time_line[0])
activity = activity_handler.ActivityHandler(timeline=event_data[0])
activity.process()
except Exception as e:
logger.exception("Tautulli WebSocket :: Failed to process session data: %s." % e)
if info_type == 'timeline':
time_line = info.get('TimelineEntry', info.get('_children', {}))
if event_type == 'timeline':
event_data = event.get('TimelineEntry', event.get('_children', {}))
if not time_line:
logger.debug("Tautulli WebSocket :: Timeline event found but unable to get timeline data.")
if not event_data:
logger.debug("Tautulli WebSocket :: Timeline event found but unable to get websocket data.")
return False
try:
activity = activity_handler.TimelineHandler(timeline=time_line[0])
activity = activity_handler.TimelineHandler(timeline=event_data[0])
activity.process()
except Exception as e:
logger.exception("Tautulli WebSocket :: Failed to process timeline data: %s." % e)
if event_type == 'reachability':
event_data = event.get('ReachabilityNotification', event.get('_children', {}))
if not event_data:
logger.debug("Tautulli WebSocket :: Reachability event found but unable to get websocket data.")
return False
try:
activity = activity_handler.ReachabilityHandler(data=event_data[0])
activity.process()
except Exception as e:
logger.exception("Tautulli WebSocket :: Failed to process reachability data: %s." % e)
return True

View File

@@ -1378,8 +1378,8 @@ class WebInterface(object):
user_id (str): The id of the Plex user
Optional parameters:
order_column (str): "last_seen", "ip_address", "platform", "player",
"last_played", "play_count"
order_column (str): "last_seen", "first_seen", "ip_address", "platform",
"player", "last_played", "play_count"
order_dir (str): "desc" or "asc"
start (int): Row to start from, 0
length (int): Number of items to return, 25
@@ -1397,6 +1397,7 @@ class WebInterface(object):
"ip_address": "xxx.xxx.xxx.xxx",
"last_played": "Game of Thrones - The Red Woman",
"last_seen": 1462591869,
"first_seen": 1583968210,
"live": 0,
"media_index": 1,
"media_type": "episode",
@@ -1423,6 +1424,7 @@ class WebInterface(object):
if not kwargs.get('json_data'):
# TODO: Find some one way to automatically get the columns
dt_columns = [("last_seen", True, False),
("first_seen", True, False),
("ip_address", True, True),
("platform", True, True),
("player", True, True),
@@ -2993,13 +2995,7 @@ class WebInterface(object):
"time_format": plexpy.CONFIG.TIME_FORMAT,
"week_start_monday": checked(plexpy.CONFIG.WEEK_START_MONDAY),
"get_file_sizes": checked(plexpy.CONFIG.GET_FILE_SIZES),
"grouping_global_history": checked(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY),
"grouping_user_history": checked(plexpy.CONFIG.GROUPING_USER_HISTORY),
"grouping_charts": checked(plexpy.CONFIG.GROUPING_CHARTS),
"monitor_pms_updates": checked(plexpy.CONFIG.MONITOR_PMS_UPDATES),
"monitor_remote_access": checked(plexpy.CONFIG.MONITOR_REMOTE_ACCESS),
"remote_access_ping_interval": plexpy.CONFIG.REMOTE_ACCESS_PING_INTERVAL,
"remote_access_ping_threshold": plexpy.CONFIG.REMOTE_ACCESS_PING_THRESHOLD,
"refresh_libraries_interval": plexpy.CONFIG.REFRESH_LIBRARIES_INTERVAL,
"refresh_libraries_on_startup": checked(plexpy.CONFIG.REFRESH_LIBRARIES_ON_STARTUP),
"refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL,
@@ -3072,12 +3068,12 @@ class WebInterface(object):
checked_configs = [
"launch_browser", "launch_startup", "enable_https", "https_create_cert",
"api_enabled", "freeze_db", "check_github",
"grouping_global_history", "grouping_user_history", "grouping_charts", "group_history_tables",
"group_history_tables",
"pms_url_manual", "week_start_monday",
"refresh_libraries_on_startup", "refresh_users_on_startup",
"notify_consecutive", "notify_recently_added_upgrade",
"notify_group_recently_added_grandparent", "notify_group_recently_added_parent",
"monitor_pms_updates", "monitor_remote_access", "get_file_sizes", "log_blacklist", "http_hash_password",
"monitor_pms_updates", "get_file_sizes", "log_blacklist", "http_hash_password",
"allow_guest_access", "cache_images", "http_proxy", "http_basic_auth", "notify_concurrent_by_ip",
"history_table_activity", "plexpy_auto_update",
"themoviedb_lookup", "tvmaze_lookup", "musicbrainz_lookup", "http_plex_admin",
@@ -3130,8 +3126,6 @@ class WebInterface(object):
kwargs.get('refresh_users_interval') != str(plexpy.CONFIG.REFRESH_USERS_INTERVAL) or \
kwargs.get('pms_update_check_interval') != str(plexpy.CONFIG.PMS_UPDATE_CHECK_INTERVAL) or \
kwargs.get('monitor_pms_updates') != plexpy.CONFIG.MONITOR_PMS_UPDATES or \
kwargs.get('monitor_remote_access') != plexpy.CONFIG.MONITOR_REMOTE_ACCESS or \
kwargs.get('remote_access_ping_interval') != str(plexpy.CONFIG.REMOTE_ACCESS_PING_INTERVAL) or \
kwargs.get('pms_url_manual') != plexpy.CONFIG.PMS_URL_MANUAL:
reschedule = True
@@ -3756,7 +3750,7 @@ class WebInterface(object):
app (str): "tautulli" or "plexwatch" or "plexivity"
database_file (file): The database file to import (multipart/form-data)
or
database_path (str): The full path to the plexwatch database file
database_path (str): The full path to the database file to import
method (str): For Tautulli only, "merge" or "overwrite"
table_name (str): For PlexWatch or Plexivity only, "processed" or "grouped"
@@ -3770,7 +3764,7 @@ class WebInterface(object):
Returns:
json:
{"result": "success",
"message": "Import has started. Check the logs to monitor any problems."
"message": "Database import has started. Check the logs to monitor any problems."
}
```
"""
@@ -3799,7 +3793,7 @@ class WebInterface(object):
'method': method,
'backup': helpers.bool_true(backup)}).start()
return {'result': 'success',
'message': 'Import has started. Check the logs to monitor any problems.'}
'message': 'Database import has started. Check the logs to monitor any problems.'}
else:
if database_file:
helpers.delete_file(database_path)
@@ -3814,7 +3808,7 @@ class WebInterface(object):
'table_name': table_name,
'import_ignore_interval': import_ignore_interval}).start()
return {'result': 'success',
'message': 'Import has started. Check the logs to monitor any problems.'}
'message': 'Database import has started. Check the logs to monitor any problems.'}
else:
if database_file:
helpers.delete_file(database_path)
@@ -3829,7 +3823,7 @@ class WebInterface(object):
'table_name': table_name,
'import_ignore_interval': import_ignore_interval}).start()
return {'result': 'success',
'message': 'Import has started. Check the logs to monitor any problems.'}
'message': 'Database import has started. Check the logs to monitor any problems.'}
else:
if database_file:
helpers.delete_file(database_path)
@@ -3838,6 +3832,56 @@ class WebInterface(object):
else:
return {'result': 'error', 'message': 'App not recognized for import'}
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def import_config(self, config_file=None, config_path=None, backup=False, **kwargs):
""" Import a Tautulli config file.
```
Required parameters:
config_file (file): The config file to import (multipart/form-data)
or
config_path (str): The full path to the config file to import
Optional parameters:
backup (bool): true or false whether to backup
the current config before importing
Returns:
json:
{"result": "success",
"message": "Config import has started. Check the logs to monitor any problems. "
"Tautulli will restart automatically."
}
```
"""
if database.IS_IMPORTING:
return {'result': 'error',
'message': 'Database import is in progress. Please wait until it is finished to import a config.'}
if config_file:
config_path = os.path.join(plexpy.CONFIG.CACHE_DIR, config_file.filename + '.import.ini')
logger.info("Received config file '%s' for import. Saving to cache '%s'.",
config_file.filename, config_path)
with open(config_path, 'wb') as f:
while True:
data = config_file.file.read(8192)
if not data:
break
f.write(data)
if not config_path:
return {'result': 'error', 'message': 'No config specified for import'}
config.set_import_thread(config=config_path, backup=helpers.bool_true(backup))
return {'result': 'success',
'message': 'Config import has started. Check the logs to monitor any problems. '
'Tautulli will restart automatically.'}
@cherrypy.expose
@requireAuth(member_of("admin"))
def import_database_tool(self, app=None, **kwargs):
@@ -3851,6 +3895,11 @@ class WebInterface(object):
logger.warn("No app specified for import.")
return
@cherrypy.expose
@requireAuth(member_of("admin"))
def import_config_tool(self, **kwargs):
return serve_template(templatename="config_import.html", title="Import Tautulli Configuration")
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@@ -4113,7 +4162,8 @@ class WebInterface(object):
def do_state_change(self, signal, title, timer, **kwargs):
message = title
quote = self.random_arnold_quotes()
plexpy.SIGNAL = signal
if signal:
plexpy.SIGNAL = signal
if plexpy.CONFIG.HTTP_ROOT.strip('/'):
new_http_root = '/' + plexpy.CONFIG.HTTP_ROOT.strip('/') + '/'
@@ -4162,6 +4212,13 @@ class WebInterface(object):
def reset_git_install(self, **kwargs):
return self.do_state_change('reset', 'Resetting to {}'.format(common.RELEASE), 120)
@cherrypy.expose
@requireAuth(member_of("admin"))
def restart_import_config(self, **kwargs):
if config.IMPORT_THREAD:
config.IMPORT_THREAD.start()
return self.do_state_change(None, 'Importing a Config', 15)
@cherrypy.expose
@requireAuth(member_of("admin"))
def get_changelog(self, latest_only=False, since_prev_release=False, update_shown=False, **kwargs):

View File

@@ -264,7 +264,7 @@ def initialize(options):
cherrypy.engine.start()
cherrypy.engine.block()
except IOError:
sys.stderr.write('Failed to start on port: %i. Is something else running?\n' % (options['http_port']))
logger.error("Tautulli WebStart :: Failed to start on port: %i. Is something else running?" % options['http_port'])
sys.exit(1)
cherrypy.server.wait()

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
if [[ "$TAUTULLI_DOCKER" = "True" ]]; then
if [[ -v PUID && -v PGID ]]; then
if [[ "$TAUTULLI_DOCKER" == "True" ]]; then
if [[ -n $PUID && -n $PGID ]]; then
getent group "$PGID" 2>&1 > /dev/null || groupadd -g "$PGID" tautulli
getent passwd "$PUID" 2>&1 > /dev/null || useradd -r -u "$PUID" -g "$PGID" tautulli
@@ -14,8 +14,20 @@ if [[ "$TAUTULLI_DOCKER" = "True" ]]; then
echo "Running Tautulli using user $user (uid=$PUID) and group $group (gid=$PGID)"
su "$user" -g "$group" -c "python /app/Tautulli.py --datadir /config"
else
python Tautulli.py --datadir /config
python Tautulli.py --datadir /config
fi
else
python Tautulli.py &> /dev/null &
python_versions=("python3" "python3.8" "python3.7" "python3.6" "python" "python2" "python2.7")
for cmd in "${python_versions[@]}"; do
if command -v "$cmd" >/dev/null; then
echo "Starting Tautulli with $cmd."
if [[ "$(uname -s)" == "Darwin" ]]; then
$cmd Tautulli.py &> /dev/null &
else
$cmd Tautulli.py --quiet --daemon
fi
exit
fi
done
echo "Unable to start Tautulli. No Python interpreter was found in the following options:" "${python_versions[@]}"
fi