Compare commits

...

67 Commits

Author SHA1 Message Date
JonnyWong16
1698622d63 v2.5.3 2020-07-10 17:07:18 -07:00
JonnyWong16
fa27271647 Change shebang on contrib scripts 2020-07-10 17:02:23 -07:00
JonnyWong16
d837811c68 Improve start script 2020-07-09 17:13:16 -07:00
JonnyWong16
ad195f0969 Fix deleteing more than 1000 history entries at the same time 2020-07-08 12:27:20 -07:00
JonnyWong16
4a8748e322 Live TV library not being recreated after server identifier is changed
* Fixes Tautulli/Tautulli-Issues#261
2020-07-07 18:14:00 -07:00
JonnyWong16
0f016c83ea Fix ipwhois data location for macOS package
* Fixes Tautulli/Tautulli-Issues#260
2020-07-07 17:25:46 -07:00
JonnyWong16
061ae44da4 Fix indentation in macOS postinstall script 2020-07-07 17:05:15 -07:00
JonnyWong16
a8b90bf100 Reduce macOS build requirement to pyobjc-framework-Cocoa 2020-07-07 17:05:10 -07:00
JonnyWong16
eb3cd49bc4 Add hidden import pkg_resources.py2_warn to macos.spec
* Fixes build on macOS 10.13 (High Sierra)
2020-07-06 20:57:37 -07:00
JonnyWong16
416d869288 Add python version to Google Analytics 2020-07-06 18:13:33 -07:00
JonnyWong16
a116c26c25 Run python scripts with the same sys.executable as Tautulli 2020-07-06 11:32:16 -07:00
JonnyWong16
cc4ec53dac Full path to python3 interpreter in FreeBSD startup script 2020-07-06 10:08:36 -07:00
JonnyWong16
63164c7ff5 Quote command in systemd script 2020-07-06 09:37:37 -07:00
JonnyWong16
9815c014e8 Add python interpreter to init-scripts 2020-07-06 09:30:35 -07:00
JonnyWong16
41843dc573 Rename some column headers 2020-07-04 12:22:40 -07:00
JonnyWong16
cc6bd528a5 Add architecture to release assets 2020-07-04 11:28:22 -07:00
JonnyWong16
2625ef5fb9 Use Popen to restart on macOS 2020-07-03 19:48:27 -07:00
JonnyWong16
dbd2d28877 Set macOS menu bar icon thread to daemon 2020-07-03 19:47:57 -07:00
JonnyWong16
f70f814c70 Shutdown tray icons last 2020-07-03 19:47:11 -07:00
JonnyWong16
6710e42134 Hide macOS dock icon for pkg install 2020-07-03 19:46:27 -07:00
JonnyWong16
78c5b45e43 Also fix e562ec9 for Python 2 2020-07-03 11:24:47 -07:00
JonnyWong16
e562ec96fa Fix encoding when reading a newsletter file 2020-07-02 20:46:42 -07:00
JonnyWong16
9b5e01c319 Fix logger for email notification exception 2020-07-02 12:45:45 -07:00
JonnyWong16
0097532f4a Fix startup script 2020-07-02 12:33:32 -07:00
JonnyWong16
91935c9018 Add hidden import cheroot.ssl.builtin for pyinstaller 2020-07-02 09:20:58 -07:00
JonnyWong16
83df807f7e Fix typo in eb3db20 2020-07-02 09:15:13 -07:00
JonnyWong16
eb3db20340 Add hidden import chroot.ssl for pyinstaller 2020-07-02 09:11:15 -07:00
JonnyWong16
6dab6194ea Replace which with command -v in startup script 2020-07-01 22:44:05 -07:00
JonnyWong16
356f64cac0 v2.5.2 2020-07-01 19:49:44 -07:00
JonnyWong16
f77f289125 Move GitHub sponsor first 2020-07-01 15:53:08 -07:00
JonnyWong16
280257477a Revert "Change shebang to python3"
This reverts commit cd8a899521.
2020-07-01 14:59:18 -07:00
JonnyWong16
660141cb16 Try various python versions in startup script 2020-07-01 14:43:35 -07:00
JonnyWong16
cd8a899521 Change shebang to python3 2020-07-01 14:43:04 -07:00
JonnyWong16
cb577c51b8 v2.5.2-beta 2020-06-27 15:04:06 -07:00
JonnyWong16
1c395ab10c Patch SameSite support into cookies
* Python 2.7 is missing SameSite cookie attribute
2020-06-27 15:01:16 -07:00
JonnyWong16
07d7170e49 v2.5.1-beta 2020-06-26 18:37:07 -07:00
JonnyWong16
88e23627fd Fix typo 2020-06-25 19:12:10 -07:00
JonnyWong16
48f846da40 Expire the previous JWT on update if HTTP root is set
* Required for Tautulli/Tautulli-Issues#255
2020-06-24 14:04:07 -07:00
JonnyWong16
ff887d9948 Remove unnecessary x_plex_headers from 805d45b 2020-06-23 20:07:45 -07:00
JonnyWong16
617b0d6fd9 Set JWT cookie path to HTTP root
* Fixes Tautulli/Tautulli-Issues#255
2020-06-23 20:00:50 -07:00
JonnyWong16
805d45bd33 Don't overwrite PMS_UUID when fetching a new token 2020-06-23 19:47:01 -07:00
JonnyWong16
fef428202f Start Tautulli using different user in Docker container 2020-06-21 12:38:27 -07:00
JonnyWong16
40fd82febd Only change Docker permissions if PUID/PGID exists 2020-06-21 10:38:26 -07:00
JonnyWong16
45f0001da5 Fix Docker permissions if pre-existing PUID/PGID 2020-06-21 09:58:29 -07:00
JonnyWong16
c7a3e1e3bf Change Docker default PUID and PGID 2020-06-21 00:27:48 -07:00
JonnyWong16
9dd8cc9e49 Fix Docker container not using PUID and PGID environment variables 2020-06-20 23:51:29 -07:00
JonnyWong16
d252d4cd2d Update Publish Docker workflow 2020-06-20 23:51:21 -07:00
JonnyWong16
bc1328040c Update Publish Release workflow 2020-06-20 23:51:20 -07:00
JonnyWong16
82919d3c1d Fix indent in MacOS postinstall script 2020-06-20 23:51:19 -07:00
JonnyWong16
7c801c2f5e Add flag for offical mobile app 2020-06-20 16:16:35 -07:00
JonnyWong16
9a932aea12 Fix text wrapping on user player stats 2020-06-20 15:03:51 -07:00
JonnyWong16
5696e75abe Add LG platform icon 2020-06-20 15:03:32 -07:00
JonnyWong16
efb3f748c2 Improve app registration instructions 2020-06-20 11:36:54 -07:00
JonnyWong16
450b3865a8 Validate OneSignal Player ID when registering device 2020-06-20 10:59:55 -07:00
JonnyWong16
970667adca Only allow temporary device token access to register app 2020-06-20 10:58:49 -07:00
JonnyWong16
89307dad01 Show missing pyobjc module message on MacOS menu bar setting 2020-06-14 15:22:58 -07:00
JonnyWong16
451feda86b Rename system tray to menu bar on MacOS 2020-06-14 14:59:37 -07:00
JonnyWong16
4d241fac48 Try import rumps 2020-06-14 14:52:55 -07:00
JonnyWong16
4390f5cbc8 Check for Foundation module for MacOS system track icon
* Fixes Tautulli/Tautulli-Issues#249
2020-06-13 14:36:47 -07:00
JonnyWong16
7f9d46eac3 Fix Cloudinary upload for Python 2 2020-06-03 20:41:57 -07:00
JonnyWong16
d0f28883aa Remove ability to login using Plex username / password.
* Only login using Plex OAuth
2020-06-02 17:28:24 -07:00
JonnyWong16
48203e64a9 Improve test browser notifications 2020-06-01 22:55:59 -07:00
JonnyWong16
42b17ca495 Change default recently added notification delay to 300s 2020-06-01 16:44:01 -07:00
JonnyWong16
d8080fe506 Fix creating self-signed certificates on Python 3
* Fixes Tautulli/Tautulli-Issues#248
2020-06-01 16:40:25 -07:00
JonnyWong16
be910e24f7 Update release workflow
* Update joncloud/makensis-action@v1.2
2020-05-31 15:35:55 -07:00
JonnyWong16
ce6d70f6fd Fix CHANGELOG.md 2020-05-31 15:29:40 -07:00
JonnyWong16
827e05e4d7 Update release workflow
* Update joncloud/makensis-action@v2
2020-05-31 15:29:23 -07:00
41 changed files with 336 additions and 147 deletions

View File

@@ -7,3 +7,4 @@ package
pylintrc pylintrc
*.md *.md
!CHANGELOG*.md !CHANGELOG*.md
start.bat

View File

@@ -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

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2.1.0 uses: actions/checkout@v2
- name: Set Release Version - name: Set Release Version
id: get_version id: get_version
@@ -28,13 +28,13 @@ jobs:
echo $GITHUB_SHA > version.txt echo $GITHUB_SHA > version.txt
- name: Set Up Python - name: Set Up Python
uses: actions/setup-python@v1.2.0 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: 3.8
- name: Cache Dependencies - name: Cache Dependencies
id: cache_dependencies id: cache_dependencies
uses: actions/cache@v1 uses: actions/cache@v2
with: with:
path: ~\AppData\Local\pip\Cache path: ~\AppData\Local\pip\Cache
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-windows.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-windows.txt') }}
@@ -50,17 +50,18 @@ jobs:
pyinstaller -y ./package/Tautulli-windows.spec pyinstaller -y ./package/Tautulli-windows.spec
- name: Create Installer - name: Create Installer
uses: joncloud/makensis-action@v1 uses: joncloud/makensis-action@v1.2
with: with:
script-file: ./package/Tautulli.nsi script-file: ./package/Tautulli.nsi
arguments: /DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }} /DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe arguments: /DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }} /DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
includeMorePlugins: package/nsis-plugins include-more-plugins: true
include-custom-plugins-path: package/nsis-plugins
- name: Upload Installer - name: Upload Installer
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v2
with: with:
name: Tautulli-windows-installer name: Tautulli-windows-installer
path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
- name: Post Status to Discord - name: Post Status to Discord
uses: sarisia/actions-status-discord@v1 uses: sarisia/actions-status-discord@v1
@@ -68,14 +69,14 @@ jobs:
with: with:
webhook: ${{ secrets.DISCORD_WEBHOOK }} webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }} status: ${{ job.status }}
job: Build Windows Installer title: Build Windows Installer
nofail: true nofail: true
build-macos: build-macos:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2.1.0 uses: actions/checkout@v2
- name: Set Release Version - name: Set Release Version
id: get_version id: get_version
@@ -93,13 +94,13 @@ jobs:
echo $GITHUB_SHA > version.txt echo $GITHUB_SHA > version.txt
- name: Set Up Python - name: Set Up Python
uses: actions/setup-python@v1.2.0 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: 3.8
- name: Cache Dependencies - name: Cache Dependencies
id: cache_dependencies id: cache_dependencies
uses: actions/cache@v1 uses: actions/cache@v2
with: with:
path: ~/Library/Caches/pip path: ~/Library/Caches/pip
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-macos.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-macos.txt') }}
@@ -116,13 +117,13 @@ jobs:
- name: Create Installer - name: Create Installer
run: | run: |
sudo pkgbuild --install-location /Applications --version ${{ steps.get_version.outputs.VERSION }} --component ./dist/Tautulli.app --scripts ./package/macos-scripts Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg sudo pkgbuild --install-location /Applications --version ${{ steps.get_version.outputs.VERSION }} --component ./dist/Tautulli.app --scripts ./package/macos-scripts Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
- name: Upload Installer - name: Upload Installer
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v2
with: with:
name: Tautulli-macos-installer name: Tautulli-macos-installer
path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
- name: Post Status to Discord - name: Post Status to Discord
uses: sarisia/actions-status-discord@v1 uses: sarisia/actions-status-discord@v1
@@ -130,7 +131,7 @@ jobs:
with: with:
webhook: ${{ secrets.DISCORD_WEBHOOK }} webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }} status: ${{ job.status }}
job: Build MacOS Installer title: Build MacOS Installer
nofail: true nofail: true
release: release:
@@ -142,7 +143,7 @@ jobs:
uses: technote-space/workflow-conclusion-action@v1 uses: technote-space/workflow-conclusion-action@v1
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2.1.0 uses: actions/checkout@v2
- name: Set Release Version - name: Set Release Version
id: get_version id: get_version
@@ -151,13 +152,13 @@ jobs:
- name: Download Windows Installer - name: Download Windows Installer
if: env.WORKFLOW_CONCLUSION == 'success' if: env.WORKFLOW_CONCLUSION == 'success'
uses: actions/download-artifact@v1 uses: actions/download-artifact@v2
with: with:
name: Tautulli-windows-installer name: Tautulli-windows-installer
- name: Download MacOS Installer - name: Download MacOS Installer
if: env.WORKFLOW_CONCLUSION == 'success' if: env.WORKFLOW_CONCLUSION == 'success'
uses: actions/download-artifact@v1 uses: actions/download-artifact@v2
with: with:
name: Tautulli-macos-installer name: Tautulli-macos-installer
@@ -187,8 +188,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: Tautulli-windows-installer/Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe asset_path: ./Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}.exe asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
asset_content_type: application/vnd.microsoft.portable-executable asset_content_type: application/vnd.microsoft.portable-executable
- name: Upload MacOS Installer - name: Upload MacOS Installer
@@ -198,6 +199,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: Tautulli-macos-installer/Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg asset_path: ./Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}.pkg asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
asset_content_type: application/vnd.apple.installer+xml asset_content_type: application/vnd.apple.installer+xml

View File

@@ -1,16 +1,46 @@
# Changelog # Changelog
# v2.5.0-beta (2020-05-31) ## v2.5.3 (2020-07-10)
* History:
* Fix: Unable to delete more than 1000 history entries at the same time.
* Notifications:
* Change: Python script notifications to run using the same Python interpreter as Tautulli.
* Newsletters:
* Fix: Unable to view newsletters with special characters.
* Other:
* Fix: Tautulli failing to start after enabling HTTPS when installed using the Windows / macOS installers.
* Fix: Startup script not working on macOS.
* Fix: Unable to hide dock icon on macOS with the pkg install. Refer to the FAQ regarding the Python rocket dock icon.
* Change: Added path to Python interpreter in system startup (daemon) scripts.
* Change: Added Python version to Google analytics.
## v2.5.2 (2020-07-01)
* Announcements: * Announcements:
* Tautulli now supports Python 3! * Tautulli now supports Python 3!
* Python 2 is still supported for the time being, but it is recommended to upgrade to Python 3. * Python 2 is still supported for the time being, but it is recommended to upgrade to Python 3.
* Notifications:
* Fix: Error uploading images to Cloudinary on Python 2.
* Fix: Testing browser notifications alert not disappearing.
* Change: Default recently added notification delay set to 300 seconds.
* UI: * UI:
* Fix: MacOS menu bar icon causing Tautulli to fail to start.
* Fix: Unable to login to Tautulli on Python 2.
* New: Windows and MacOS setting to enable Tautulli to start automatically when you login. * New: Windows and MacOS setting to enable Tautulli to start automatically when you login.
* New: Added system tray icon for MacOS. * New: Added menu bar icon for MacOS.
* New: Ability to import a Tautulli database in the settings. * New: Ability to import a Tautulli database in the settings.
* New: Added Tautulli news area on the settings page. * New: Added Tautulli news area on the settings page.
* New: Added platform icon for LG devices.
* Remove: Ability to login to Tautulli using a Plex username and password has been removed. Login using a Plex.tv account is only supported via OAuth.
* Mobile App:
* Fix: Improved API security and validation when registering the Android app.
* Docker:
* Fix: Docker container not respecting the PUID and PGID environment variables.
* Other: * Other:
* Fix: Error creating self-signed certificates on Python 3.
* Fix: Tautulli login session cookie not set on the HTTP root path.
* New: Windows and MacOS app installers to install Tautulli without needing Python installed. * New: Windows and MacOS app installers to install Tautulli without needing Python installed.

View File

@@ -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

View File

@@ -257,17 +257,18 @@ def main():
plexpy.HTTP_ROOT) plexpy.HTTP_ROOT)
if common.PLATFORM == 'Darwin' and plexpy.CONFIG.SYS_TRAY_ICON: if common.PLATFORM == 'Darwin' and plexpy.CONFIG.SYS_TRAY_ICON:
try: if not macos.HAS_PYOBJC:
import AppKit
except ImportError:
logger.warn("The pyobjc module is missing. Install this " logger.warn("The pyobjc module is missing. Install this "
"module to enable the system tray icon.") "module to enable the MacOS menu bar icon.")
plexpy.CONFIG.SYS_TRAY_ICON = False plexpy.CONFIG.SYS_TRAY_ICON = False
if plexpy.CONFIG.SYS_TRAY_ICON: if plexpy.CONFIG.SYS_TRAY_ICON:
# MacOS system tray icon must be run on the main thread and is blocking # MacOS menu bar icon must be run on the main thread and is blocking
# Start the rest of Tautulli on a new thread # Start the rest of Tautulli on a new thread
threading.Thread(target=wait).start() thread = threading.Thread(target=wait)
thread.daemon = True
thread.start()
plexpy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray() plexpy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
plexpy.MAC_SYS_TRAY_ICON.start() plexpy.MAC_SYS_TRAY_ICON.start()
else: else:

View File

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

View File

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

View File

@@ -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>&nbsp; Sponsor <i class="fa fa-heart fa-sm" style="color: #ea4aaa;"></i>&nbsp; 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':

View File

@@ -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);
} }

View 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

View File

@@ -36,7 +36,3 @@ function check_notifications() {
check_notifications(); check_notifications();
}, 5000); }, 5000);
} }
$(document).ready(function () {
check_notifications();
});

View File

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

View File

@@ -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">

View File

@@ -13,7 +13,11 @@ DOCUMENTATION :: END
% for device in sorted(devices_list, key=lambda k: k['device_name']): % for device in sorted(devices_list, key=lambda k: k['device_name']):
<li class="mobile-device pointer" data-id="${device['id']}" data-name="${device['device_name']}"> <li class="mobile-device pointer" data-id="${device['id']}" data-name="${device['device_name']}">
<span> <span>
% if device['official']:
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span> <span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span>
% else:
<span class="toggle-left officail-tooltip" data-toggle="tooltip" data-placement="top" title="Unofficial or Unknown App"><i class="fa fa-lg fa-fw fa-exclamation-triangle"></i></span>
% endif
${device['friendly_name'] or device['device_name']} &nbsp;<span class="friendly_name">(${device['id']})</span> ${device['friendly_name'] or device['device_name']} &nbsp;<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>

View File

@@ -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);
} }
} }

View File

@@ -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);
} }

View File

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

View File

@@ -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">

View File

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

View File

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

View File

@@ -31,11 +31,13 @@
# sudo chown tautulli:tautulli -R /opt/Tautulli # sudo chown tautulli:tautulli -R /opt/Tautulli
# #
# - Adjust ExecStart= to point to: # - Adjust ExecStart= to point to:
# 1. Your Tautulli executable # 1. Your Python interpreter (get the path with "command -v python3")
# - Default: /usr/bin/python3
# 2. Your Tautulli executable
# - Default: /opt/Tautulli/Tautulli.py # - Default: /opt/Tautulli/Tautulli.py
# 2. Your config file (recommended is to put it somewhere in /etc) # 3. Your config file (recommended is to put it somewhere in /etc)
# - Default: --config /opt/Tautulli/config.ini # - Default: --config /opt/Tautulli/config.ini
# 3. Your datadir (recommended is to NOT put it in your Tautulli exec dir) # 4. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
# - Default: --datadir /opt/Tautulli # - Default: --datadir /opt/Tautulli
# #
# - Adjust User= and Group= to the user/group you want Tautulli to run as. # - Adjust User= and Group= to the user/group you want Tautulli to run as.
@@ -50,7 +52,7 @@ Wants=network-online.target
After=network-online.target After=network-online.target
[Service] [Service]
ExecStart=/opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch ExecStart=/usr/bin/python3 /opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch
GuessMainPID=no GuessMainPID=no
Type=forking Type=forking
User=tautulli User=tautulli

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -37,8 +37,7 @@ from apscheduler.triggers.interval import IntervalTrigger
from UniversalAnalytics import Tracker from UniversalAnalytics import Tracker
import pytz import pytz
PYTHON_VERSION = sys.version_info[:3] PYTHON2 = sys.version_info[0] == 2
PYTHON2 = PYTHON_VERSION[0] == 2
if PYTHON2: if PYTHON2:
import activity_handler import activity_handler
@@ -745,7 +744,7 @@ def dbcheck():
c_db.execute( c_db.execute(
'CREATE TABLE IF NOT EXISTS mobile_devices (id INTEGER PRIMARY KEY AUTOINCREMENT, ' 'CREATE TABLE IF NOT EXISTS mobile_devices (id INTEGER PRIMARY KEY AUTOINCREMENT, '
'device_id TEXT NOT NULL UNIQUE, device_token TEXT, device_name TEXT, friendly_name TEXT, ' 'device_id TEXT NOT NULL UNIQUE, device_token TEXT, device_name TEXT, friendly_name TEXT, '
'last_seen INTEGER)' 'last_seen INTEGER, official INTEGER DEFAULT 0)'
) )
# tvmaze_lookup table :: This table keeps record of the TVmaze lookups # tvmaze_lookup table :: This table keeps record of the TVmaze lookups
@@ -2001,6 +2000,19 @@ def dbcheck():
'ALTER TABLE mobile_devices ADD COLUMN last_seen INTEGER' 'ALTER TABLE mobile_devices ADD COLUMN last_seen INTEGER'
) )
# Upgrade mobile_devices table from earlier versions
try:
c_db.execute('SELECT official FROM mobile_devices')
except sqlite3.OperationalError:
logger.debug("Altering database. Updating database table mobile_devices.")
c_db.execute(
'ALTER TABLE mobile_devices ADD COLUMN official INTEGER DEFAULT 0'
)
# Update official mobile device flag
for device_id, in c_db.execute('SELECT device_id FROM mobile_devices').fetchall():
c_db.execute('UPDATE mobile_devices SET official = ? WHERE device_id = ?',
[mobile_app.validate_device_id(device_id), device_id])
# Upgrade notifiers table from earlier versions # Upgrade notifiers table from earlier versions
try: try:
c_db.execute('SELECT custom_conditions FROM notifiers') c_db.execute('SELECT custom_conditions FROM notifiers')
@@ -2198,11 +2210,6 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
logger.info("Removing pidfile %s", PIDFILE) logger.info("Removing pidfile %s", PIDFILE)
os.remove(PIDFILE) os.remove(PIDFILE)
if WIN_SYS_TRAY_ICON:
WIN_SYS_TRAY_ICON.shutdown()
elif MAC_SYS_TRAY_ICON:
MAC_SYS_TRAY_ICON.shutdown()
if restart: if restart:
logger.info("Tautulli is restarting...") logger.info("Tautulli is restarting...")
@@ -2225,7 +2232,7 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
# https://bugs.python.org/issue19066 # https://bugs.python.org/issue19066
if NOFORK: if NOFORK:
pass pass
elif common.PLATFORM == 'Windows': elif common.PLATFORM in ('Windows', 'Darwin'):
subprocess.Popen(args, cwd=os.getcwd()) subprocess.Popen(args, cwd=os.getcwd())
else: else:
os.execv(exe, args) os.execv(exe, args)
@@ -2235,6 +2242,11 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
logger.shutdown() logger.shutdown()
if WIN_SYS_TRAY_ICON:
WIN_SYS_TRAY_ICON.shutdown()
elif MAC_SYS_TRAY_ICON:
MAC_SYS_TRAY_ICON.shutdown()
os._exit(0) os._exit(0)
@@ -2251,6 +2263,7 @@ def initialize_tracker():
'appInstallerId': CONFIG.GIT_BRANCH, 'appInstallerId': CONFIG.GIT_BRANCH,
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform 'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform
'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro 'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro
'dimension3': common.PYTHON_VERSION,
'userLanguage': SYS_LANGUAGE, 'userLanguage': SYS_LANGUAGE,
'documentEncoding': SYS_ENCODING, 'documentEncoding': SYS_ENCODING,
'noninteractive': True 'noninteractive': True

View File

@@ -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

View File

@@ -35,6 +35,7 @@ PLATFORM_RELEASE = platform.release()
PLATFORM_VERSION = platform.version() PLATFORM_VERSION = platform.version()
PLATFORM_LINUX_DISTRO = ' '.join(x for x in distro.linux_distribution() if x) PLATFORM_LINUX_DISTRO = ' '.join(x for x in distro.linux_distribution() if x)
PLATFORM_DEVICE_NAME = platform.node() PLATFORM_DEVICE_NAME = platform.node()
PYTHON_VERSION = platform.python_version()
BRANCH = version.PLEXPY_BRANCH BRANCH = version.PLEXPY_BRANCH
RELEASE = version.PLEXPY_RELEASE_VERSION RELEASE = version.PLEXPY_RELEASE_VERSION
@@ -104,6 +105,7 @@ PLATFORM_NAMES = {
'nexus': 'android', 'nexus': 'android',
'macos': 'macos', 'macos': 'macos',
'microsoft edge': 'msedge', 'microsoft edge': 'msedge',
'netcast': 'lg',
'opera': 'opera', 'opera': 'opera',
'osx': 'macos', 'osx': 'macos',
'playstation': 'playstation', 'playstation': 'playstation',
@@ -119,6 +121,7 @@ PLATFORM_NAMES = {
'tizen': 'samsung', 'tizen': 'samsung',
'tvos': 'atv', 'tvos': 'atv',
'vizio': 'opera', 'vizio': 'opera',
'webos': 'lg',
'wiiu': 'wiiu', 'wiiu': 'wiiu',
'windows': 'windows', 'windows': 'windows',
'windows phone': 'wp', 'windows phone': 'wp',

View File

@@ -75,7 +75,6 @@ _CONFIG_DEFINITIONS = {
'PMS_UPDATE_CHECK_INTERVAL': (int, 'Advanced', 24), 'PMS_UPDATE_CHECK_INTERVAL': (int, 'Advanced', 24),
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'), 'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
'TIME_FORMAT': (str, 'General', 'HH:mm'), 'TIME_FORMAT': (str, 'General', 'HH:mm'),
'ADD_LIVE_TV_LIBRARY': (int, 'Advanced', 1),
'ANON_REDIRECT': (str, 'General', 'http://www.nullrefer.com/?'), 'ANON_REDIRECT': (str, 'General', 'http://www.nullrefer.com/?'),
'API_ENABLED': (int, 'General', 1), 'API_ENABLED': (int, 'General', 1),
'API_KEY': (str, 'General', ''), 'API_KEY': (str, 'General', ''),
@@ -348,7 +347,7 @@ _CONFIG_DEFINITIONS = {
'NOTIFY_GROUP_RECENTLY_ADDED': (int, 'Monitoring', 1), 'NOTIFY_GROUP_RECENTLY_ADDED': (int, 'Monitoring', 1),
'NOTIFY_UPLOAD_POSTERS': (int, 'Monitoring', 0), 'NOTIFY_UPLOAD_POSTERS': (int, 'Monitoring', 0),
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0), 'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 60), 'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 300),
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0), 'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
'NOTIFY_RECENTLY_ADDED_UPGRADE': (int, 'Monitoring', 0), 'NOTIFY_RECENTLY_ADDED_UPGRADE': (int, 'Monitoring', 0),
'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0), 'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0),
@@ -943,4 +942,10 @@ class Config(object):
if plexpy.DOCKER: if plexpy.DOCKER:
self.PLEXPY_AUTO_UPDATE = 0 self.PLEXPY_AUTO_UPDATE = 0
self.CONFIG_VERSION == 15 self.CONFIG_VERSION = 15
if self.CONFIG_VERSION == 15:
if self.HTTP_ROOT and self.HTTP_ROOT != '/':
self.JWT_UPDATE_SECRET = True
self.CONFIG_VERSION = 16

View File

@@ -27,10 +27,10 @@ import time
import plexpy import plexpy
if plexpy.PYTHON2: if plexpy.PYTHON2:
import logger import logger
from helpers import cast_to_int, bool_true from helpers import cast_to_int, bool_true, chunk
else: else:
from plexpy import logger from plexpy import logger
from plexpy.helpers import cast_to_int, bool_true from plexpy.helpers import cast_to_int, bool_true, chunk
FILENAME = "tautulli.db" FILENAME = "tautulli.db"
@@ -218,12 +218,16 @@ def delete_rows_from_table(table, row_ids):
if row_ids: if row_ids:
logger.info("Tautulli Database :: Deleting row ids %s from %s database table", row_ids, table) logger.info("Tautulli Database :: Deleting row ids %s from %s database table", row_ids, table)
query = "DELETE FROM " + table + " WHERE id IN (%s) " % ','.join(['?'] * len(row_ids))
monitor_db = MonitorDatabase()
# SQlite verions prior to 3.32.0 (2020-05-22) have maximum variable limit of 999
# https://sqlite.org/limits.html
sqlite_max_variable_number = 999
monitor_db = MonitorDatabase()
try: try:
monitor_db.action(query, row_ids) for row_ids_group in chunk(row_ids, sqlite_max_variable_number):
return True query = "DELETE FROM " + table + " WHERE id IN (%s) " % ','.join(['?'] * len(row_ids_group))
monitor_db.action(query, row_ids_group)
except Exception as e: except Exception as e:
logger.error("Tautulli Database :: Failed to delete rows from %s database table: %s" % (table, e)) logger.error("Tautulli Database :: Failed to delete rows from %s database table: %s" % (table, e))
return False return False

View File

@@ -31,7 +31,7 @@ import datetime
from functools import wraps from functools import wraps
import hashlib import hashlib
import imghdr import imghdr
from future.moves.itertools import zip_longest from future.moves.itertools import islice, zip_longest
import ipwhois import ipwhois
import ipwhois.exceptions import ipwhois.exceptions
import ipwhois.utils import ipwhois.utils
@@ -52,10 +52,12 @@ import xmltodict
import plexpy import plexpy
if plexpy.PYTHON2: if plexpy.PYTHON2:
import common
import logger import logger
import request import request
from api2 import API2 from api2 import API2
else: else:
from plexpy import common
from plexpy import logger from plexpy import logger
from plexpy import request from plexpy import request
from plexpy.api2 import API2 from plexpy.api2 import API2
@@ -445,22 +447,25 @@ def create_https_certificates(ssl_cert, ssl_key):
return False return False
from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA
issuer = common.PRODUCT
serial = timestamp() serial = timestamp()
not_before = 0
not_after = 60 * 60 * 24 * 365 * 10 # ten years
domains = ['DNS:' + d.strip() for d in plexpy.CONFIG.HTTPS_DOMAIN.split(',') if d] domains = ['DNS:' + d.strip() for d in plexpy.CONFIG.HTTPS_DOMAIN.split(',') if d]
ips = ['IP:' + d.strip() for d in plexpy.CONFIG.HTTPS_IP.split(',') if d] ips = ['IP:' + d.strip() for d in plexpy.CONFIG.HTTPS_IP.split(',') if d]
altNames = ','.join(domains + ips) alt_names = ','.join(domains + ips).encode('utf-8')
# Create the self-signed Tautulli certificate # Create the self-signed Tautulli certificate
logger.debug("Generating self-signed SSL certificate.") logger.debug("Generating self-signed SSL certificate.")
pkey = createKeyPair(TYPE_RSA, 2048) pkey = createKeyPair(TYPE_RSA, 2048)
cert = createSelfSignedCertificate("Tautulli", pkey, serial, 0, 60 * 60 * 24 * 365 * 10, altNames) # ten years cert = createSelfSignedCertificate(issuer, pkey, serial, not_before, not_after, alt_names)
# Save the key and certificate to disk # Save the key and certificate to disk
try: try:
with open(ssl_cert, "w") as fp: with open(ssl_cert, "w") as fp:
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8'))
with open(ssl_key, "w") as fp: with open(ssl_key, "w") as fp:
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey).decode('utf-8'))
except IOError as e: except IOError as e:
logger.error("Error creating SSL key and certificate: %s", e) logger.error("Error creating SSL key and certificate: %s", e)
return False return False
@@ -732,7 +737,7 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
) )
try: try:
response = upload(img_data, response = upload((img_title, img_data),
public_id='{}_{}'.format(fallback, rating_key), public_id='{}_{}'.format(fallback, rating_key),
tags=['tautulli', fallback, str(rating_key)], tags=['tautulli', fallback, str(rating_key)],
context={'title': img_title, 'rating_key': str(rating_key), 'fallback': fallback}) context={'title': img_title, 'rating_key': str(rating_key), 'fallback': fallback})
@@ -1063,6 +1068,11 @@ def grouper(iterable, n, fillvalue=None):
return zip_longest(fillvalue=fillvalue, *args) return zip_longest(fillvalue=fillvalue, *args)
def chunk(it, size):
it = iter(it)
return iter(lambda: tuple(islice(it, size)), ())
def traverse_map(obj, func): def traverse_map(obj, func):
if isinstance(obj, list): if isinstance(obj, list):
new_obj = [] new_obj = []

View File

@@ -88,6 +88,8 @@ def refresh_libraries():
if result == 'insert': if result == 'insert':
new_keys.append(section['section_id']) new_keys.append(section['section_id'])
add_live_tv_library(refresh=True)
query = 'UPDATE library_sections SET is_active = 0 WHERE server_id != ? OR ' \ query = 'UPDATE library_sections SET is_active = 0 WHERE server_id != ? OR ' \
'section_id NOT IN ({})'.format(', '.join(['?'] * len(section_ids))) 'section_id NOT IN ({})'.format(', '.join(['?'] * len(section_ids)))
monitor_db.action(query=query, args=[plexpy.CONFIG.PMS_IDENTIFIER] + section_ids) monitor_db.action(query=query, args=[plexpy.CONFIG.PMS_IDENTIFIER] + section_ids)
@@ -115,28 +117,28 @@ def refresh_libraries():
return False return False
def add_live_tv_library(): def add_live_tv_library(refresh=False):
if not plexpy.CONFIG.ADD_LIVE_TV_LIBRARY: monitor_db = database.MonitorDatabase()
result = monitor_db.select_single('SELECT * FROM library_sections '
'WHERE section_id = ? and server_id = ?',
[common.LIVE_TV_SECTION_ID, plexpy.CONFIG.PMS_IDENTIFIER])
if result and not refresh or not result and refresh:
return return
logger.info("Tautulli Libraries :: Adding Live TV library to the database.") logger.info("Tautulli Libraries :: Adding Live TV library to the database.")
monitor_db = database.MonitorDatabase()
section_keys = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER, section_keys = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
'section_id': common.LIVE_TV_SECTION_ID} 'section_id': common.LIVE_TV_SECTION_ID}
section_values = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER, section_values = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
'section_id': common.LIVE_TV_SECTION_ID, 'section_id': common.LIVE_TV_SECTION_ID,
'section_name': common.LIVE_TV_SECTION_NAME, 'section_name': common.LIVE_TV_SECTION_NAME,
'section_type': 'live' 'section_type': 'live',
'is_active': 1
} }
result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values) result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values)
if result == 'insert':
plexpy.CONFIG.__setattr__('ADD_LIVE_TV_LIBRARY', 0)
plexpy.CONFIG.write()
def has_library_type(section_type): def has_library_type(section_type):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()

View File

@@ -19,6 +19,15 @@ import os
import subprocess import subprocess
import sys import sys
import plistlib import plistlib
try:
import AppKit
import Foundation
HAS_PYOBJC = True
except ImportError:
HAS_PYOBJC = False
if HAS_PYOBJC:
import rumps import rumps
import plexpy import plexpy
@@ -59,11 +68,11 @@ class MacOSSystemTray(object):
self.tray_icon = rumps.App(common.PRODUCT, icon=self.icon, menu=self.menu, quit_button=None) self.tray_icon = rumps.App(common.PRODUCT, icon=self.icon, menu=self.menu, quit_button=None)
def start(self): def start(self):
logger.info("Launching MacOS system tray icon.") logger.info("Launching MacOS menu bar icon.")
try: try:
self.tray_icon.run() self.tray_icon.run()
except Exception as e: except Exception as e:
logger.error("Unable to launch system tray icon: %s." % e) logger.error("Unable to launch menu bar icon: %s." % e)
def shutdown(self): def shutdown(self):
rumps.quit_application() rumps.quit_application()

View File

@@ -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:

View File

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

View File

@@ -882,8 +882,6 @@ class ANDROIDAPP(Notifier):
'priority': 3 'priority': 3
} }
_ONESIGNAL_APP_ID = '3b4b666a-d557-4b92-acdf-e2c8c4b95357'
def agent_notify(self, subject='', body='', action='', notification_id=None, **kwargs): def agent_notify(self, subject='', body='', action='', notification_id=None, **kwargs):
# Check mobile device is still registered # Check mobile device is still registered
device = mobile_app.get_mobile_devices(device_id=self.config['device_id']) device = mobile_app.get_mobile_devices(device_id=self.config['device_id'])
@@ -930,7 +928,7 @@ class ANDROIDAPP(Notifier):
#logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce))) #logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce)))
#logger.debug("Salt (base64): {}".format(base64.b64encode(salt))) #logger.debug("Salt (base64): {}".format(base64.b64encode(salt)))
payload = {'app_id': self._ONESIGNAL_APP_ID, payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
'include_player_ids': [self.config['device_id']], 'include_player_ids': [self.config['device_id']],
'contents': {'en': 'Tautulli Notification'}, 'contents': {'en': 'Tautulli Notification'},
'data': {'encrypted': True, 'data': {'encrypted': True,
@@ -943,7 +941,7 @@ class ANDROIDAPP(Notifier):
"Android app notifications will be sent unecrypted. " "Android app notifications will be sent unecrypted. "
"Install the library to encrypt the notifications.") "Install the library to encrypt the notifications.")
payload = {'app_id': self._ONESIGNAL_APP_ID, payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
'include_player_ids': [self.config['device_id']], 'include_player_ids': [self.config['device_id']],
'contents': {'en': 'Tautulli Notification'}, 'contents': {'en': 'Tautulli Notification'},
'data': {'encrypted': False, 'data': {'encrypted': False,
@@ -960,7 +958,7 @@ class ANDROIDAPP(Notifier):
db = database.MonitorDatabase() db = database.MonitorDatabase()
try: try:
query = 'SELECT * FROM mobile_devices' query = 'SELECT * FROM mobile_devices WHERE official = 1'
result = db.select(query=query) result = db.select(query=query)
except Exception as e: except Exception as e:
logger.warn("Tautulli Notifiers :: Unable to retrieve Android app devices list: %s." % e) logger.warn("Tautulli Notifiers :: Unable to retrieve Android app devices list: %s." % e)
@@ -1123,17 +1121,22 @@ class BROWSER(Notifier):
return True return True
def _return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Allow Notifications', config_option = [{'label': 'Note',
'description': 'You may need to refresh the page after saving for changes to take effect.',
'input_type': 'help'
},
{'label': 'Allow Notifications',
'value': 'Allow Notifications', 'value': 'Allow Notifications',
'name': 'browser_allow_browser', 'name': 'browser_allow_browser',
'description': 'Click to allow browser notifications. You must click this button for each browser.', 'description': 'Click to allow browser notifications. '
'You must click this button for each browser.',
'input_type': 'button' 'input_type': 'button'
}, },
{'label': 'Auto Hide Delay', {'label': 'Auto Hide Delay',
'value': self.config['auto_hide_delay'], 'value': self.config['auto_hide_delay'],
'name': 'browser_auto_hide_delay', 'name': 'browser_auto_hide_delay',
'description': 'Set the number of seconds for the notification to remain visible. \ 'description': 'Set the number of seconds for the notification to remain visible. '
Set 0 to disable auto hiding. (Note: Some browsers have a maximum time limit.)', 'Set 0 to disable auto hiding. (Note: Some browsers have a maximum time limit.)',
'input_type': 'number' 'input_type': 'number'
} }
] ]
@@ -1396,8 +1399,7 @@ class EMAIL(Notifier):
success = True success = True
except Exception as e: except Exception as e:
logger.error("Tautulli Notifiers :: {name} notification failed: {e}".format( logger.error("Tautulli Notifiers :: %s notification failed: %s", self.NAME, e)
name=self.NAME, e=str(e).decode('utf-8')))
finally: finally:
if mailserver: if mailserver:
@@ -2967,7 +2969,7 @@ class SCRIPTS(Notifier):
'.php': 'php', '.php': 'php',
'.pl': 'perl', '.pl': 'perl',
'.ps1': 'powershell -executionPolicy bypass -file', '.ps1': 'powershell -executionPolicy bypass -file',
'.py': 'python', '.py': 'python' if plexpy.FROZEN else sys.executable,
'.pyw': 'pythonw', '.pyw': 'pythonw',
'.rb': 'ruby', '.rb': 'ruby',
'.sh': '' '.sh': ''
@@ -3005,7 +3007,7 @@ class SCRIPTS(Notifier):
'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT, 'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT,
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY, 'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY,
'TAUTULLI_ENCODING': plexpy.SYS_ENCODING, 'TAUTULLI_ENCODING': plexpy.SYS_ENCODING,
'TAUTULLI_PYTHON_VERSION': '.'.join(map(str, plexpy.PYTHON_VERSION)) 'TAUTULLI_PYTHON_VERSION': common.PYTHON_VERSION
} }
if user_id: if user_id:

View File

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

View File

@@ -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

View File

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