Compare commits
39 Commits
v2.5.0-bet
...
v2.5.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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
|
||||||
|
35
.github/workflows/publish-release.yml
vendored
35
.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,14 +50,15 @@ 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 }}.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 }}.exe
|
||||||
@@ -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') }}
|
||||||
@@ -119,7 +120,7 @@ jobs:
|
|||||||
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 }}.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 }}.pkg
|
||||||
@@ -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,7 +188,7 @@ 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 }}.exe
|
||||||
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
|
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
|
||||||
asset_content_type: application/vnd.microsoft.portable-executable
|
asset_content_type: application/vnd.microsoft.portable-executable
|
||||||
|
|
||||||
@@ -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 }}.pkg
|
||||||
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
|
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
|
||||||
asset_content_type: application/vnd.apple.installer+xml
|
asset_content_type: application/vnd.apple.installer+xml
|
||||||
|
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,16 +1,30 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
# v2.5.0-beta (2020-05-31)
|
## 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
|
||||||
|
@@ -257,15 +257,13 @@ 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()
|
threading.Thread(target=wait).start()
|
||||||
plexpy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
|
plexpy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
|
||||||
|
@@ -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();
|
|
||||||
});
|
|
@@ -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>
|
||||||
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span>
|
% if device['official']:
|
||||||
|
<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);
|
||||||
}
|
}
|
||||||
|
@@ -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">
|
||||||
|
@@ -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
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
dialogText=`osascript -e 'set dialogText to button returned of (display dialog "Installation complete. Start Tautulli?" buttons {"Start", "Close"})'`;
|
dialogText=`osascript -e 'set dialogText to button returned of (display dialog "Installation complete. Start Tautulli?" buttons {"Start", "Close"})'`;
|
||||||
if [[ $dialogText == 'Start' ]]
|
if [[ $dialogText == 'Start' ]]
|
||||||
then
|
then
|
||||||
open /Applications/Tautulli.app
|
open /Applications/Tautulli.app
|
||||||
else
|
else
|
||||||
exit 0;
|
exit 0;
|
||||||
fi
|
fi
|
||||||
|
@@ -745,7 +745,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 +2001,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')
|
||||||
|
@@ -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
|
||||||
|
@@ -104,6 +104,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 +120,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',
|
||||||
|
@@ -348,7 +348,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 +943,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
|
||||||
|
@@ -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})
|
||||||
|
@@ -19,7 +19,16 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import plistlib
|
import plistlib
|
||||||
import rumps
|
|
||||||
|
try:
|
||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
HAS_PYOBJC = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PYOBJC = False
|
||||||
|
|
||||||
|
if HAS_PYOBJC:
|
||||||
|
import rumps
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
if plexpy.PYTHON2:
|
if plexpy.PYTHON2:
|
||||||
@@ -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:
|
||||||
|
@@ -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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@@ -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.2"
|
@@ -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
|
||||||
|
37
start.sh
37
start.sh
@@ -1,2 +1,37 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
python Tautulli.py &> /dev/null &
|
|
||||||
|
if [[ "$TAUTULLI_DOCKER" = "True" ]]; then
|
||||||
|
if [[ -v PUID && -v 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
|
||||||
|
if which python3 >/dev/null; then
|
||||||
|
python3 Tautulli.py &> /dev/null &
|
||||||
|
elif which python3.8 >/dev/null; then
|
||||||
|
python3.8 Tautulli.py &> /dev/null &
|
||||||
|
elif which python3.7 >/dev/null; then
|
||||||
|
python3.7 Tautulli.py &> /dev/null &
|
||||||
|
elif which python3.6 >/dev/null; then
|
||||||
|
python3.6 Tautulli.py &> /dev/null &
|
||||||
|
elif which python >/dev/null; then
|
||||||
|
python Tautulli.py &> /dev/null &
|
||||||
|
elif which python2 >/dev/null; then
|
||||||
|
python2 Tautulli.py &> /dev/null &
|
||||||
|
elif which python2.7 >/dev/null; then
|
||||||
|
python2.7 Tautulli.py &> /dev/null &
|
||||||
|
else
|
||||||
|
echo "Cannot start Tautulli: python not found."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
Reference in New Issue
Block a user