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
|
||||
*.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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
@@ -29,16 +32,24 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
uses: crazy-max/ghaction-docker-buildx@v3
|
||||
with:
|
||||
version: latest
|
||||
buildx-version: latest
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2.1.0
|
||||
- name: Cache Docker Layers
|
||||
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)
|
||||
run: |
|
||||
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 }} \
|
||||
--output "type=image,push=false" \
|
||||
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
|
||||
@@ -59,6 +70,7 @@ jobs:
|
||||
if: success()
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--platform ${{ steps.prepare.outputs.docker_platforms }} \
|
||||
--output "type=image,push=true" \
|
||||
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
|
||||
@@ -79,5 +91,5 @@ jobs:
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
job: ${{ github.workflow }}
|
||||
title: ${{ github.workflow }}
|
||||
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
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2.1.0
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set Release Version
|
||||
id: get_version
|
||||
@@ -28,13 +28,13 @@ jobs:
|
||||
echo $GITHUB_SHA > version.txt
|
||||
|
||||
- name: Set Up Python
|
||||
uses: actions/setup-python@v1.2.0
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Cache Dependencies
|
||||
id: cache_dependencies
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~\AppData\Local\pip\Cache
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-windows.txt') }}
|
||||
@@ -50,17 +50,18 @@ jobs:
|
||||
pyinstaller -y ./package/Tautulli-windows.spec
|
||||
|
||||
- name: Create Installer
|
||||
uses: joncloud/makensis-action@v1
|
||||
uses: joncloud/makensis-action@v1.2
|
||||
with:
|
||||
script-file: ./package/Tautulli.nsi
|
||||
arguments: /DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }} /DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
|
||||
includeMorePlugins: package/nsis-plugins
|
||||
arguments: /DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }} /DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
include-more-plugins: true
|
||||
include-custom-plugins-path: package/nsis-plugins
|
||||
|
||||
- name: Upload Installer
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Tautulli-windows-installer
|
||||
path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
|
||||
path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
@@ -68,14 +69,14 @@ jobs:
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
job: Build Windows Installer
|
||||
title: Build Windows Installer
|
||||
nofail: true
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2.1.0
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set Release Version
|
||||
id: get_version
|
||||
@@ -93,13 +94,13 @@ jobs:
|
||||
echo $GITHUB_SHA > version.txt
|
||||
|
||||
- name: Set Up Python
|
||||
uses: actions/setup-python@v1.2.0
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Cache Dependencies
|
||||
id: cache_dependencies
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/Library/Caches/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-macos.txt') }}
|
||||
@@ -116,13 +117,13 @@ jobs:
|
||||
|
||||
- name: Create Installer
|
||||
run: |
|
||||
sudo pkgbuild --install-location /Applications --version ${{ steps.get_version.outputs.VERSION }} --component ./dist/Tautulli.app --scripts ./package/macos-scripts Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
|
||||
sudo pkgbuild --install-location /Applications --version ${{ steps.get_version.outputs.VERSION }} --component ./dist/Tautulli.app --scripts ./package/macos-scripts Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
|
||||
- name: Upload Installer
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Tautulli-macos-installer
|
||||
path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
|
||||
path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
@@ -130,7 +131,7 @@ jobs:
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
job: Build MacOS Installer
|
||||
title: Build MacOS Installer
|
||||
nofail: true
|
||||
|
||||
release:
|
||||
@@ -142,7 +143,7 @@ jobs:
|
||||
uses: technote-space/workflow-conclusion-action@v1
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2.1.0
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set Release Version
|
||||
id: get_version
|
||||
@@ -151,13 +152,13 @@ jobs:
|
||||
|
||||
- name: Download Windows Installer
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/download-artifact@v1
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: Tautulli-windows-installer
|
||||
|
||||
- name: Download MacOS Installer
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/download-artifact@v1
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: Tautulli-macos-installer
|
||||
|
||||
@@ -187,8 +188,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: Tautulli-windows-installer/Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
|
||||
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe
|
||||
asset_path: ./Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
|
||||
- name: Upload MacOS Installer
|
||||
@@ -198,6 +199,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: Tautulli-macos-installer/Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
|
||||
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg
|
||||
asset_path: ./Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
asset_content_type: application/vnd.apple.installer+xml
|
||||
|
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,16 +1,46 @@
|
||||
# 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:
|
||||
* Tautulli now supports Python 3!
|
||||
* Python 2 is still supported for the time being, but it is recommended to upgrade to Python 3.
|
||||
* UI:
|
||||
* Notifications:
|
||||
* Fix: Error uploading images to Cloudinary on Python 2.
|
||||
* Fix: Testing browser notifications alert not disappearing.
|
||||
* Change: Default recently added notification delay set to 300 seconds.
|
||||
* UI:
|
||||
* Fix: MacOS menu bar icon causing Tautulli to fail to start.
|
||||
* Fix: Unable to login to Tautulli on Python 2.
|
||||
* New: Windows and MacOS setting to enable Tautulli to start automatically when you login.
|
||||
* New: Added system tray icon for MacOS.
|
||||
* New: Added menu bar icon for MacOS.
|
||||
* New: Ability to import a Tautulli database in the settings.
|
||||
* New: Added Tautulli news area on the settings page.
|
||||
* New: Added platform icon for LG devices.
|
||||
* Remove: Ability to login to Tautulli using a Plex username and password has been removed. Login using a Plex.tv account is only supported via OAuth.
|
||||
* Mobile App:
|
||||
* Fix: Improved API security and validation when registering the Android app.
|
||||
* Docker:
|
||||
* Fix: Docker container not respecting the PUID and PGID environment variables.
|
||||
* Other:
|
||||
* Fix: Error creating self-signed certificates on Python 3.
|
||||
* Fix: Tautulli login session cookie not set on the HTTP root path.
|
||||
* New: Windows and MacOS app installers to install Tautulli without needing Python installed.
|
||||
|
||||
|
||||
|
@@ -16,7 +16,7 @@ RUN \
|
||||
|
||||
COPY . /app
|
||||
|
||||
CMD [ "python", "Tautulli.py", "--datadir", "/config" ]
|
||||
ENTRYPOINT [ "./start.sh" ]
|
||||
|
||||
VOLUME /config
|
||||
EXPOSE 8181
|
||||
|
13
Tautulli.py
13
Tautulli.py
@@ -257,17 +257,18 @@ def main():
|
||||
plexpy.HTTP_ROOT)
|
||||
|
||||
if common.PLATFORM == 'Darwin' and plexpy.CONFIG.SYS_TRAY_ICON:
|
||||
try:
|
||||
import AppKit
|
||||
except ImportError:
|
||||
if not macos.HAS_PYOBJC:
|
||||
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
|
||||
|
||||
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
|
||||
threading.Thread(target=wait).start()
|
||||
thread = threading.Thread(target=wait)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
plexpy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
|
||||
plexpy.MAC_SYS_TRAY_ICON.start()
|
||||
else:
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Display information
|
||||
echo "This script will remove *.pyc files. These files are generated by Python, but they can cause conflicts after an upgrade. It's safe to remove them, because they will be regenerated."
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Parameter check
|
||||
if [ -z "$1" ]; then
|
||||
|
@@ -230,20 +230,12 @@ ${next.modalIncludes()}
|
||||
</div>
|
||||
</div>
|
||||
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
|
||||
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
|
||||
<li><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li>
|
||||
<li class="active"><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li>
|
||||
<li><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
|
||||
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center">
|
||||
<p>
|
||||
Click the button below to continue to Patreon.
|
||||
</p>
|
||||
<a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank">
|
||||
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
|
||||
</a>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="github-donation" style="text-align: center">
|
||||
<div role="tabpanel" class="tab-pane active" id="github-donation" style="text-align: center">
|
||||
<p>
|
||||
Click the button below to continue to GitHub.
|
||||
</p>
|
||||
@@ -251,6 +243,14 @@ ${next.modalIncludes()}
|
||||
<i class="fa fa-heart fa-sm" style="color: #ea4aaa;"></i> Sponsor
|
||||
</a>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="patreon-donation" style="text-align: center">
|
||||
<p>
|
||||
Click the button below to continue to Patreon.
|
||||
</p>
|
||||
<a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank">
|
||||
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
|
||||
</a>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
|
||||
<p>
|
||||
Click the button below to continue to PayPal.
|
||||
@@ -296,9 +296,7 @@ ${next.modalIncludes()}
|
||||
<script src="${http_root}js/ipaddr.min.js"></script>
|
||||
<script src="${http_root}js/script.js${cache_param}"></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>
|
||||
% endif
|
||||
<script>
|
||||
% if _session['user_group'] == 'admin':
|
||||
$('body').on('click', '#updateDismiss', function() {
|
||||
@@ -423,6 +421,10 @@ ${next.modalIncludes()}
|
||||
$(document).on('hidden.bs.modal', '.modal', function () {
|
||||
$('.modal:visible').length && $(document.body).addClass('modal-open');
|
||||
});
|
||||
|
||||
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
||||
check_notifications();
|
||||
% endif
|
||||
});
|
||||
|
||||
% if _session['user_group'] != 'admin':
|
||||
|
@@ -2161,7 +2161,7 @@ div.advanced-setting {
|
||||
li.advanced-setting {
|
||||
border-left: 1px solid #cc7b19;
|
||||
}
|
||||
.docker-setting {
|
||||
.setting-message {
|
||||
color: #cc7b19;
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -2312,6 +2312,7 @@ li.advanced-setting {
|
||||
width: 140px;
|
||||
margin-left: 10px;
|
||||
margin-bottom: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.user-player-instance-playcount h3 {
|
||||
font-size: 30px;
|
||||
@@ -3873,6 +3874,10 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-color: #31afe1;
|
||||
background-image: url(../images/platforms/kodi.svg);
|
||||
}
|
||||
.platform-lg {
|
||||
background-color: #a50034;
|
||||
background-image: url(../images/platforms/lg.svg);
|
||||
}
|
||||
.platform-linux {
|
||||
background-color: #1793d0;
|
||||
background-image: url(../images/platforms/linux.svg);
|
||||
@@ -3974,6 +3979,9 @@ a:hover .overlay-refresh-image:hover {
|
||||
.platform-kodi-rgba {
|
||||
background-color: rgba(49, 175, 225, 0.40);
|
||||
}
|
||||
.platform-lg-rgba {
|
||||
background-color: rgba(165, 0, 52, 0.40);
|
||||
}
|
||||
.platform-linux-rgba {
|
||||
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();
|
||||
}, 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="parent_count">Total Seasons / Albums</th>
|
||||
<th align="left" id="child_count">Total Episodes / Tracks</th>
|
||||
<th align="left" id="last_accessed">Last Accessed</th>
|
||||
<th align="left" id="last_accessed">Last Streamed</th>
|
||||
<th align="left" id="last_played">Last Played</th>
|
||||
<th align="left" id="total_plays">Total Plays</th>
|
||||
<th align="left" id="total_duration">Total Played Duration</th>
|
||||
|
@@ -24,7 +24,7 @@
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- 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">
|
||||
<!-- Apple -->
|
||||
<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']):
|
||||
<li class="mobile-device pointer" data-id="${device['id']}" data-name="${device['device_name']}">
|
||||
<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>
|
||||
<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']}">
|
||||
@@ -138,4 +142,6 @@ DOCUMENTATION :: END
|
||||
}
|
||||
verifiedDevice = true;
|
||||
})
|
||||
|
||||
$('.officail-tooltip').tooltip();
|
||||
</script>
|
@@ -853,10 +853,7 @@
|
||||
PNotify.prototype.options.hide = true;
|
||||
PNotify.prototype.options.delay = $('#browser_auto_hide_delay').val() * 1000;
|
||||
}
|
||||
var notification = new PNotify({
|
||||
title: $('#test_subject').val(),
|
||||
text: $('#test_body').val()
|
||||
});
|
||||
displayPNotify($('#test_subject').val(), $('#test_body').val());
|
||||
showMsg('<i class="fa fa-check"></i> Notification sent.', false, true, 5000);
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
from plexpy.helpers import anon_url, checked
|
||||
|
||||
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_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>
|
||||
% 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">
|
||||
<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>
|
||||
<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 class="checkbox">
|
||||
<label>
|
||||
@@ -1047,7 +1056,7 @@
|
||||
</div>
|
||||
<div id="notify_recently_added_delay_error" class="alert alert-danger settings-alert" role="alert"></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 class="form-group advanced-setting">
|
||||
<label>Flush Recently Added</label>
|
||||
@@ -1426,6 +1435,7 @@
|
||||
<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 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 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>
|
||||
@@ -1861,7 +1871,10 @@ Rating: {rating}/10 --> Rating: /10
|
||||
<label>Instructions</label>
|
||||
<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)
|
||||
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>
|
||||
<label>QR Code</label>
|
||||
<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');
|
||||
}
|
||||
function OAuthSuccessCallback(authToken) {
|
||||
var x_plex_headers = getPlexHeaders();
|
||||
$("#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');
|
||||
getServerOptions(authToken);
|
||||
}
|
||||
|
@@ -284,7 +284,7 @@ DOCUMENTATION :: END
|
||||
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="last_seen">Last Seen</th>
|
||||
<th align="left" id="last_seen">Last Streamed</th>
|
||||
<th align="left" id="ip_address">IP Address</th>
|
||||
<th align="left" id="platform">Last Platform</th>
|
||||
<th align="left" id="player">Last Player</th>
|
||||
|
@@ -27,7 +27,7 @@ DOCUMENTATION :: END
|
||||
<div id="user-player-image-${a['result_id']}">
|
||||
<div class="user-player-instance-box svg-icon platform-${a['platform_name']}"></div>
|
||||
</div>
|
||||
<div class="user-player-instance-name">
|
||||
<div class="user-player-instance-name" title="${a['player_name']}">
|
||||
${a['player_name']}
|
||||
</div>
|
||||
<div class="user-player-instance-playcount">
|
||||
|
@@ -34,7 +34,7 @@
|
||||
<th align="left" id="edit_row">Edit</th>
|
||||
<th align="right" id="avatar"></th>
|
||||
<th align="left" id="friendly_name">User</th>
|
||||
<th align="left" id="last_seen">Last Seen</th>
|
||||
<th align="left" id="last_seen">Last Streamed</th>
|
||||
<th align="left" id="last_known_ip">Last Known IP</th>
|
||||
<th align="left" id="last_platform">Last Platform</th>
|
||||
<th align="left" id="last_player">Last Player</th>
|
||||
|
@@ -38,6 +38,7 @@ load_rc_config ${name}
|
||||
status_cmd="${name}_status"
|
||||
stop_cmd="${name}_stop"
|
||||
|
||||
command_interpreter="/usr/local/bin/python3"
|
||||
command="${tautulli_dir}/Tautulli.py"
|
||||
command_args="--daemon --pidfile ${tautulli_pid} --quiet --nolaunch ${tautulli_flags}"
|
||||
|
||||
|
@@ -31,11 +31,13 @@
|
||||
# sudo chown tautulli:tautulli -R /opt/Tautulli
|
||||
#
|
||||
# - Adjust ExecStart= to point to:
|
||||
# 1. Your Tautulli executable
|
||||
# 1. Your Python interpreter (get the path with "command -v python3")
|
||||
# - Default: /usr/bin/python3
|
||||
# 2. Your Tautulli executable
|
||||
# - Default: /opt/Tautulli/Tautulli.py
|
||||
# 2. Your config file (recommended is to put it somewhere in /etc)
|
||||
# 3. Your config file (recommended is to put it somewhere in /etc)
|
||||
# - Default: --config /opt/Tautulli/config.ini
|
||||
# 3. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
|
||||
# 4. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
|
||||
# - Default: --datadir /opt/Tautulli
|
||||
#
|
||||
# - Adjust User= and Group= to the user/group you want Tautulli to run as.
|
||||
@@ -50,7 +52,7 @@ Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch
|
||||
ExecStart=/usr/bin/python3 /opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch
|
||||
GuessMainPID=no
|
||||
Type=forking
|
||||
User=tautulli
|
||||
|
@@ -100,7 +100,7 @@ def createSelfSignedCertificate(issuerName, issuerKey, serial, notBefore, notAft
|
||||
cert.set_pubkey(issuerKey)
|
||||
|
||||
if altNames:
|
||||
cert.add_extensions([crypto.X509Extension("subjectAltName", False, altNames)])
|
||||
cert.add_extensions([crypto.X509Extension(b"subjectAltName", False, altNames)])
|
||||
|
||||
cert.sign(issuerKey, digest)
|
||||
return cert
|
||||
|
@@ -16,10 +16,10 @@ analysis = Analysis(
|
||||
('../CHANGELOG.md', '.'),
|
||||
('../LICENSE', '.'),
|
||||
('../version.txt', '.'),
|
||||
('../lib/ipwhois/data', 'data')
|
||||
('../lib/ipwhois/data', 'ipwhois/data')
|
||||
],
|
||||
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
|
||||
hiddenimports=['Foundation', 'AppKit'],
|
||||
hiddenimports=['pkg_resources.py2_warn', 'Foundation', 'AppKit', 'cheroot.ssl', 'cheroot.ssl.builtin'],
|
||||
cipher=block_cipher
|
||||
)
|
||||
pyz = PYZ(
|
||||
@@ -47,5 +47,9 @@ app = BUNDLE(
|
||||
name='Tautulli.app',
|
||||
icon='../data/interfaces/default/images/logo-circle.icns',
|
||||
bundle_identifier='com.Tautulli.Tautulli',
|
||||
version=VERSION
|
||||
version=VERSION,
|
||||
info_plist={
|
||||
'LSBackgroundOnly': True,
|
||||
'LSUIElement': True
|
||||
}
|
||||
)
|
||||
|
@@ -16,6 +16,7 @@ analysis = Analysis(
|
||||
('..\\lib\\ipwhois\\data', 'data')
|
||||
],
|
||||
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
|
||||
hiddenimports=['cheroot.ssl', 'cheroot.ssl.builtin'],
|
||||
cipher=block_cipher,
|
||||
)
|
||||
pyz = PYZ(
|
||||
|
@@ -3,7 +3,7 @@
|
||||
dialogText=`osascript -e 'set dialogText to button returned of (display dialog "Installation complete. Start Tautulli?" buttons {"Start", "Close"})'`;
|
||||
if [[ $dialogText == 'Start' ]]
|
||||
then
|
||||
open /Applications/Tautulli.app
|
||||
open /Applications/Tautulli.app
|
||||
else
|
||||
exit 0;
|
||||
fi
|
||||
|
@@ -1,4 +1,4 @@
|
||||
pyinstaller
|
||||
pyopenssl
|
||||
pycryptodomex
|
||||
pyobjc
|
||||
pyobjc-framework-Cocoa
|
@@ -37,8 +37,7 @@ from apscheduler.triggers.interval import IntervalTrigger
|
||||
from UniversalAnalytics import Tracker
|
||||
import pytz
|
||||
|
||||
PYTHON_VERSION = sys.version_info[:3]
|
||||
PYTHON2 = PYTHON_VERSION[0] == 2
|
||||
PYTHON2 = sys.version_info[0] == 2
|
||||
|
||||
if PYTHON2:
|
||||
import activity_handler
|
||||
@@ -745,7 +744,7 @@ def dbcheck():
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS mobile_devices (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
'device_id TEXT NOT NULL UNIQUE, device_token TEXT, device_name TEXT, friendly_name TEXT, '
|
||||
'last_seen INTEGER)'
|
||||
'last_seen INTEGER, official INTEGER DEFAULT 0)'
|
||||
)
|
||||
|
||||
# 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'
|
||||
)
|
||||
|
||||
# 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
|
||||
try:
|
||||
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)
|
||||
os.remove(PIDFILE)
|
||||
|
||||
if WIN_SYS_TRAY_ICON:
|
||||
WIN_SYS_TRAY_ICON.shutdown()
|
||||
elif MAC_SYS_TRAY_ICON:
|
||||
MAC_SYS_TRAY_ICON.shutdown()
|
||||
|
||||
if restart:
|
||||
logger.info("Tautulli is restarting...")
|
||||
|
||||
@@ -2225,7 +2232,7 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
|
||||
# https://bugs.python.org/issue19066
|
||||
if NOFORK:
|
||||
pass
|
||||
elif common.PLATFORM == 'Windows':
|
||||
elif common.PLATFORM in ('Windows', 'Darwin'):
|
||||
subprocess.Popen(args, cwd=os.getcwd())
|
||||
else:
|
||||
os.execv(exe, args)
|
||||
@@ -2235,6 +2242,11 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
|
||||
|
||||
logger.shutdown()
|
||||
|
||||
if WIN_SYS_TRAY_ICON:
|
||||
WIN_SYS_TRAY_ICON.shutdown()
|
||||
elif MAC_SYS_TRAY_ICON:
|
||||
MAC_SYS_TRAY_ICON.shutdown()
|
||||
|
||||
os._exit(0)
|
||||
|
||||
|
||||
@@ -2251,6 +2263,7 @@ def initialize_tracker():
|
||||
'appInstallerId': CONFIG.GIT_BRANCH,
|
||||
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform
|
||||
'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro
|
||||
'dimension3': common.PYTHON_VERSION,
|
||||
'userLanguage': SYS_LANGUAGE,
|
||||
'documentEncoding': SYS_ENCODING,
|
||||
'noninteractive': True
|
||||
|
@@ -136,7 +136,11 @@ class API2(object):
|
||||
self._api_app = True
|
||||
|
||||
if plexpy.CONFIG.API_ENABLED and not self._api_msg or self._api_cmd in ('get_apikey', 'docs', 'docs_md'):
|
||||
if self._api_apikey == plexpy.CONFIG.API_KEY 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
|
||||
|
||||
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:
|
||||
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'
|
||||
return
|
||||
|
||||
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'
|
||||
return
|
||||
|
||||
@@ -422,7 +426,7 @@ class API2(object):
|
||||
self._api_result_type = 'success'
|
||||
mobile_app.set_temp_device_token(None)
|
||||
else:
|
||||
self._api_msg = 'Device registartion failed: database error.'
|
||||
self._api_msg = 'Device registration failed: database error.'
|
||||
self._api_result_type = 'error'
|
||||
|
||||
return
|
||||
|
@@ -35,6 +35,7 @@ PLATFORM_RELEASE = platform.release()
|
||||
PLATFORM_VERSION = platform.version()
|
||||
PLATFORM_LINUX_DISTRO = ' '.join(x for x in distro.linux_distribution() if x)
|
||||
PLATFORM_DEVICE_NAME = platform.node()
|
||||
PYTHON_VERSION = platform.python_version()
|
||||
BRANCH = version.PLEXPY_BRANCH
|
||||
RELEASE = version.PLEXPY_RELEASE_VERSION
|
||||
|
||||
@@ -104,6 +105,7 @@ PLATFORM_NAMES = {
|
||||
'nexus': 'android',
|
||||
'macos': 'macos',
|
||||
'microsoft edge': 'msedge',
|
||||
'netcast': 'lg',
|
||||
'opera': 'opera',
|
||||
'osx': 'macos',
|
||||
'playstation': 'playstation',
|
||||
@@ -119,6 +121,7 @@ PLATFORM_NAMES = {
|
||||
'tizen': 'samsung',
|
||||
'tvos': 'atv',
|
||||
'vizio': 'opera',
|
||||
'webos': 'lg',
|
||||
'wiiu': 'wiiu',
|
||||
'windows': 'windows',
|
||||
'windows phone': 'wp',
|
||||
|
@@ -75,7 +75,6 @@ _CONFIG_DEFINITIONS = {
|
||||
'PMS_UPDATE_CHECK_INTERVAL': (int, 'Advanced', 24),
|
||||
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
|
||||
'TIME_FORMAT': (str, 'General', 'HH:mm'),
|
||||
'ADD_LIVE_TV_LIBRARY': (int, 'Advanced', 1),
|
||||
'ANON_REDIRECT': (str, 'General', 'http://www.nullrefer.com/?'),
|
||||
'API_ENABLED': (int, 'General', 1),
|
||||
'API_KEY': (str, 'General', ''),
|
||||
@@ -348,7 +347,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'NOTIFY_GROUP_RECENTLY_ADDED': (int, 'Monitoring', 1),
|
||||
'NOTIFY_UPLOAD_POSTERS': (int, 'Monitoring', 0),
|
||||
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
|
||||
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 60),
|
||||
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 300),
|
||||
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
|
||||
'NOTIFY_RECENTLY_ADDED_UPGRADE': (int, 'Monitoring', 0),
|
||||
'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0),
|
||||
@@ -943,4 +942,10 @@ class Config(object):
|
||||
if plexpy.DOCKER:
|
||||
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
|
||||
if plexpy.PYTHON2:
|
||||
import logger
|
||||
from helpers import cast_to_int, bool_true
|
||||
from helpers import cast_to_int, bool_true, chunk
|
||||
else:
|
||||
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"
|
||||
@@ -218,12 +218,16 @@ def delete_rows_from_table(table, row_ids):
|
||||
|
||||
if row_ids:
|
||||
logger.info("Tautulli Database :: Deleting row ids %s from %s database table", row_ids, table)
|
||||
query = "DELETE FROM " + table + " WHERE id IN (%s) " % ','.join(['?'] * len(row_ids))
|
||||
monitor_db = MonitorDatabase()
|
||||
|
||||
# SQlite verions prior to 3.32.0 (2020-05-22) have maximum variable limit of 999
|
||||
# https://sqlite.org/limits.html
|
||||
sqlite_max_variable_number = 999
|
||||
|
||||
monitor_db = MonitorDatabase()
|
||||
try:
|
||||
monitor_db.action(query, row_ids)
|
||||
return True
|
||||
for row_ids_group in chunk(row_ids, sqlite_max_variable_number):
|
||||
query = "DELETE FROM " + table + " WHERE id IN (%s) " % ','.join(['?'] * len(row_ids_group))
|
||||
monitor_db.action(query, row_ids_group)
|
||||
except Exception as e:
|
||||
logger.error("Tautulli Database :: Failed to delete rows from %s database table: %s" % (table, e))
|
||||
return False
|
||||
|
@@ -31,7 +31,7 @@ import datetime
|
||||
from functools import wraps
|
||||
import hashlib
|
||||
import imghdr
|
||||
from future.moves.itertools import zip_longest
|
||||
from future.moves.itertools import islice, zip_longest
|
||||
import ipwhois
|
||||
import ipwhois.exceptions
|
||||
import ipwhois.utils
|
||||
@@ -52,10 +52,12 @@ import xmltodict
|
||||
|
||||
import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import common
|
||||
import logger
|
||||
import request
|
||||
from api2 import API2
|
||||
else:
|
||||
from plexpy import common
|
||||
from plexpy import logger
|
||||
from plexpy import request
|
||||
from plexpy.api2 import API2
|
||||
@@ -445,22 +447,25 @@ def create_https_certificates(ssl_cert, ssl_key):
|
||||
return False
|
||||
from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA
|
||||
|
||||
issuer = common.PRODUCT
|
||||
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]
|
||||
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
|
||||
logger.debug("Generating self-signed SSL certificate.")
|
||||
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
|
||||
try:
|
||||
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:
|
||||
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:
|
||||
logger.error("Error creating SSL key and certificate: %s", e)
|
||||
return False
|
||||
@@ -732,7 +737,7 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
|
||||
)
|
||||
|
||||
try:
|
||||
response = upload(img_data,
|
||||
response = upload((img_title, img_data),
|
||||
public_id='{}_{}'.format(fallback, rating_key),
|
||||
tags=['tautulli', fallback, str(rating_key)],
|
||||
context={'title': img_title, 'rating_key': str(rating_key), 'fallback': fallback})
|
||||
@@ -1063,6 +1068,11 @@ def grouper(iterable, n, fillvalue=None):
|
||||
return zip_longest(fillvalue=fillvalue, *args)
|
||||
|
||||
|
||||
def chunk(it, size):
|
||||
it = iter(it)
|
||||
return iter(lambda: tuple(islice(it, size)), ())
|
||||
|
||||
|
||||
def traverse_map(obj, func):
|
||||
if isinstance(obj, list):
|
||||
new_obj = []
|
||||
|
@@ -88,6 +88,8 @@ def refresh_libraries():
|
||||
if result == 'insert':
|
||||
new_keys.append(section['section_id'])
|
||||
|
||||
add_live_tv_library(refresh=True)
|
||||
|
||||
query = 'UPDATE library_sections SET is_active = 0 WHERE server_id != ? OR ' \
|
||||
'section_id NOT IN ({})'.format(', '.join(['?'] * len(section_ids)))
|
||||
monitor_db.action(query=query, args=[plexpy.CONFIG.PMS_IDENTIFIER] + section_ids)
|
||||
@@ -115,28 +117,28 @@ def refresh_libraries():
|
||||
return False
|
||||
|
||||
|
||||
def add_live_tv_library():
|
||||
if not plexpy.CONFIG.ADD_LIVE_TV_LIBRARY:
|
||||
def add_live_tv_library(refresh=False):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
result = monitor_db.select_single('SELECT * FROM library_sections '
|
||||
'WHERE section_id = ? and server_id = ?',
|
||||
[common.LIVE_TV_SECTION_ID, plexpy.CONFIG.PMS_IDENTIFIER])
|
||||
|
||||
if result and not refresh or not result and refresh:
|
||||
return
|
||||
|
||||
logger.info("Tautulli Libraries :: Adding Live TV library to the database.")
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
section_keys = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
'section_id': common.LIVE_TV_SECTION_ID}
|
||||
section_values = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
'section_id': common.LIVE_TV_SECTION_ID,
|
||||
'section_name': common.LIVE_TV_SECTION_NAME,
|
||||
'section_type': 'live'
|
||||
'section_type': 'live',
|
||||
'is_active': 1
|
||||
}
|
||||
|
||||
result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values)
|
||||
|
||||
if result == 'insert':
|
||||
plexpy.CONFIG.__setattr__('ADD_LIVE_TV_LIBRARY', 0)
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
|
||||
def has_library_type(section_type):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
@@ -19,7 +19,16 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
import plistlib
|
||||
import rumps
|
||||
|
||||
try:
|
||||
import AppKit
|
||||
import Foundation
|
||||
HAS_PYOBJC = True
|
||||
except ImportError:
|
||||
HAS_PYOBJC = False
|
||||
|
||||
if HAS_PYOBJC:
|
||||
import rumps
|
||||
|
||||
import plexpy
|
||||
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)
|
||||
|
||||
def start(self):
|
||||
logger.info("Launching MacOS system tray icon.")
|
||||
logger.info("Launching MacOS menu bar icon.")
|
||||
try:
|
||||
self.tray_icon.run()
|
||||
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):
|
||||
rumps.quit_application()
|
||||
|
@@ -18,6 +18,7 @@
|
||||
from __future__ import unicode_literals
|
||||
from future.builtins import str
|
||||
|
||||
import requests
|
||||
import threading
|
||||
|
||||
import plexpy
|
||||
@@ -34,6 +35,8 @@ else:
|
||||
TEMP_DEVICE_TOKEN = None
|
||||
INVALIDATE_TIMER = None
|
||||
|
||||
_ONESIGNAL_APP_ID = '3b4b666a-d557-4b92-acdf-e2c8c4b95357'
|
||||
|
||||
|
||||
def set_temp_device_token(token=None):
|
||||
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}
|
||||
values = {'device_name': device_name,
|
||||
'device_token': device_token}
|
||||
'device_token': device_token,
|
||||
'official': validate_device_id(device_id=device_id)}
|
||||
|
||||
if friendly_name:
|
||||
values['friendly_name'] = friendly_name
|
||||
@@ -161,6 +165,14 @@ def set_last_seen(device_token=None):
|
||||
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():
|
||||
devices = get_mobile_devices()
|
||||
for d in devices:
|
||||
|
@@ -17,6 +17,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from io import open
|
||||
import os
|
||||
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
@@ -214,7 +215,7 @@ def get_newsletter(newsletter_uuid=None, newsletter_id_name=None):
|
||||
|
||||
if newsletter_file in os.listdir(newsletter_folder):
|
||||
try:
|
||||
with open(newsletter_file_fp, 'r') as n_file:
|
||||
with open(newsletter_file_fp, 'r', encoding='utf-8') as n_file:
|
||||
newsletter = n_file.read()
|
||||
return newsletter
|
||||
except OSError as e:
|
||||
|
@@ -882,8 +882,6 @@ class ANDROIDAPP(Notifier):
|
||||
'priority': 3
|
||||
}
|
||||
|
||||
_ONESIGNAL_APP_ID = '3b4b666a-d557-4b92-acdf-e2c8c4b95357'
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', notification_id=None, **kwargs):
|
||||
# Check mobile device is still registered
|
||||
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("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']],
|
||||
'contents': {'en': 'Tautulli Notification'},
|
||||
'data': {'encrypted': True,
|
||||
@@ -943,7 +941,7 @@ class ANDROIDAPP(Notifier):
|
||||
"Android app notifications will be sent unecrypted. "
|
||||
"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']],
|
||||
'contents': {'en': 'Tautulli Notification'},
|
||||
'data': {'encrypted': False,
|
||||
@@ -960,7 +958,7 @@ class ANDROIDAPP(Notifier):
|
||||
db = database.MonitorDatabase()
|
||||
|
||||
try:
|
||||
query = 'SELECT * FROM mobile_devices'
|
||||
query = 'SELECT * FROM mobile_devices WHERE official = 1'
|
||||
result = db.select(query=query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli Notifiers :: Unable to retrieve Android app devices list: %s." % e)
|
||||
@@ -1123,17 +1121,22 @@ class BROWSER(Notifier):
|
||||
return True
|
||||
|
||||
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',
|
||||
'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'
|
||||
},
|
||||
{'label': 'Auto Hide Delay',
|
||||
'value': self.config['auto_hide_delay'],
|
||||
'name': 'browser_auto_hide_delay',
|
||||
'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.)',
|
||||
'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.)',
|
||||
'input_type': 'number'
|
||||
}
|
||||
]
|
||||
@@ -1396,8 +1399,7 @@ class EMAIL(Notifier):
|
||||
success = True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Tautulli Notifiers :: {name} notification failed: {e}".format(
|
||||
name=self.NAME, e=str(e).decode('utf-8')))
|
||||
logger.error("Tautulli Notifiers :: %s notification failed: %s", self.NAME, e)
|
||||
|
||||
finally:
|
||||
if mailserver:
|
||||
@@ -2967,7 +2969,7 @@ class SCRIPTS(Notifier):
|
||||
'.php': 'php',
|
||||
'.pl': 'perl',
|
||||
'.ps1': 'powershell -executionPolicy bypass -file',
|
||||
'.py': 'python',
|
||||
'.py': 'python' if plexpy.FROZEN else sys.executable,
|
||||
'.pyw': 'pythonw',
|
||||
'.rb': 'ruby',
|
||||
'.sh': ''
|
||||
@@ -3005,7 +3007,7 @@ class SCRIPTS(Notifier):
|
||||
'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT,
|
||||
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY,
|
||||
'TAUTULLI_ENCODING': plexpy.SYS_ENCODING,
|
||||
'TAUTULLI_PYTHON_VERSION': '.'.join(map(str, plexpy.PYTHON_VERSION))
|
||||
'TAUTULLI_PYTHON_VERSION': common.PYTHON_VERSION
|
||||
}
|
||||
|
||||
if user_id:
|
||||
|
@@ -17,5 +17,5 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.5.0-beta"
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.5.3"
|
@@ -41,6 +41,13 @@ else:
|
||||
from plexpy.users import Users, refresh_users
|
||||
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_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'
|
||||
|
||||
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:
|
||||
return True, plex_login[0], plex_login[1]
|
||||
|
||||
@@ -296,9 +303,13 @@ class AuthController(object):
|
||||
self.on_logout(payload['user'], payload['user_group'])
|
||||
|
||||
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]['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
|
||||
|
||||
@@ -344,7 +355,9 @@ class AuthController(object):
|
||||
jwt_cookie = str(JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID)
|
||||
cherrypy.response.cookie[jwt_cookie] = jwt_token
|
||||
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.response.status = 200
|
||||
|
33
start.sh
33
start.sh
@@ -1,2 +1,33 @@
|
||||
#!/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