Compare commits
92 Commits
v2.5.2-bet
...
v2.5.4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
21dec5feb3 | ||
![]() |
bee4106af0 | ||
![]() |
bbb6e46515 | ||
![]() |
570ebb4f73 | ||
![]() |
d93204af4e | ||
![]() |
702f116db9 | ||
![]() |
0c8607b3ec | ||
![]() |
3a2cc6efc7 | ||
![]() |
1b37ff1655 | ||
![]() |
769934c8a5 | ||
![]() |
7f1a4ec34a | ||
![]() |
27438f7915 | ||
![]() |
8651bef9c1 | ||
![]() |
36324d10dc | ||
![]() |
0272c35047 | ||
![]() |
70c0f912e2 | ||
![]() |
59a6acc088 | ||
![]() |
10b0726727 | ||
![]() |
8f1360d7c2 | ||
![]() |
e0e5ac9ecc | ||
![]() |
c814f219a2 | ||
![]() |
9095fc0c7a | ||
![]() |
a675202537 | ||
![]() |
b52ab4885b | ||
![]() |
43e26c9b56 | ||
![]() |
703a7feed2 | ||
![]() |
7b69ed4cec | ||
![]() |
fcca7f969e | ||
![]() |
ec34ea2116 | ||
![]() |
3dc36c3b92 | ||
![]() |
f0d4fd5523 | ||
![]() |
7fe6c72fe2 | ||
![]() |
d216d0f27f | ||
![]() |
43a7758acd | ||
![]() |
3043956dec | ||
![]() |
06665fdd06 | ||
![]() |
beff5caaac | ||
![]() |
3859412b2c | ||
![]() |
f7ec476fc0 | ||
![]() |
b97d32671d | ||
![]() |
01c56ef280 | ||
![]() |
b9422312f3 | ||
![]() |
9a0f83c3e7 | ||
![]() |
fbfedb2e62 | ||
![]() |
4f8a462041 | ||
![]() |
141d043a6a | ||
![]() |
c1266fed12 | ||
![]() |
4a4be9798d | ||
![]() |
172692ccca | ||
![]() |
50e7c0469f | ||
![]() |
44f74e3590 | ||
![]() |
63656b73c2 | ||
![]() |
40ecf56904 | ||
![]() |
b4a10adec2 | ||
![]() |
1698622d63 | ||
![]() |
fa27271647 | ||
![]() |
d837811c68 | ||
![]() |
ad195f0969 | ||
![]() |
4a8748e322 | ||
![]() |
0f016c83ea | ||
![]() |
061ae44da4 | ||
![]() |
a8b90bf100 | ||
![]() |
eb3cd49bc4 | ||
![]() |
416d869288 | ||
![]() |
a116c26c25 | ||
![]() |
cc4ec53dac | ||
![]() |
63164c7ff5 | ||
![]() |
9815c014e8 | ||
![]() |
69675151bf | ||
![]() |
99e395ddfa | ||
![]() |
7fe1e542df | ||
![]() |
938134081b | ||
![]() |
3fd2234a92 | ||
![]() |
41843dc573 | ||
![]() |
cc6bd528a5 | ||
![]() |
2625ef5fb9 | ||
![]() |
dbd2d28877 | ||
![]() |
f70f814c70 | ||
![]() |
6710e42134 | ||
![]() |
78c5b45e43 | ||
![]() |
e562ec96fa | ||
![]() |
9b5e01c319 | ||
![]() |
0097532f4a | ||
![]() |
91935c9018 | ||
![]() |
83df807f7e | ||
![]() |
eb3db20340 | ||
![]() |
6dab6194ea | ||
![]() |
356f64cac0 | ||
![]() |
f77f289125 | ||
![]() |
280257477a | ||
![]() |
660141cb16 | ||
![]() |
cd8a899521 |
16
.github/workflows/publish-release.yml
vendored
16
.github/workflows/publish-release.yml
vendored
@@ -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
24
API.md
@@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
66
CHANGELOG.md
66
CHANGELOG.md
@@ -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.
|
||||
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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."
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Parameter check
|
||||
if [ -z "$1" ]; then
|
||||
|
@@ -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');
|
||||
|
@@ -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> 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.
|
||||
|
138
data/interfaces/default/config_import.html
Normal file
138
data/interfaces/default/config_import.html
Normal 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> 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> " + data.message;
|
||||
window.location.href = 'restart_import_config';
|
||||
} else {
|
||||
msg = "<i class='fa fa-exclamation-triangle'></i> " + 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> 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>
|
@@ -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);
|
||||
}
|
||||
|
1
data/interfaces/default/images/rating/themoviedb.svg
Normal file
1
data/interfaces/default/images/rating/themoviedb.svg
Normal 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 |
@@ -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>
|
||||
|
@@ -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>';
|
||||
}
|
||||
|
@@ -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>';
|
||||
}
|
||||
|
@@ -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 + ' ' + 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%",
|
||||
|
@@ -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>';
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -121,6 +121,7 @@ DOCUMENTATION :: END
|
||||
});
|
||||
|
||||
$('#api_qr_address').change(function () {
|
||||
this.value = $.trim(this.value);
|
||||
var url = $(this).val();
|
||||
checkQRAddress(url);
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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> 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>
|
||||
|
@@ -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> 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> Tautulli is ${message.lower()}</h3>
|
||||
<br />
|
||||
<h4>Restart in <span class="countdown"></span></h4>
|
||||
% endif
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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}"
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
)
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
|
@@ -1,4 +1,4 @@
|
||||
pyinstaller
|
||||
pyopenssl
|
||||
pycryptodomex
|
||||
pyobjc
|
||||
pyobjc-framework-Cocoa
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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.'},
|
||||
|
617
plexpy/config.py
617
plexpy/config.py
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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'])
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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,
|
||||
|
@@ -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 = {}
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
@@ -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,
|
||||
|
@@ -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"
|
@@ -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
|
||||
|
@@ -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):
|
||||
|
@@ -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()
|
||||
|
20
start.sh
20
start.sh
@@ -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
|
||||
|
Reference in New Issue
Block a user