Compare commits
67 Commits
v2.5.0-bet
...
v2.5.3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1698622d63 | ||
![]() |
fa27271647 | ||
![]() |
d837811c68 | ||
![]() |
ad195f0969 | ||
![]() |
4a8748e322 | ||
![]() |
0f016c83ea | ||
![]() |
061ae44da4 | ||
![]() |
a8b90bf100 | ||
![]() |
eb3cd49bc4 | ||
![]() |
416d869288 | ||
![]() |
a116c26c25 | ||
![]() |
cc4ec53dac | ||
![]() |
63164c7ff5 | ||
![]() |
9815c014e8 | ||
![]() |
41843dc573 | ||
![]() |
cc6bd528a5 | ||
![]() |
2625ef5fb9 | ||
![]() |
dbd2d28877 | ||
![]() |
f70f814c70 | ||
![]() |
6710e42134 | ||
![]() |
78c5b45e43 | ||
![]() |
e562ec96fa | ||
![]() |
9b5e01c319 | ||
![]() |
0097532f4a | ||
![]() |
91935c9018 | ||
![]() |
83df807f7e | ||
![]() |
eb3db20340 | ||
![]() |
6dab6194ea | ||
![]() |
356f64cac0 | ||
![]() |
f77f289125 | ||
![]() |
280257477a | ||
![]() |
660141cb16 | ||
![]() |
cd8a899521 | ||
![]() |
cb577c51b8 | ||
![]() |
1c395ab10c | ||
![]() |
07d7170e49 | ||
![]() |
88e23627fd | ||
![]() |
48f846da40 | ||
![]() |
ff887d9948 | ||
![]() |
617b0d6fd9 | ||
![]() |
805d45bd33 | ||
![]() |
fef428202f | ||
![]() |
40fd82febd | ||
![]() |
45f0001da5 | ||
![]() |
c7a3e1e3bf | ||
![]() |
9dd8cc9e49 | ||
![]() |
d252d4cd2d | ||
![]() |
bc1328040c | ||
![]() |
82919d3c1d | ||
![]() |
7c801c2f5e | ||
![]() |
9a932aea12 | ||
![]() |
5696e75abe | ||
![]() |
efb3f748c2 | ||
![]() |
450b3865a8 | ||
![]() |
970667adca | ||
![]() |
89307dad01 | ||
![]() |
451feda86b | ||
![]() |
4d241fac48 | ||
![]() |
4390f5cbc8 | ||
![]() |
7f9d46eac3 | ||
![]() |
d0f28883aa | ||
![]() |
48203e64a9 | ||
![]() |
42b17ca495 | ||
![]() |
d8080fe506 | ||
![]() |
be910e24f7 | ||
![]() |
ce6d70f6fd | ||
![]() |
827e05e4d7 |
@@ -7,3 +7,4 @@ package
|
|||||||
pylintrc
|
pylintrc
|
||||||
*.md
|
*.md
|
||||||
!CHANGELOG*.md
|
!CHANGELOG*.md
|
||||||
|
start.bat
|
||||||
|
22
.github/workflows/publish-docker.yml
vendored
22
.github/workflows/publish-docker.yml
vendored
@@ -7,6 +7,9 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
id: prepare
|
id: prepare
|
||||||
run: |
|
run: |
|
||||||
@@ -29,16 +32,24 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: crazy-max/ghaction-docker-buildx@v1
|
uses: crazy-max/ghaction-docker-buildx@v3
|
||||||
with:
|
with:
|
||||||
version: latest
|
buildx-version: latest
|
||||||
|
|
||||||
- name: Checkout Code
|
- name: Cache Docker Layers
|
||||||
uses: actions/checkout@v2.1.0
|
id: cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
- name: Docker Buildx (no push)
|
- name: Docker Buildx (no push)
|
||||||
run: |
|
run: |
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
|
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||||
|
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||||
--platform ${{ steps.prepare.outputs.docker_platforms }} \
|
--platform ${{ steps.prepare.outputs.docker_platforms }} \
|
||||||
--output "type=image,push=false" \
|
--output "type=image,push=false" \
|
||||||
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
|
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
|
||||||
@@ -59,6 +70,7 @@ jobs:
|
|||||||
if: success()
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
|
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||||
--platform ${{ steps.prepare.outputs.docker_platforms }} \
|
--platform ${{ steps.prepare.outputs.docker_platforms }} \
|
||||||
--output "type=image,push=true" \
|
--output "type=image,push=true" \
|
||||||
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
|
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
|
||||||
@@ -79,5 +91,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
status: ${{ job.status }}
|
status: ${{ job.status }}
|
||||||
job: ${{ github.workflow }}
|
title: ${{ github.workflow }}
|
||||||
nofail: true
|
nofail: true
|
||||||
|
47
.github/workflows/publish-release.yml
vendored
47
.github/workflows/publish-release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2.1.0
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set Release Version
|
- name: Set Release Version
|
||||||
id: get_version
|
id: get_version
|
||||||
@@ -28,13 +28,13 @@ jobs:
|
|||||||
echo $GITHUB_SHA > version.txt
|
echo $GITHUB_SHA > version.txt
|
||||||
|
|
||||||
- name: Set Up Python
|
- name: Set Up Python
|
||||||
uses: actions/setup-python@v1.2.0
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
id: cache_dependencies
|
id: cache_dependencies
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ~\AppData\Local\pip\Cache
|
path: ~\AppData\Local\pip\Cache
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-windows.txt') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-windows.txt') }}
|
||||||
@@ -50,17 +50,18 @@ jobs:
|
|||||||
pyinstaller -y ./package/Tautulli-windows.spec
|
pyinstaller -y ./package/Tautulli-windows.spec
|
||||||
|
|
||||||
- name: Create Installer
|
- name: Create Installer
|
||||||
uses: joncloud/makensis-action@v1
|
uses: joncloud/makensis-action@v1.2
|
||||||
with:
|
with:
|
||||||
script-file: ./package/Tautulli.nsi
|
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
|
||||||
includeMorePlugins: package/nsis-plugins
|
include-more-plugins: true
|
||||||
|
include-custom-plugins-path: package/nsis-plugins
|
||||||
|
|
||||||
- name: Upload Installer
|
- name: Upload Installer
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: Tautulli-windows-installer
|
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
|
- name: Post Status to Discord
|
||||||
uses: sarisia/actions-status-discord@v1
|
uses: sarisia/actions-status-discord@v1
|
||||||
@@ -68,14 +69,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
status: ${{ job.status }}
|
status: ${{ job.status }}
|
||||||
job: Build Windows Installer
|
title: Build Windows Installer
|
||||||
nofail: true
|
nofail: true
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2.1.0
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set Release Version
|
- name: Set Release Version
|
||||||
id: get_version
|
id: get_version
|
||||||
@@ -93,13 +94,13 @@ jobs:
|
|||||||
echo $GITHUB_SHA > version.txt
|
echo $GITHUB_SHA > version.txt
|
||||||
|
|
||||||
- name: Set Up Python
|
- name: Set Up Python
|
||||||
uses: actions/setup-python@v1.2.0
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
id: cache_dependencies
|
id: cache_dependencies
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ~/Library/Caches/pip
|
path: ~/Library/Caches/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-macos.txt') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-macos.txt') }}
|
||||||
@@ -116,13 +117,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Installer
|
- name: Create Installer
|
||||||
run: |
|
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
|
- name: Upload Installer
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: Tautulli-macos-installer
|
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
|
- name: Post Status to Discord
|
||||||
uses: sarisia/actions-status-discord@v1
|
uses: sarisia/actions-status-discord@v1
|
||||||
@@ -130,7 +131,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
status: ${{ job.status }}
|
status: ${{ job.status }}
|
||||||
job: Build MacOS Installer
|
title: Build MacOS Installer
|
||||||
nofail: true
|
nofail: true
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@@ -142,7 +143,7 @@ jobs:
|
|||||||
uses: technote-space/workflow-conclusion-action@v1
|
uses: technote-space/workflow-conclusion-action@v1
|
||||||
|
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2.1.0
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set Release Version
|
- name: Set Release Version
|
||||||
id: get_version
|
id: get_version
|
||||||
@@ -151,13 +152,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Download Windows Installer
|
- name: Download Windows Installer
|
||||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: Tautulli-windows-installer
|
name: Tautulli-windows-installer
|
||||||
|
|
||||||
- name: Download MacOS Installer
|
- name: Download MacOS Installer
|
||||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: Tautulli-macos-installer
|
name: Tautulli-macos-installer
|
||||||
|
|
||||||
@@ -187,8 +188,8 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: Tautulli-windows-installer/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 }}.exe
|
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||||
asset_content_type: application/vnd.microsoft.portable-executable
|
asset_content_type: application/vnd.microsoft.portable-executable
|
||||||
|
|
||||||
- name: Upload MacOS Installer
|
- name: Upload MacOS Installer
|
||||||
@@ -198,6 +199,6 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: Tautulli-macos-installer/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 }}.pkg
|
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||||
asset_content_type: application/vnd.apple.installer+xml
|
asset_content_type: application/vnd.apple.installer+xml
|
||||||
|
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,16 +1,46 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
# v2.5.0-beta (2020-05-31)
|
## 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: 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.2 (2020-07-01)
|
||||||
|
|
||||||
* Announcements:
|
* Announcements:
|
||||||
* Tautulli now supports Python 3!
|
* Tautulli now supports Python 3!
|
||||||
* Python 2 is still supported for the time being, but it is recommended to upgrade to 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:
|
* UI:
|
||||||
|
* Fix: MacOS menu bar icon causing Tautulli to fail to start.
|
||||||
|
* Fix: Unable to login to Tautulli on Python 2.
|
||||||
* New: Windows and MacOS setting to enable Tautulli to start automatically when you login.
|
* New: Windows and MacOS setting to enable Tautulli to start automatically when you login.
|
||||||
* New: Added system tray icon for MacOS.
|
* New: Added menu bar icon for MacOS.
|
||||||
* New: Ability to import a Tautulli database in the settings.
|
* New: Ability to import a Tautulli database in the settings.
|
||||||
* New: Added Tautulli news area on the settings page.
|
* 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:
|
* 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.
|
* New: Windows and MacOS app installers to install Tautulli without needing Python installed.
|
||||||
|
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@ RUN \
|
|||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
CMD [ "python", "Tautulli.py", "--datadir", "/config" ]
|
ENTRYPOINT [ "./start.sh" ]
|
||||||
|
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
EXPOSE 8181
|
EXPOSE 8181
|
||||||
|
13
Tautulli.py
13
Tautulli.py
@@ -257,17 +257,18 @@ def main():
|
|||||||
plexpy.HTTP_ROOT)
|
plexpy.HTTP_ROOT)
|
||||||
|
|
||||||
if common.PLATFORM == 'Darwin' and plexpy.CONFIG.SYS_TRAY_ICON:
|
if common.PLATFORM == 'Darwin' and plexpy.CONFIG.SYS_TRAY_ICON:
|
||||||
try:
|
if not macos.HAS_PYOBJC:
|
||||||
import AppKit
|
|
||||||
except ImportError:
|
|
||||||
logger.warn("The pyobjc module is missing. Install this "
|
logger.warn("The pyobjc module is missing. Install this "
|
||||||
"module to enable the system tray icon.")
|
"module to enable the MacOS menu bar icon.")
|
||||||
plexpy.CONFIG.SYS_TRAY_ICON = False
|
plexpy.CONFIG.SYS_TRAY_ICON = False
|
||||||
|
|
||||||
if plexpy.CONFIG.SYS_TRAY_ICON:
|
if plexpy.CONFIG.SYS_TRAY_ICON:
|
||||||
# MacOS system tray icon must be run on the main thread and is blocking
|
# MacOS menu bar icon must be run on the main thread and is blocking
|
||||||
# Start the rest of Tautulli on a new thread
|
# 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 = macos.MacOSSystemTray()
|
||||||
plexpy.MAC_SYS_TRAY_ICON.start()
|
plexpy.MAC_SYS_TRAY_ICON.start()
|
||||||
else:
|
else:
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Display information
|
# 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."
|
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
|
# Parameter check
|
||||||
if [ -z "$1" ]; then
|
if [ -z "$1" ]; then
|
||||||
|
@@ -230,20 +230,12 @@ ${next.modalIncludes()}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
|
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
|
||||||
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
|
<li class="active"><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li>
|
||||||
<li><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>
|
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="patreon-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 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">
|
|
||||||
<p>
|
<p>
|
||||||
Click the button below to continue to GitHub.
|
Click the button below to continue to GitHub.
|
||||||
</p>
|
</p>
|
||||||
@@ -251,6 +243,14 @@ ${next.modalIncludes()}
|
|||||||
<i class="fa fa-heart fa-sm" style="color: #ea4aaa;"></i> Sponsor
|
<i class="fa fa-heart fa-sm" style="color: #ea4aaa;"></i> Sponsor
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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">
|
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
|
||||||
<p>
|
<p>
|
||||||
Click the button below to continue to PayPal.
|
Click the button below to continue to PayPal.
|
||||||
@@ -296,9 +296,7 @@ ${next.modalIncludes()}
|
|||||||
<script src="${http_root}js/ipaddr.min.js"></script>
|
<script src="${http_root}js/ipaddr.min.js"></script>
|
||||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
||||||
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
|
||||||
<script src="${http_root}js/ajaxNotifications.js"></script>
|
<script src="${http_root}js/ajaxNotifications.js"></script>
|
||||||
% endif
|
|
||||||
<script>
|
<script>
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$('body').on('click', '#updateDismiss', function() {
|
$('body').on('click', '#updateDismiss', function() {
|
||||||
@@ -423,6 +421,10 @@ ${next.modalIncludes()}
|
|||||||
$(document).on('hidden.bs.modal', '.modal', function () {
|
$(document).on('hidden.bs.modal', '.modal', function () {
|
||||||
$('.modal:visible').length && $(document.body).addClass('modal-open');
|
$('.modal:visible').length && $(document.body).addClass('modal-open');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
||||||
|
check_notifications();
|
||||||
|
% endif
|
||||||
});
|
});
|
||||||
|
|
||||||
% if _session['user_group'] != 'admin':
|
% if _session['user_group'] != 'admin':
|
||||||
|
@@ -2161,7 +2161,7 @@ div.advanced-setting {
|
|||||||
li.advanced-setting {
|
li.advanced-setting {
|
||||||
border-left: 1px solid #cc7b19;
|
border-left: 1px solid #cc7b19;
|
||||||
}
|
}
|
||||||
.docker-setting {
|
.setting-message {
|
||||||
color: #cc7b19;
|
color: #cc7b19;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
@@ -2312,6 +2312,7 @@ li.advanced-setting {
|
|||||||
width: 140px;
|
width: 140px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.user-player-instance-playcount h3 {
|
.user-player-instance-playcount h3 {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
@@ -3873,6 +3874,10 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
background-color: #31afe1;
|
background-color: #31afe1;
|
||||||
background-image: url(../images/platforms/kodi.svg);
|
background-image: url(../images/platforms/kodi.svg);
|
||||||
}
|
}
|
||||||
|
.platform-lg {
|
||||||
|
background-color: #a50034;
|
||||||
|
background-image: url(../images/platforms/lg.svg);
|
||||||
|
}
|
||||||
.platform-linux {
|
.platform-linux {
|
||||||
background-color: #1793d0;
|
background-color: #1793d0;
|
||||||
background-image: url(../images/platforms/linux.svg);
|
background-image: url(../images/platforms/linux.svg);
|
||||||
@@ -3974,6 +3979,9 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
.platform-kodi-rgba {
|
.platform-kodi-rgba {
|
||||||
background-color: rgba(49, 175, 225, 0.40);
|
background-color: rgba(49, 175, 225, 0.40);
|
||||||
}
|
}
|
||||||
|
.platform-lg-rgba {
|
||||||
|
background-color: rgba(165, 0, 52, 0.40);
|
||||||
|
}
|
||||||
.platform-linux-rgba {
|
.platform-linux-rgba {
|
||||||
background-color: rgba(23, 147, 208, 0.40);
|
background-color: rgba(23, 147, 208, 0.40);
|
||||||
}
|
}
|
||||||
|
7
data/interfaces/default/images/platforms/lg.svg
Normal file
7
data/interfaces/default/images/platforms/lg.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<title>lg</title>
|
||||||
|
<path fill="#fff" d="M30.203 31.797c0 8.176-6.654 14.832-14.835 14.82-7.927-0.011-14.818-6.28-14.812-14.838 0.005-8.282 6.541-14.82 14.841-14.803 8.618 0.017 14.807 6.969 14.806 14.822zM26.577 32.388c-0.087 4.433-3.485 9.518-9.37 10.487-6.122 1.008-11.584-2.989-12.814-8.656-0.632-2.912-0.221-5.696 1.362-8.228 2.347-3.754 5.815-5.502 10.222-5.453 0-0.387 0-0.761 0-1.134-4.114-0.281-9.226 1.824-11.763 6.923-2.454 4.932-1.296 10.953 2.811 14.672 4.153 3.762 10.224 4.309 14.953 1.326 2.328-1.468 3.999-3.496 4.997-6.067 0.628-1.617 0.882-3.296 0.813-5.032-2.967 0-5.909 0-8.864 0 0 0.39 0 0.768 0 1.162 2.558-0 5.097-0 7.652-0zM15.991 37.112c0-0.129 0-0.221 0-0.313 0-3.731 0-7.463 0-11.194 0-0.060-0.004-0.119 0-0.179 0.009-0.118-0.038-0.166-0.16-0.163-0.278 0.006-0.556 0.012-0.833-0.002-0.178-0.008-0.237 0.042-0.237 0.23 0.005 4.194 0.005 8.389-0 12.583-0 0.198 0.065 0.239 0.249 0.237 1.224-0.007 2.448-0.004 3.672-0.004 0.072 0 0.143 0 0.244 0 0-0.343-0.008-0.665 0.003-0.987 0.006-0.166-0.050-0.214-0.214-0.212-0.82 0.007-1.641 0.003-2.461 0.003-0.078 0-0.155 0-0.263 0zM12.434 27.068c0.003-0.987-0.799-1.798-1.785-1.805s-1.799 0.796-1.805 1.782c-0.006 0.985 0.799 1.8 1.783 1.805 0.985 0.004 1.804-0.803 1.807-1.783z"></path>
|
||||||
|
<path fill="#fff" d="M63.467 30.606c0 2.864 0 5.707 0 8.571-1.242 0-2.479 0-3.742 0 0-0.468 0-0.933 0-1.433-0.203 0.226-0.366 0.432-0.553 0.612-0.683 0.656-1.518 1-2.441 1.136-1.187 0.174-2.348 0.075-3.462-0.4-1.234-0.526-2.145-1.407-2.8-2.565-0.599-1.058-0.906-2.207-1.035-3.409-0.148-1.367-0.103-2.723 0.28-4.051 0.797-2.764 2.635-4.391 5.453-4.899 1.534-0.277 3.058-0.208 4.54 0.311 1.243 0.436 2.298 1.139 3.011 2.276 0.431 0.688 0.584 1.467 0.687 2.258 0.013 0.097 0.028 0.195 0.046 0.318-0.064 0.003-0.126 0.010-0.188 0.010-1.389 0.001-2.779-0.002-4.169 0.003-0.151 0.001-0.215-0.034-0.245-0.197-0.229-1.234-1.281-1.773-2.308-1.679-1.182 0.108-1.823 0.859-2.22 1.888-0.211 0.547-0.315 1.12-0.352 1.703-0.066 1.061-0.039 2.117 0.31 3.138 0.211 0.618 0.523 1.173 1.050 1.579 1.371 1.055 3.326 0.436 3.877-1.228 0.090-0.274 0.157-0.557 0.246-0.875-0.112 0-0.182 0-0.251 0-0.794 0-1.588-0.005-2.382 0.004-0.168 0.002-0.213-0.053-0.212-0.214 0.006-0.887 0.005-1.774 0.001-2.66-0.001-0.139 0.019-0.215 0.192-0.215 2.17 0.006 4.341 0.004 6.511 0.004 0.045 0 0.090 0.008 0.157 0.013z"></path>
|
||||||
|
<path fill="#fff" d="M48.501 35.522c0 1.233 0 2.44 0 3.661-3.613 0-7.216 0-10.834 0 0-4.923 0-9.841 0-14.77 1.44 0 2.872 0 4.331 0 0 0.092 0 0.175 0 0.259 0 3.526 0 7.053 0 10.579 0 0.271 0 0.271 0.267 0.271 1.985 0 3.97 0 5.954 0 0.086 0 0.171 0 0.281 0z"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
@@ -36,7 +36,3 @@ function check_notifications() {
|
|||||||
check_notifications();
|
check_notifications();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
check_notifications();
|
|
||||||
});
|
|
@@ -38,7 +38,7 @@
|
|||||||
<th align="left" id="count">Total Movies / TV Shows / Artists</th>
|
<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="parent_count">Total Seasons / Albums</th>
|
||||||
<th align="left" id="child_count">Total Episodes / Tracks</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="last_played">Last Played</th>
|
||||||
<th align="left" id="total_plays">Total Plays</th>
|
<th align="left" id="total_plays">Total Plays</th>
|
||||||
<th align="left" id="total_duration">Total Played Duration</th>
|
<th align="left" id="total_duration">Total Played Duration</th>
|
||||||
|
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<!-- ICONS -->
|
<!-- ICONS -->
|
||||||
<!-- Android -->
|
<!-- Android -->
|
||||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials>
|
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
|
||||||
<meta name="theme-color" content="#282a2d">
|
<meta name="theme-color" content="#282a2d">
|
||||||
<!-- Apple -->
|
<!-- Apple -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||||
|
@@ -13,7 +13,11 @@ DOCUMENTATION :: END
|
|||||||
% for device in sorted(devices_list, key=lambda k: k['device_name']):
|
% for device in sorted(devices_list, key=lambda k: k['device_name']):
|
||||||
<li class="mobile-device pointer" data-id="${device['id']}" data-name="${device['device_name']}">
|
<li class="mobile-device pointer" data-id="${device['id']}" data-name="${device['device_name']}">
|
||||||
<span>
|
<span>
|
||||||
|
% if device['official']:
|
||||||
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span>
|
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span>
|
||||||
|
% else:
|
||||||
|
<span class="toggle-left officail-tooltip" data-toggle="tooltip" data-placement="top" title="Unofficial or Unknown App"><i class="fa fa-lg fa-fw fa-exclamation-triangle"></i></span>
|
||||||
|
% endif
|
||||||
${device['friendly_name'] or device['device_name']} <span class="friendly_name">(${device['id']})</span>
|
${device['friendly_name'] or device['device_name']} <span class="friendly_name">(${device['id']})</span>
|
||||||
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span>
|
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span>
|
||||||
<span class="toggle-right friendly_name" id="device-last_seen-${device['id']}">
|
<span class="toggle-right friendly_name" id="device-last_seen-${device['id']}">
|
||||||
@@ -138,4 +142,6 @@ DOCUMENTATION :: END
|
|||||||
}
|
}
|
||||||
verifiedDevice = true;
|
verifiedDevice = true;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$('.officail-tooltip').tooltip();
|
||||||
</script>
|
</script>
|
@@ -853,10 +853,7 @@
|
|||||||
PNotify.prototype.options.hide = true;
|
PNotify.prototype.options.hide = true;
|
||||||
PNotify.prototype.options.delay = $('#browser_auto_hide_delay').val() * 1000;
|
PNotify.prototype.options.delay = $('#browser_auto_hide_delay').val() * 1000;
|
||||||
}
|
}
|
||||||
var notification = new PNotify({
|
displayPNotify($('#test_subject').val(), $('#test_body').val());
|
||||||
title: $('#test_subject').val(),
|
|
||||||
text: $('#test_body').val()
|
|
||||||
});
|
|
||||||
showMsg('<i class="fa fa-check"></i> Notification sent.', false, true, 5000);
|
showMsg('<i class="fa fa-check"></i> Notification sent.', false, true, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
from plexpy.helpers import anon_url, checked
|
from plexpy.helpers import anon_url, checked
|
||||||
|
|
||||||
docker_setting = 'disabled' if plexpy.DOCKER else ''
|
docker_setting = 'disabled' if plexpy.DOCKER else ''
|
||||||
docker_msg = '<span class="docker-setting small">(Controlled by Docker Container)</span>' if plexpy.DOCKER else ''
|
docker_msg = '<span class="setting-message small">(Controlled by Docker Container)</span>' if plexpy.DOCKER else ''
|
||||||
|
|
||||||
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['label'].lower())
|
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['label'].lower())
|
||||||
available_newsletter_agents = sorted(newsletters.available_newsletter_agents(), key=lambda k: k['label'].lower())
|
available_newsletter_agents = sorted(newsletters.available_newsletter_agents(), key=lambda k: k['label'].lower())
|
||||||
@@ -458,11 +458,20 @@
|
|||||||
|
|
||||||
<p class="help-block">Note: Web interface changes require a restart.</p>
|
<p class="help-block">Note: Web interface changes require a restart.</p>
|
||||||
% if common.PLATFORM in ('Windows', 'Darwin'):
|
% if common.PLATFORM in ('Windows', 'Darwin'):
|
||||||
|
<%
|
||||||
|
tray = {'Windows': 'System Tray', 'Darwin': 'Menu Bar'}
|
||||||
|
tray_disabled = tray_disabled_msg = ''
|
||||||
|
if common.PLATFORM == 'Darwin':
|
||||||
|
from plexpy.macos import HAS_PYOBJC
|
||||||
|
if not HAS_PYOBJC:
|
||||||
|
tray_disabled = 'disabled'
|
||||||
|
tray_disabled_msg = '<span class="setting-message small">(Missing pyobjc module)</span>'
|
||||||
|
%>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" class="http-settings" name="sys_tray_icon" id="sys_tray_icon" value="1" ${config['sys_tray_icon']}> Enable System Tray Icon
|
<input type="checkbox" class="http-settings" name="sys_tray_icon" id="sys_tray_icon" value="1" ${config['sys_tray_icon']} ${tray_disabled}> Enable ${tray[common.PLATFORM]} Icon ${tray_disabled_msg | n}
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">Show Tautulli shortcut in the system tray.</p>
|
<p class="help-block">Show Tautulli shortcut in the ${tray[common.PLATFORM].lower()}.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
@@ -1047,7 +1056,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="notify_recently_added_delay_error" class="alert alert-danger settings-alert" role="alert"></div>
|
<div id="notify_recently_added_delay_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">Set the delay (in seconds) to wait for consecutive recently added items to group together and to allow metadata to be processed before sending the notification. Minimum 60 seconds.</p>
|
<p class="help-block">Set the delay (in seconds) to wait for consecutive recently added items to group together and to allow metadata to be processed before sending the recently added notification. Minimum 60 seconds, default 300.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group advanced-setting">
|
<div class="form-group advanced-setting">
|
||||||
<label>Flush Recently Added</label>
|
<label>Flush Recently Added</label>
|
||||||
@@ -1426,6 +1435,7 @@
|
|||||||
<label>Registered Devices</label>
|
<label>Registered Devices</label>
|
||||||
<p class="help-block">Register a new device using a QR code, or configure an existing device by clicking the settings icon on the right.</p>
|
<p class="help-block">Register a new device using a QR code, or configure an existing device by clicking the settings icon on the right.</p>
|
||||||
<p id="app_api_msg" style="color: #eb8600;">Warning: The API must be enabled under <a data-tab-destination="web_interface" data-target="api_enabled">Web Interface</a> to use the app.</p>
|
<p id="app_api_msg" style="color: #eb8600;">Warning: The API must be enabled under <a data-tab-destination="web_interface" data-target="api_enabled">Web Interface</a> to use the app.</p>
|
||||||
|
<br />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div id="plexpy-mobile-devices-table" class="col-md-12">
|
<div id="plexpy-mobile-devices-table" class="col-md-12">
|
||||||
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading registered devices...</div>
|
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading registered devices...</div>
|
||||||
@@ -1861,7 +1871,10 @@ Rating: {rating}/10 --> Rating: /10
|
|||||||
<label>Instructions</label>
|
<label>Instructions</label>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Scan the QR code below with the Tautulli Android app to automatically register it with the server (make sure the Tautulli Address below is correct)
|
Scan the QR code below with the Tautulli Android app to automatically register it with the server (make sure the Tautulli Address below is correct)
|
||||||
or manually enter the connection info and device token into the app settings.
|
or manually enter the connection info and device token into the app settings. This window will automatically close once device registration is successful.
|
||||||
|
</p>
|
||||||
|
<p class="help-block">
|
||||||
|
Note: OneSignal.com must not be blocked (e.g. in Pi-hole) for device registration.
|
||||||
</p>
|
</p>
|
||||||
<label>QR Code</label>
|
<label>QR Code</label>
|
||||||
<pre id="api_qr_code" style="text-align: center"></pre>
|
<pre id="api_qr_code" style="text-align: center"></pre>
|
||||||
@@ -2541,9 +2554,7 @@ $(document).ready(function() {
|
|||||||
$("#token_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
|
$("#token_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
|
||||||
}
|
}
|
||||||
function OAuthSuccessCallback(authToken) {
|
function OAuthSuccessCallback(authToken) {
|
||||||
var x_plex_headers = getPlexHeaders();
|
|
||||||
$("#pms_token").val(authToken);
|
$("#pms_token").val(authToken);
|
||||||
$("#pms_uuid").val(x_plex_headers['X-Plex-Client-Identifier']);
|
|
||||||
$("#token_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
|
$("#token_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
|
||||||
getServerOptions(authToken);
|
getServerOptions(authToken);
|
||||||
}
|
}
|
||||||
|
@@ -284,7 +284,7 @@ DOCUMENTATION :: END
|
|||||||
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
|
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th align="left" id="last_seen">Last Seen</th>
|
<th align="left" id="last_seen">Last Streamed</th>
|
||||||
<th align="left" id="ip_address">IP Address</th>
|
<th align="left" id="ip_address">IP Address</th>
|
||||||
<th align="left" id="platform">Last Platform</th>
|
<th align="left" id="platform">Last Platform</th>
|
||||||
<th align="left" id="player">Last Player</th>
|
<th align="left" id="player">Last Player</th>
|
||||||
|
@@ -27,7 +27,7 @@ DOCUMENTATION :: END
|
|||||||
<div id="user-player-image-${a['result_id']}">
|
<div id="user-player-image-${a['result_id']}">
|
||||||
<div class="user-player-instance-box svg-icon platform-${a['platform_name']}"></div>
|
<div class="user-player-instance-box svg-icon platform-${a['platform_name']}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-player-instance-name">
|
<div class="user-player-instance-name" title="${a['player_name']}">
|
||||||
${a['player_name']}
|
${a['player_name']}
|
||||||
</div>
|
</div>
|
||||||
<div class="user-player-instance-playcount">
|
<div class="user-player-instance-playcount">
|
||||||
|
@@ -34,7 +34,7 @@
|
|||||||
<th align="left" id="edit_row">Edit</th>
|
<th align="left" id="edit_row">Edit</th>
|
||||||
<th align="right" id="avatar"></th>
|
<th align="right" id="avatar"></th>
|
||||||
<th align="left" id="friendly_name">User</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_known_ip">Last Known IP</th>
|
||||||
<th align="left" id="last_platform">Last Platform</th>
|
<th align="left" id="last_platform">Last Platform</th>
|
||||||
<th align="left" id="last_player">Last Player</th>
|
<th align="left" id="last_player">Last Player</th>
|
||||||
|
@@ -38,6 +38,7 @@ load_rc_config ${name}
|
|||||||
status_cmd="${name}_status"
|
status_cmd="${name}_status"
|
||||||
stop_cmd="${name}_stop"
|
stop_cmd="${name}_stop"
|
||||||
|
|
||||||
|
command_interpreter="/usr/local/bin/python3"
|
||||||
command="${tautulli_dir}/Tautulli.py"
|
command="${tautulli_dir}/Tautulli.py"
|
||||||
command_args="--daemon --pidfile ${tautulli_pid} --quiet --nolaunch ${tautulli_flags}"
|
command_args="--daemon --pidfile ${tautulli_pid} --quiet --nolaunch ${tautulli_flags}"
|
||||||
|
|
||||||
|
@@ -31,11 +31,13 @@
|
|||||||
# sudo chown tautulli:tautulli -R /opt/Tautulli
|
# sudo chown tautulli:tautulli -R /opt/Tautulli
|
||||||
#
|
#
|
||||||
# - Adjust ExecStart= to point to:
|
# - 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
|
# - 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
|
# - 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
|
# - Default: --datadir /opt/Tautulli
|
||||||
#
|
#
|
||||||
# - Adjust User= and Group= to the user/group you want Tautulli to run as.
|
# - 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
|
After=network-online.target
|
||||||
|
|
||||||
[Service]
|
[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
|
GuessMainPID=no
|
||||||
Type=forking
|
Type=forking
|
||||||
User=tautulli
|
User=tautulli
|
||||||
|
@@ -100,7 +100,7 @@ def createSelfSignedCertificate(issuerName, issuerKey, serial, notBefore, notAft
|
|||||||
cert.set_pubkey(issuerKey)
|
cert.set_pubkey(issuerKey)
|
||||||
|
|
||||||
if altNames:
|
if altNames:
|
||||||
cert.add_extensions([crypto.X509Extension("subjectAltName", False, altNames)])
|
cert.add_extensions([crypto.X509Extension(b"subjectAltName", False, altNames)])
|
||||||
|
|
||||||
cert.sign(issuerKey, digest)
|
cert.sign(issuerKey, digest)
|
||||||
return cert
|
return cert
|
||||||
|
@@ -16,10 +16,10 @@ analysis = Analysis(
|
|||||||
('../CHANGELOG.md', '.'),
|
('../CHANGELOG.md', '.'),
|
||||||
('../LICENSE', '.'),
|
('../LICENSE', '.'),
|
||||||
('../version.txt', '.'),
|
('../version.txt', '.'),
|
||||||
('../lib/ipwhois/data', 'data')
|
('../lib/ipwhois/data', 'ipwhois/data')
|
||||||
],
|
],
|
||||||
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
|
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
|
cipher=block_cipher
|
||||||
)
|
)
|
||||||
pyz = PYZ(
|
pyz = PYZ(
|
||||||
@@ -47,5 +47,9 @@ app = BUNDLE(
|
|||||||
name='Tautulli.app',
|
name='Tautulli.app',
|
||||||
icon='../data/interfaces/default/images/logo-circle.icns',
|
icon='../data/interfaces/default/images/logo-circle.icns',
|
||||||
bundle_identifier='com.Tautulli.Tautulli',
|
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')
|
('..\\lib\\ipwhois\\data', 'data')
|
||||||
],
|
],
|
||||||
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
|
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
|
||||||
|
hiddenimports=['cheroot.ssl', 'cheroot.ssl.builtin'],
|
||||||
cipher=block_cipher,
|
cipher=block_cipher,
|
||||||
)
|
)
|
||||||
pyz = PYZ(
|
pyz = PYZ(
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
pyinstaller
|
pyinstaller
|
||||||
pyopenssl
|
pyopenssl
|
||||||
pycryptodomex
|
pycryptodomex
|
||||||
pyobjc
|
pyobjc-framework-Cocoa
|
@@ -37,8 +37,7 @@ from apscheduler.triggers.interval import IntervalTrigger
|
|||||||
from UniversalAnalytics import Tracker
|
from UniversalAnalytics import Tracker
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
PYTHON_VERSION = sys.version_info[:3]
|
PYTHON2 = sys.version_info[0] == 2
|
||||||
PYTHON2 = PYTHON_VERSION[0] == 2
|
|
||||||
|
|
||||||
if PYTHON2:
|
if PYTHON2:
|
||||||
import activity_handler
|
import activity_handler
|
||||||
@@ -745,7 +744,7 @@ def dbcheck():
|
|||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS mobile_devices (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'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, '
|
'device_id TEXT NOT NULL UNIQUE, device_token TEXT, device_name TEXT, friendly_name TEXT, '
|
||||||
'last_seen INTEGER)'
|
'last_seen INTEGER, official INTEGER DEFAULT 0)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# tvmaze_lookup table :: This table keeps record of the TVmaze lookups
|
# tvmaze_lookup table :: This table keeps record of the TVmaze lookups
|
||||||
@@ -2001,6 +2000,19 @@ def dbcheck():
|
|||||||
'ALTER TABLE mobile_devices ADD COLUMN last_seen INTEGER'
|
'ALTER TABLE mobile_devices ADD COLUMN last_seen INTEGER'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Upgrade mobile_devices table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT official FROM mobile_devices')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug("Altering database. Updating database table mobile_devices.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE mobile_devices ADD COLUMN official INTEGER DEFAULT 0'
|
||||||
|
)
|
||||||
|
# Update official mobile device flag
|
||||||
|
for device_id, in c_db.execute('SELECT device_id FROM mobile_devices').fetchall():
|
||||||
|
c_db.execute('UPDATE mobile_devices SET official = ? WHERE device_id = ?',
|
||||||
|
[mobile_app.validate_device_id(device_id), device_id])
|
||||||
|
|
||||||
# Upgrade notifiers table from earlier versions
|
# Upgrade notifiers table from earlier versions
|
||||||
try:
|
try:
|
||||||
c_db.execute('SELECT custom_conditions FROM notifiers')
|
c_db.execute('SELECT custom_conditions FROM notifiers')
|
||||||
@@ -2198,11 +2210,6 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
|
|||||||
logger.info("Removing pidfile %s", PIDFILE)
|
logger.info("Removing pidfile %s", PIDFILE)
|
||||||
os.remove(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:
|
if restart:
|
||||||
logger.info("Tautulli is restarting...")
|
logger.info("Tautulli is restarting...")
|
||||||
|
|
||||||
@@ -2225,7 +2232,7 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
|
|||||||
# https://bugs.python.org/issue19066
|
# https://bugs.python.org/issue19066
|
||||||
if NOFORK:
|
if NOFORK:
|
||||||
pass
|
pass
|
||||||
elif common.PLATFORM == 'Windows':
|
elif common.PLATFORM in ('Windows', 'Darwin'):
|
||||||
subprocess.Popen(args, cwd=os.getcwd())
|
subprocess.Popen(args, cwd=os.getcwd())
|
||||||
else:
|
else:
|
||||||
os.execv(exe, args)
|
os.execv(exe, args)
|
||||||
@@ -2235,6 +2242,11 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
|
|||||||
|
|
||||||
logger.shutdown()
|
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)
|
os._exit(0)
|
||||||
|
|
||||||
|
|
||||||
@@ -2251,6 +2263,7 @@ def initialize_tracker():
|
|||||||
'appInstallerId': CONFIG.GIT_BRANCH,
|
'appInstallerId': CONFIG.GIT_BRANCH,
|
||||||
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform
|
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform
|
||||||
'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro
|
'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro
|
||||||
|
'dimension3': common.PYTHON_VERSION,
|
||||||
'userLanguage': SYS_LANGUAGE,
|
'userLanguage': SYS_LANGUAGE,
|
||||||
'documentEncoding': SYS_ENCODING,
|
'documentEncoding': SYS_ENCODING,
|
||||||
'noninteractive': True
|
'noninteractive': True
|
||||||
|
@@ -136,7 +136,11 @@ class API2(object):
|
|||||||
self._api_app = True
|
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 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 or (self._api_app and self._api_apikey == mobile_app.get_temp_device_token()):
|
if 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 \
|
||||||
|
self._api_cmd == 'register_device':
|
||||||
self._api_authenticated = True
|
self._api_authenticated = True
|
||||||
|
|
||||||
elif self._api_app and mobile_app.get_mobile_device_by_token(self._api_apikey):
|
elif self._api_app and mobile_app.get_mobile_device_by_token(self._api_apikey):
|
||||||
@@ -403,12 +407,12 @@ class API2(object):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
if not device_id:
|
if not device_id:
|
||||||
self._api_msg = 'Device registartion failed: no device id provided.'
|
self._api_msg = 'Device registration failed: no device id provided.'
|
||||||
self._api_result_type = 'error'
|
self._api_result_type = 'error'
|
||||||
return
|
return
|
||||||
|
|
||||||
elif not device_name:
|
elif not device_name:
|
||||||
self._api_msg = 'Device registartion failed: no device name provided.'
|
self._api_msg = 'Device registration failed: no device name provided.'
|
||||||
self._api_result_type = 'error'
|
self._api_result_type = 'error'
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -422,7 +426,7 @@ class API2(object):
|
|||||||
self._api_result_type = 'success'
|
self._api_result_type = 'success'
|
||||||
mobile_app.set_temp_device_token(None)
|
mobile_app.set_temp_device_token(None)
|
||||||
else:
|
else:
|
||||||
self._api_msg = 'Device registartion failed: database error.'
|
self._api_msg = 'Device registration failed: database error.'
|
||||||
self._api_result_type = 'error'
|
self._api_result_type = 'error'
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@@ -35,6 +35,7 @@ PLATFORM_RELEASE = platform.release()
|
|||||||
PLATFORM_VERSION = platform.version()
|
PLATFORM_VERSION = platform.version()
|
||||||
PLATFORM_LINUX_DISTRO = ' '.join(x for x in distro.linux_distribution() if x)
|
PLATFORM_LINUX_DISTRO = ' '.join(x for x in distro.linux_distribution() if x)
|
||||||
PLATFORM_DEVICE_NAME = platform.node()
|
PLATFORM_DEVICE_NAME = platform.node()
|
||||||
|
PYTHON_VERSION = platform.python_version()
|
||||||
BRANCH = version.PLEXPY_BRANCH
|
BRANCH = version.PLEXPY_BRANCH
|
||||||
RELEASE = version.PLEXPY_RELEASE_VERSION
|
RELEASE = version.PLEXPY_RELEASE_VERSION
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ PLATFORM_NAMES = {
|
|||||||
'nexus': 'android',
|
'nexus': 'android',
|
||||||
'macos': 'macos',
|
'macos': 'macos',
|
||||||
'microsoft edge': 'msedge',
|
'microsoft edge': 'msedge',
|
||||||
|
'netcast': 'lg',
|
||||||
'opera': 'opera',
|
'opera': 'opera',
|
||||||
'osx': 'macos',
|
'osx': 'macos',
|
||||||
'playstation': 'playstation',
|
'playstation': 'playstation',
|
||||||
@@ -119,6 +121,7 @@ PLATFORM_NAMES = {
|
|||||||
'tizen': 'samsung',
|
'tizen': 'samsung',
|
||||||
'tvos': 'atv',
|
'tvos': 'atv',
|
||||||
'vizio': 'opera',
|
'vizio': 'opera',
|
||||||
|
'webos': 'lg',
|
||||||
'wiiu': 'wiiu',
|
'wiiu': 'wiiu',
|
||||||
'windows': 'windows',
|
'windows': 'windows',
|
||||||
'windows phone': 'wp',
|
'windows phone': 'wp',
|
||||||
|
@@ -75,7 +75,6 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'PMS_UPDATE_CHECK_INTERVAL': (int, 'Advanced', 24),
|
'PMS_UPDATE_CHECK_INTERVAL': (int, 'Advanced', 24),
|
||||||
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
|
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
|
||||||
'TIME_FORMAT': (str, 'General', 'HH:mm'),
|
'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', 'http://www.nullrefer.com/?'),
|
||||||
'API_ENABLED': (int, 'General', 1),
|
'API_ENABLED': (int, 'General', 1),
|
||||||
'API_KEY': (str, 'General', ''),
|
'API_KEY': (str, 'General', ''),
|
||||||
@@ -348,7 +347,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'NOTIFY_GROUP_RECENTLY_ADDED': (int, 'Monitoring', 1),
|
'NOTIFY_GROUP_RECENTLY_ADDED': (int, 'Monitoring', 1),
|
||||||
'NOTIFY_UPLOAD_POSTERS': (int, 'Monitoring', 0),
|
'NOTIFY_UPLOAD_POSTERS': (int, 'Monitoring', 0),
|
||||||
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
|
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
|
||||||
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 60),
|
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 300),
|
||||||
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
|
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
|
||||||
'NOTIFY_RECENTLY_ADDED_UPGRADE': (int, 'Monitoring', 0),
|
'NOTIFY_RECENTLY_ADDED_UPGRADE': (int, 'Monitoring', 0),
|
||||||
'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0),
|
'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0),
|
||||||
@@ -943,4 +942,10 @@ class Config(object):
|
|||||||
if plexpy.DOCKER:
|
if plexpy.DOCKER:
|
||||||
self.PLEXPY_AUTO_UPDATE = 0
|
self.PLEXPY_AUTO_UPDATE = 0
|
||||||
|
|
||||||
self.CONFIG_VERSION == 15
|
self.CONFIG_VERSION = 15
|
||||||
|
|
||||||
|
if self.CONFIG_VERSION == 15:
|
||||||
|
if self.HTTP_ROOT and self.HTTP_ROOT != '/':
|
||||||
|
self.JWT_UPDATE_SECRET = True
|
||||||
|
|
||||||
|
self.CONFIG_VERSION = 16
|
||||||
|
@@ -27,10 +27,10 @@ import time
|
|||||||
import plexpy
|
import plexpy
|
||||||
if plexpy.PYTHON2:
|
if plexpy.PYTHON2:
|
||||||
import logger
|
import logger
|
||||||
from helpers import cast_to_int, bool_true
|
from helpers import cast_to_int, bool_true, chunk
|
||||||
else:
|
else:
|
||||||
from plexpy import logger
|
from plexpy import logger
|
||||||
from plexpy.helpers import cast_to_int, bool_true
|
from plexpy.helpers import cast_to_int, bool_true, chunk
|
||||||
|
|
||||||
|
|
||||||
FILENAME = "tautulli.db"
|
FILENAME = "tautulli.db"
|
||||||
@@ -218,12 +218,16 @@ def delete_rows_from_table(table, row_ids):
|
|||||||
|
|
||||||
if row_ids:
|
if row_ids:
|
||||||
logger.info("Tautulli Database :: Deleting row ids %s from %s database table", row_ids, table)
|
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:
|
try:
|
||||||
monitor_db.action(query, row_ids)
|
for row_ids_group in chunk(row_ids, sqlite_max_variable_number):
|
||||||
return True
|
query = "DELETE FROM " + table + " WHERE id IN (%s) " % ','.join(['?'] * len(row_ids_group))
|
||||||
|
monitor_db.action(query, row_ids_group)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Database :: Failed to delete rows from %s database table: %s" % (table, e))
|
logger.error("Tautulli Database :: Failed to delete rows from %s database table: %s" % (table, e))
|
||||||
return False
|
return False
|
||||||
|
@@ -31,7 +31,7 @@ import datetime
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
import hashlib
|
import hashlib
|
||||||
import imghdr
|
import imghdr
|
||||||
from future.moves.itertools import zip_longest
|
from future.moves.itertools import islice, zip_longest
|
||||||
import ipwhois
|
import ipwhois
|
||||||
import ipwhois.exceptions
|
import ipwhois.exceptions
|
||||||
import ipwhois.utils
|
import ipwhois.utils
|
||||||
@@ -52,10 +52,12 @@ import xmltodict
|
|||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
if plexpy.PYTHON2:
|
if plexpy.PYTHON2:
|
||||||
|
import common
|
||||||
import logger
|
import logger
|
||||||
import request
|
import request
|
||||||
from api2 import API2
|
from api2 import API2
|
||||||
else:
|
else:
|
||||||
|
from plexpy import common
|
||||||
from plexpy import logger
|
from plexpy import logger
|
||||||
from plexpy import request
|
from plexpy import request
|
||||||
from plexpy.api2 import API2
|
from plexpy.api2 import API2
|
||||||
@@ -445,22 +447,25 @@ def create_https_certificates(ssl_cert, ssl_key):
|
|||||||
return False
|
return False
|
||||||
from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA
|
from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA
|
||||||
|
|
||||||
|
issuer = common.PRODUCT
|
||||||
serial = timestamp()
|
serial = timestamp()
|
||||||
|
not_before = 0
|
||||||
|
not_after = 60 * 60 * 24 * 365 * 10 # ten years
|
||||||
domains = ['DNS:' + d.strip() for d in plexpy.CONFIG.HTTPS_DOMAIN.split(',') if d]
|
domains = ['DNS:' + d.strip() for d in plexpy.CONFIG.HTTPS_DOMAIN.split(',') if d]
|
||||||
ips = ['IP:' + d.strip() for d in plexpy.CONFIG.HTTPS_IP.split(',') if d]
|
ips = ['IP:' + d.strip() for d in plexpy.CONFIG.HTTPS_IP.split(',') if d]
|
||||||
altNames = ','.join(domains + ips)
|
alt_names = ','.join(domains + ips).encode('utf-8')
|
||||||
|
|
||||||
# Create the self-signed Tautulli certificate
|
# Create the self-signed Tautulli certificate
|
||||||
logger.debug("Generating self-signed SSL certificate.")
|
logger.debug("Generating self-signed SSL certificate.")
|
||||||
pkey = createKeyPair(TYPE_RSA, 2048)
|
pkey = createKeyPair(TYPE_RSA, 2048)
|
||||||
cert = createSelfSignedCertificate("Tautulli", pkey, serial, 0, 60 * 60 * 24 * 365 * 10, altNames) # ten years
|
cert = createSelfSignedCertificate(issuer, pkey, serial, not_before, not_after, alt_names)
|
||||||
|
|
||||||
# Save the key and certificate to disk
|
# Save the key and certificate to disk
|
||||||
try:
|
try:
|
||||||
with open(ssl_cert, "w") as fp:
|
with open(ssl_cert, "w") as fp:
|
||||||
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8'))
|
||||||
with open(ssl_key, "w") as fp:
|
with open(ssl_key, "w") as fp:
|
||||||
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey).decode('utf-8'))
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error("Error creating SSL key and certificate: %s", e)
|
logger.error("Error creating SSL key and certificate: %s", e)
|
||||||
return False
|
return False
|
||||||
@@ -732,7 +737,7 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = upload(img_data,
|
response = upload((img_title, img_data),
|
||||||
public_id='{}_{}'.format(fallback, rating_key),
|
public_id='{}_{}'.format(fallback, rating_key),
|
||||||
tags=['tautulli', fallback, str(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})
|
||||||
@@ -1063,6 +1068,11 @@ def grouper(iterable, n, fillvalue=None):
|
|||||||
return zip_longest(fillvalue=fillvalue, *args)
|
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):
|
def traverse_map(obj, func):
|
||||||
if isinstance(obj, list):
|
if isinstance(obj, list):
|
||||||
new_obj = []
|
new_obj = []
|
||||||
|
@@ -88,6 +88,8 @@ def refresh_libraries():
|
|||||||
if result == 'insert':
|
if result == 'insert':
|
||||||
new_keys.append(section['section_id'])
|
new_keys.append(section['section_id'])
|
||||||
|
|
||||||
|
add_live_tv_library(refresh=True)
|
||||||
|
|
||||||
query = 'UPDATE library_sections SET is_active = 0 WHERE server_id != ? OR ' \
|
query = 'UPDATE library_sections SET is_active = 0 WHERE server_id != ? OR ' \
|
||||||
'section_id NOT IN ({})'.format(', '.join(['?'] * len(section_ids)))
|
'section_id NOT IN ({})'.format(', '.join(['?'] * len(section_ids)))
|
||||||
monitor_db.action(query=query, args=[plexpy.CONFIG.PMS_IDENTIFIER] + section_ids)
|
monitor_db.action(query=query, args=[plexpy.CONFIG.PMS_IDENTIFIER] + section_ids)
|
||||||
@@ -115,28 +117,28 @@ def refresh_libraries():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def add_live_tv_library():
|
def add_live_tv_library(refresh=False):
|
||||||
if not plexpy.CONFIG.ADD_LIVE_TV_LIBRARY:
|
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
|
return
|
||||||
|
|
||||||
logger.info("Tautulli Libraries :: Adding Live TV library to the database.")
|
logger.info("Tautulli Libraries :: Adding Live TV library to the database.")
|
||||||
|
|
||||||
monitor_db = database.MonitorDatabase()
|
|
||||||
|
|
||||||
section_keys = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
section_keys = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
'section_id': common.LIVE_TV_SECTION_ID}
|
'section_id': common.LIVE_TV_SECTION_ID}
|
||||||
section_values = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
section_values = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
'section_id': common.LIVE_TV_SECTION_ID,
|
'section_id': common.LIVE_TV_SECTION_ID,
|
||||||
'section_name': common.LIVE_TV_SECTION_NAME,
|
'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)
|
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):
|
def has_library_type(section_type):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
@@ -19,6 +19,15 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import plistlib
|
import plistlib
|
||||||
|
|
||||||
|
try:
|
||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
HAS_PYOBJC = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PYOBJC = False
|
||||||
|
|
||||||
|
if HAS_PYOBJC:
|
||||||
import rumps
|
import rumps
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
@@ -59,11 +68,11 @@ class MacOSSystemTray(object):
|
|||||||
self.tray_icon = rumps.App(common.PRODUCT, icon=self.icon, menu=self.menu, quit_button=None)
|
self.tray_icon = rumps.App(common.PRODUCT, icon=self.icon, menu=self.menu, quit_button=None)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
logger.info("Launching MacOS system tray icon.")
|
logger.info("Launching MacOS menu bar icon.")
|
||||||
try:
|
try:
|
||||||
self.tray_icon.run()
|
self.tray_icon.run()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Unable to launch system tray icon: %s." % e)
|
logger.error("Unable to launch menu bar icon: %s." % e)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
rumps.quit_application()
|
rumps.quit_application()
|
||||||
|
@@ -18,6 +18,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from future.builtins import str
|
from future.builtins import str
|
||||||
|
|
||||||
|
import requests
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
@@ -34,6 +35,8 @@ else:
|
|||||||
TEMP_DEVICE_TOKEN = None
|
TEMP_DEVICE_TOKEN = None
|
||||||
INVALIDATE_TIMER = None
|
INVALIDATE_TIMER = None
|
||||||
|
|
||||||
|
_ONESIGNAL_APP_ID = '3b4b666a-d557-4b92-acdf-e2c8c4b95357'
|
||||||
|
|
||||||
|
|
||||||
def set_temp_device_token(token=None):
|
def set_temp_device_token(token=None):
|
||||||
global TEMP_DEVICE_TOKEN
|
global TEMP_DEVICE_TOKEN
|
||||||
@@ -84,7 +87,8 @@ def add_mobile_device(device_id=None, device_name=None, device_token=None, frien
|
|||||||
|
|
||||||
keys = {'device_id': device_id}
|
keys = {'device_id': device_id}
|
||||||
values = {'device_name': device_name,
|
values = {'device_name': device_name,
|
||||||
'device_token': device_token}
|
'device_token': device_token,
|
||||||
|
'official': validate_device_id(device_id=device_id)}
|
||||||
|
|
||||||
if friendly_name:
|
if friendly_name:
|
||||||
values['friendly_name'] = friendly_name
|
values['friendly_name'] = friendly_name
|
||||||
@@ -161,6 +165,14 @@ def set_last_seen(device_token=None):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def validate_device_id(device_id):
|
||||||
|
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)
|
||||||
|
return r.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def blacklist_logger():
|
def blacklist_logger():
|
||||||
devices = get_mobile_devices()
|
devices = get_mobile_devices()
|
||||||
for d in devices:
|
for d in devices:
|
||||||
|
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from io import open
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
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):
|
if newsletter_file in os.listdir(newsletter_folder):
|
||||||
try:
|
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()
|
newsletter = n_file.read()
|
||||||
return newsletter
|
return newsletter
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
@@ -882,8 +882,6 @@ class ANDROIDAPP(Notifier):
|
|||||||
'priority': 3
|
'priority': 3
|
||||||
}
|
}
|
||||||
|
|
||||||
_ONESIGNAL_APP_ID = '3b4b666a-d557-4b92-acdf-e2c8c4b95357'
|
|
||||||
|
|
||||||
def agent_notify(self, subject='', body='', action='', notification_id=None, **kwargs):
|
def agent_notify(self, subject='', body='', action='', notification_id=None, **kwargs):
|
||||||
# Check mobile device is still registered
|
# Check mobile device is still registered
|
||||||
device = mobile_app.get_mobile_devices(device_id=self.config['device_id'])
|
device = mobile_app.get_mobile_devices(device_id=self.config['device_id'])
|
||||||
@@ -930,7 +928,7 @@ class ANDROIDAPP(Notifier):
|
|||||||
#logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce)))
|
#logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce)))
|
||||||
#logger.debug("Salt (base64): {}".format(base64.b64encode(salt)))
|
#logger.debug("Salt (base64): {}".format(base64.b64encode(salt)))
|
||||||
|
|
||||||
payload = {'app_id': self._ONESIGNAL_APP_ID,
|
payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
|
||||||
'include_player_ids': [self.config['device_id']],
|
'include_player_ids': [self.config['device_id']],
|
||||||
'contents': {'en': 'Tautulli Notification'},
|
'contents': {'en': 'Tautulli Notification'},
|
||||||
'data': {'encrypted': True,
|
'data': {'encrypted': True,
|
||||||
@@ -943,7 +941,7 @@ class ANDROIDAPP(Notifier):
|
|||||||
"Android app notifications will be sent unecrypted. "
|
"Android app notifications will be sent unecrypted. "
|
||||||
"Install the library to encrypt the notifications.")
|
"Install the library to encrypt the notifications.")
|
||||||
|
|
||||||
payload = {'app_id': self._ONESIGNAL_APP_ID,
|
payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
|
||||||
'include_player_ids': [self.config['device_id']],
|
'include_player_ids': [self.config['device_id']],
|
||||||
'contents': {'en': 'Tautulli Notification'},
|
'contents': {'en': 'Tautulli Notification'},
|
||||||
'data': {'encrypted': False,
|
'data': {'encrypted': False,
|
||||||
@@ -960,7 +958,7 @@ class ANDROIDAPP(Notifier):
|
|||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query = 'SELECT * FROM mobile_devices'
|
query = 'SELECT * FROM mobile_devices WHERE official = 1'
|
||||||
result = db.select(query=query)
|
result = db.select(query=query)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn("Tautulli Notifiers :: Unable to retrieve Android app devices list: %s." % e)
|
logger.warn("Tautulli Notifiers :: Unable to retrieve Android app devices list: %s." % e)
|
||||||
@@ -1123,17 +1121,22 @@ class BROWSER(Notifier):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _return_config_options(self):
|
def _return_config_options(self):
|
||||||
config_option = [{'label': 'Allow Notifications',
|
config_option = [{'label': 'Note',
|
||||||
|
'description': 'You may need to refresh the page after saving for changes to take effect.',
|
||||||
|
'input_type': 'help'
|
||||||
|
},
|
||||||
|
{'label': 'Allow Notifications',
|
||||||
'value': 'Allow Notifications',
|
'value': 'Allow Notifications',
|
||||||
'name': 'browser_allow_browser',
|
'name': 'browser_allow_browser',
|
||||||
'description': 'Click to allow browser notifications. You must click this button for each browser.',
|
'description': 'Click to allow browser notifications. '
|
||||||
|
'You must click this button for each browser.',
|
||||||
'input_type': 'button'
|
'input_type': 'button'
|
||||||
},
|
},
|
||||||
{'label': 'Auto Hide Delay',
|
{'label': 'Auto Hide Delay',
|
||||||
'value': self.config['auto_hide_delay'],
|
'value': self.config['auto_hide_delay'],
|
||||||
'name': 'browser_auto_hide_delay',
|
'name': 'browser_auto_hide_delay',
|
||||||
'description': 'Set the number of seconds for the notification to remain visible. \
|
'description': 'Set the number of seconds for the notification to remain visible. '
|
||||||
Set 0 to disable auto hiding. (Note: Some browsers have a maximum time limit.)',
|
'Set 0 to disable auto hiding. (Note: Some browsers have a maximum time limit.)',
|
||||||
'input_type': 'number'
|
'input_type': 'number'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1396,8 +1399,7 @@ class EMAIL(Notifier):
|
|||||||
success = True
|
success = True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Notifiers :: {name} notification failed: {e}".format(
|
logger.error("Tautulli Notifiers :: %s notification failed: %s", self.NAME, e)
|
||||||
name=self.NAME, e=str(e).decode('utf-8')))
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if mailserver:
|
if mailserver:
|
||||||
@@ -2967,7 +2969,7 @@ class SCRIPTS(Notifier):
|
|||||||
'.php': 'php',
|
'.php': 'php',
|
||||||
'.pl': 'perl',
|
'.pl': 'perl',
|
||||||
'.ps1': 'powershell -executionPolicy bypass -file',
|
'.ps1': 'powershell -executionPolicy bypass -file',
|
||||||
'.py': 'python',
|
'.py': 'python' if plexpy.FROZEN else sys.executable,
|
||||||
'.pyw': 'pythonw',
|
'.pyw': 'pythonw',
|
||||||
'.rb': 'ruby',
|
'.rb': 'ruby',
|
||||||
'.sh': ''
|
'.sh': ''
|
||||||
@@ -3005,7 +3007,7 @@ class SCRIPTS(Notifier):
|
|||||||
'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT,
|
'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT,
|
||||||
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY,
|
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY,
|
||||||
'TAUTULLI_ENCODING': plexpy.SYS_ENCODING,
|
'TAUTULLI_ENCODING': plexpy.SYS_ENCODING,
|
||||||
'TAUTULLI_PYTHON_VERSION': '.'.join(map(str, plexpy.PYTHON_VERSION))
|
'TAUTULLI_PYTHON_VERSION': common.PYTHON_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
if user_id:
|
if user_id:
|
||||||
|
@@ -17,5 +17,5 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
PLEXPY_BRANCH = "beta"
|
PLEXPY_BRANCH = "master"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.5.0-beta"
|
PLEXPY_RELEASE_VERSION = "v2.5.3"
|
@@ -41,6 +41,13 @@ else:
|
|||||||
from plexpy.users import Users, refresh_users
|
from plexpy.users import Users, refresh_users
|
||||||
from plexpy.plextv import PlexTV
|
from plexpy.plextv import PlexTV
|
||||||
|
|
||||||
|
# Monkey patch SameSite support into cookies.
|
||||||
|
# https://stackoverflow.com/a/50813092
|
||||||
|
try:
|
||||||
|
from http.cookies import Morsel
|
||||||
|
except ImportError:
|
||||||
|
from Cookie import Morsel
|
||||||
|
Morsel._reserved[str('samesite')] = str('SameSite')
|
||||||
|
|
||||||
JWT_ALGORITHM = 'HS256'
|
JWT_ALGORITHM = 'HS256'
|
||||||
JWT_COOKIE_NAME = 'tautulli_token_'
|
JWT_COOKIE_NAME = 'tautulli_token_'
|
||||||
@@ -141,7 +148,7 @@ def check_credentials(username=None, password=None, token=None, admin_login='0',
|
|||||||
return True, user_details, 'admin'
|
return True, user_details, 'admin'
|
||||||
|
|
||||||
if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS):
|
if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS):
|
||||||
plex_login = plex_user_login(username=username, password=password, token=token, headers=headers)
|
plex_login = plex_user_login(token=token, headers=headers)
|
||||||
if plex_login is not None:
|
if plex_login is not None:
|
||||||
return True, plex_login[0], plex_login[1]
|
return True, plex_login[0], plex_login[1]
|
||||||
|
|
||||||
@@ -296,9 +303,13 @@ class AuthController(object):
|
|||||||
self.on_logout(payload['user'], payload['user_group'])
|
self.on_logout(payload['user'], payload['user_group'])
|
||||||
|
|
||||||
jwt_cookie = str(JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID)
|
jwt_cookie = str(JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID)
|
||||||
cherrypy.response.cookie[jwt_cookie] = 'expire'
|
cherrypy.response.cookie[jwt_cookie] = ''
|
||||||
cherrypy.response.cookie[jwt_cookie]['expires'] = 0
|
cherrypy.response.cookie[jwt_cookie]['expires'] = 0
|
||||||
cherrypy.response.cookie[jwt_cookie]['path'] = '/'
|
cherrypy.response.cookie[jwt_cookie]['path'] = plexpy.HTTP_ROOT.rstrip('/') or '/'
|
||||||
|
|
||||||
|
if plexpy.HTTP_ROOT != '/':
|
||||||
|
# Also expire the JWT on the root path
|
||||||
|
cherrypy.response.headers['Set-Cookie'] = jwt_cookie + '=""; expires=Thu, 01 Jan 1970 12:00:00 GMT; path=/'
|
||||||
|
|
||||||
cherrypy.request.login = None
|
cherrypy.request.login = None
|
||||||
|
|
||||||
@@ -344,7 +355,9 @@ class AuthController(object):
|
|||||||
jwt_cookie = str(JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID)
|
jwt_cookie = str(JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID)
|
||||||
cherrypy.response.cookie[jwt_cookie] = jwt_token
|
cherrypy.response.cookie[jwt_cookie] = jwt_token
|
||||||
cherrypy.response.cookie[jwt_cookie]['expires'] = int(time_delta.total_seconds())
|
cherrypy.response.cookie[jwt_cookie]['expires'] = int(time_delta.total_seconds())
|
||||||
cherrypy.response.cookie[jwt_cookie]['path'] = '/'
|
cherrypy.response.cookie[jwt_cookie]['path'] = plexpy.HTTP_ROOT.rstrip('/') or '/'
|
||||||
|
cherrypy.response.cookie[jwt_cookie]['httponly'] = True
|
||||||
|
cherrypy.response.cookie[jwt_cookie]['samesite'] = 'lax'
|
||||||
|
|
||||||
cherrypy.request.login = payload
|
cherrypy.request.login = payload
|
||||||
cherrypy.response.status = 200
|
cherrypy.response.status = 200
|
||||||
|
33
start.sh
33
start.sh
@@ -1,2 +1,33 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
python Tautulli.py &> /dev/null &
|
|
||||||
|
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
|
||||||
|
|
||||||
|
user=$(getent passwd "$PUID" | cut -d: -f1)
|
||||||
|
group=$(getent group "$PGID" | cut -d: -f1)
|
||||||
|
usermod -a -G root "$user"
|
||||||
|
|
||||||
|
chown -R "$user":"$group" /config
|
||||||
|
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
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