Compare commits
77 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
97f80adf0b | ||
![]() |
2fc7b08909 | ||
![]() |
defceed696 | ||
![]() |
249533ac51 | ||
![]() |
12aee8762e | ||
![]() |
d9325b7adf | ||
![]() |
4975cad4fa | ||
![]() |
33fc079318 | ||
![]() |
b3b2752554 | ||
![]() |
505cf25ca3 | ||
![]() |
9747e3ba98 | ||
![]() |
729191722a | ||
![]() |
ff2cf73f23 | ||
![]() |
9c4d97c0f8 | ||
![]() |
be911e7700 | ||
![]() |
00629c0983 | ||
![]() |
52ebc9a908 | ||
![]() |
a029d6a931 | ||
![]() |
7641e3b081 | ||
![]() |
b54210480f | ||
![]() |
0d9c1c640e | ||
![]() |
7f84353c69 | ||
![]() |
c319a4a5cc | ||
![]() |
60f13df992 | ||
![]() |
dea51e32a5 | ||
![]() |
7019f5618b | ||
![]() |
9106c068ac | ||
![]() |
0b845294fb | ||
![]() |
7e850dd88d | ||
![]() |
877bf7060e | ||
![]() |
9326d03a57 | ||
![]() |
4787f42d2e | ||
![]() |
56a9ccd818 | ||
![]() |
1019fecc9e | ||
![]() |
1855f93c1c | ||
![]() |
52e6a44aa4 | ||
![]() |
0b77808af6 | ||
![]() |
9233ed5c53 | ||
![]() |
ee68c0f622 | ||
![]() |
366823cee9 | ||
![]() |
40e1eb9a49 | ||
![]() |
1af419a860 | ||
![]() |
397f18c435 | ||
![]() |
2e5dd05a6c | ||
![]() |
a9fb8ddfb8 | ||
![]() |
562c726787 | ||
![]() |
5f82c1dc17 | ||
![]() |
222800bdb6 | ||
![]() |
5dd3636571 | ||
![]() |
2296a9fbb3 | ||
![]() |
63b5a7c036 | ||
![]() |
b74ca2670e | ||
![]() |
393f4e0e58 | ||
![]() |
3a9ca29e99 | ||
![]() |
32995fef24 | ||
![]() |
a73c99fc64 | ||
![]() |
a5834470ba | ||
![]() |
da3bc127dc | ||
![]() |
0dddc4d58f | ||
![]() |
a4d5d9157b | ||
![]() |
c70d5d4398 | ||
![]() |
7c08b07ef5 | ||
![]() |
e426b5dd35 | ||
![]() |
2fdf619582 | ||
![]() |
d9eed14b7a | ||
![]() |
8230ffb8a4 | ||
![]() |
7098930b19 | ||
![]() |
56244245a4 | ||
![]() |
dd2f12fa8e | ||
![]() |
9598247a0d | ||
![]() |
230ee90b1c | ||
![]() |
e705bedc91 | ||
![]() |
b5ebe7590c | ||
![]() |
6d0831ceaa | ||
![]() |
19e00ee2f2 | ||
![]() |
80723d224e | ||
![]() |
ff1bd0a4b8 |
@@ -5,6 +5,7 @@ contrib
|
|||||||
init-scripts
|
init-scripts
|
||||||
package
|
package
|
||||||
pylintrc
|
pylintrc
|
||||||
|
snap
|
||||||
*.md
|
*.md
|
||||||
!CHANGELOG*.md
|
!CHANGELOG*.md
|
||||||
start.bat
|
start.bat
|
||||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,3 +1,3 @@
|
|||||||
github: JonnyWong16
|
github: JonnyWong16
|
||||||
patreon: Tautulli
|
patreon: Tautulli
|
||||||
custom: ["https://bit.ly/2InPp15"]
|
custom: ["https://bit.ly/2InPp15", "https://bit.ly/2WTq83m"]
|
20
.github/pull_request_template.md
vendored
Normal file
20
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
Please include a summary of the change and which issue is fixed.
|
||||||
|
|
||||||
|
Fixes Tautulli/Tautulli-Issues#(issue)
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
Please delete options that are not relevant.
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
|
||||||
|
- [ ] My code follows the style guidelines of this project
|
||||||
|
- [ ] I have performed a self-review of my own code
|
||||||
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] I have added or updated the docstring for new or existing methods
|
46
.github/workflows/publish-docker.yml
vendored
46
.github/workflows/publish-docker.yml
vendored
@@ -1,10 +1,14 @@
|
|||||||
name: Publish Docker
|
name: Publish Docker
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master, beta, nightly, python3]
|
branches: [master, beta, nightly]
|
||||||
tags: [v*]
|
tags: [v*]
|
||||||
|
pull_request: ~
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-docker:
|
||||||
|
name: Build Docker Image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
@@ -20,7 +24,9 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo ::set-output name=tag::${GITHUB_REF#refs/heads/}
|
echo ::set-output name=tag::${GITHUB_REF#refs/heads/}
|
||||||
fi
|
fi
|
||||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
if [[ $GITHUB_REF == refs/tags/*-beta ]]; then
|
||||||
|
echo ::set-output name=branch::beta
|
||||||
|
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||||
echo ::set-output name=branch::master
|
echo ::set-output name=branch::master
|
||||||
else
|
else
|
||||||
echo ::set-output name=branch::${GITHUB_REF#refs/heads/}
|
echo ::set-output name=branch::${GITHUB_REF#refs/heads/}
|
||||||
@@ -30,14 +36,12 @@ jobs:
|
|||||||
echo ::set-output name=docker_platforms::linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
|
echo ::set-output name=docker_platforms::linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
|
||||||
echo ::set-output name=docker_image::${{ secrets.DOCKER_REPO }}/tautulli
|
echo ::set-output name=docker_image::${{ secrets.DOCKER_REPO }}/tautulli
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set Up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
with:
|
|
||||||
platforms: all
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
|
id: buildx
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
@@ -51,14 +55,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
if: success()
|
if: success() && github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
if: success()
|
if: success() && github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
@@ -70,7 +74,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
platforms: ${{ steps.prepare.outputs.docker_platforms }}
|
platforms: ${{ steps.prepare.outputs.docker_platforms }}
|
||||||
build-args: |
|
build-args: |
|
||||||
TAG=${{ steps.prepare.outputs.tag }}
|
TAG=${{ steps.prepare.outputs.tag }}
|
||||||
@@ -83,11 +87,29 @@ jobs:
|
|||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
|
||||||
|
discord:
|
||||||
|
name: Discord Notification
|
||||||
|
needs: build-docker
|
||||||
|
if: always() && github.event_name != 'pull_request'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get Build Job Status
|
||||||
|
uses: technote-space/workflow-conclusion-action@v1
|
||||||
|
|
||||||
|
- name: Combine Job Status
|
||||||
|
id: status
|
||||||
|
run: |
|
||||||
|
failures=(neutral, skipped, timed_out, action_required)
|
||||||
|
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||||
|
echo ::set-output name=status::failure
|
||||||
|
else
|
||||||
|
echo ::set-output name=status::$WORKFLOW_CONCLUSION
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Post Status to Discord
|
- name: Post Status to Discord
|
||||||
uses: sarisia/actions-status-discord@v1
|
uses: sarisia/actions-status-discord@v1
|
||||||
if: always()
|
|
||||||
with:
|
with:
|
||||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
status: ${{ job.status }}
|
status: ${{ steps.status.outputs.status }}
|
||||||
title: ${{ github.workflow }}
|
title: ${{ github.workflow }}
|
||||||
nofail: true
|
nofail: true
|
||||||
|
@@ -1,79 +1,26 @@
|
|||||||
name: Publish Release
|
name: Publish Installers
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master, beta, nightly, python3]
|
branches: [master, beta, nightly]
|
||||||
tags: [v*]
|
tags: [v*]
|
||||||
|
pull_request: ~
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-windows:
|
build-installer:
|
||||||
runs-on: windows-latest
|
name: Build ${{ matrix.os_upper }} Installer
|
||||||
steps:
|
runs-on: ${{ matrix.os }}-latest
|
||||||
- name: Checkout Code
|
strategy:
|
||||||
uses: actions/checkout@v2
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'windows'
|
||||||
|
os_upper: 'Windows'
|
||||||
|
ext: 'exe'
|
||||||
|
- os: 'macos'
|
||||||
|
os_upper: 'MacOS'
|
||||||
|
ext: 'pkg'
|
||||||
|
|
||||||
- name: Set Release Version
|
|
||||||
id: get_version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
|
||||||
VERSION_NSIS=${GITHUB_REF#refs/tags/v}.1
|
|
||||||
echo ::set-output name=VERSION_NSIS::${VERSION_NSIS/%-beta.1/.0}
|
|
||||||
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
|
|
||||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
|
||||||
else
|
|
||||||
echo ::set-output name=VERSION_NSIS::0.0.0.0
|
|
||||||
echo ::set-output name=VERSION::0.0.0
|
|
||||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
|
|
||||||
fi
|
|
||||||
echo $GITHUB_SHA > version.txt
|
|
||||||
|
|
||||||
- name: Set Up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.8
|
|
||||||
|
|
||||||
- name: Cache Dependencies
|
|
||||||
id: cache_dependencies
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~\AppData\Local\pip\Cache
|
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-windows.txt') }}
|
|
||||||
restore-keys: ${{ runner.os }}-pip-
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r package/requirements-windows.txt
|
|
||||||
|
|
||||||
- name: Build Package
|
|
||||||
run: |
|
|
||||||
pyinstaller -y ./package/Tautulli-windows.spec
|
|
||||||
|
|
||||||
- name: Create Installer
|
|
||||||
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 }}-x64.exe
|
|
||||||
include-more-plugins: true
|
|
||||||
include-custom-plugins-path: package/nsis-plugins
|
|
||||||
|
|
||||||
- name: Upload Installer
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: Tautulli-windows-installer
|
|
||||||
path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
|
||||||
|
|
||||||
- name: Post Status to Discord
|
|
||||||
uses: sarisia/actions-status-discord@v1
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
status: ${{ job.status }}
|
|
||||||
title: Build Windows Installer
|
|
||||||
nofail: true
|
|
||||||
|
|
||||||
build-macos:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -84,13 +31,23 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||||
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
VERSION_NSIS=${GITHUB_REF#refs/tags/v}.1
|
||||||
|
echo ::set-output name=VERSION_NSIS::${VERSION_NSIS/%-beta.1/.0}
|
||||||
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
|
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
|
||||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
||||||
else
|
else
|
||||||
echo "VERSION=0.0.0" >> $GITHUB_ENV
|
echo "VERSION=0.0.0" >> $GITHUB_ENV
|
||||||
|
echo ::set-output name=VERSION_NSIS::0.0.0.0
|
||||||
echo ::set-output name=VERSION::0.0.0
|
echo ::set-output name=VERSION::0.0.0
|
||||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
|
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
|
||||||
fi
|
fi
|
||||||
|
if [[ $GITHUB_REF == refs/tags/*-beta ]]; then
|
||||||
|
echo "beta" > branch.txt
|
||||||
|
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||||
|
echo "master" > branch.txt
|
||||||
|
else
|
||||||
|
echo ${GITHUB_REF#refs/heads/} > branch.txt
|
||||||
|
fi
|
||||||
echo $GITHUB_SHA > version.txt
|
echo $GITHUB_SHA > version.txt
|
||||||
|
|
||||||
- name: Set Up Python
|
- name: Set Up Python
|
||||||
@@ -99,44 +56,56 @@ jobs:
|
|||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
id: cache_dependencies
|
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ~/Library/Caches/pip
|
path: ~\AppData\Local\pip\Cache
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-macos.txt') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles(format('package/requirements-{0}.txt', matrix.os)) }}
|
||||||
restore-keys: ${{ runner.os }}-pip-
|
restore-keys: ${{ runner.os }}-pip-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -r package/requirements-macos.txt
|
pip install -r package/requirements-${{ matrix.os }}.txt
|
||||||
|
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: |
|
run: |
|
||||||
pyinstaller -y ./package/Tautulli-macos.spec
|
pyinstaller -y ./package/Tautulli-${{ matrix.os }}.spec
|
||||||
|
|
||||||
- name: Create Installer
|
- name: Move Windows Updater Files
|
||||||
|
if: matrix.os == 'windows'
|
||||||
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 }}-x64.pkg
|
Move-Item dist\updater\* dist\Tautulli\ -Force
|
||||||
|
|
||||||
|
- name: Create Windows Installer
|
||||||
|
uses: joncloud/makensis-action@v3.4
|
||||||
|
if: matrix.os == 'windows'
|
||||||
|
with:
|
||||||
|
script-file: ./package/Tautulli.nsi
|
||||||
|
arguments: >
|
||||||
|
/DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }}
|
||||||
|
/DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||||
|
additional-plugin-paths: package/nsis-plugins
|
||||||
|
|
||||||
|
- name: Create MacOS Installer
|
||||||
|
if: matrix.os == 'macos'
|
||||||
|
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 }}-x64.pkg
|
||||||
|
|
||||||
- name: Upload Installer
|
- name: Upload Installer
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: Tautulli-macos-installer
|
name: Tautulli-${{ matrix.os }}-installer
|
||||||
path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
path: Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.${{ matrix.ext }}
|
||||||
|
|
||||||
- name: Post Status to Discord
|
|
||||||
uses: sarisia/actions-status-discord@v1
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
status: ${{ job.status }}
|
|
||||||
title: Build MacOS Installer
|
|
||||||
nofail: true
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [build-windows, build-macos]
|
name: Release Installers
|
||||||
if: startsWith(github.ref, 'refs/tags/') && always()
|
needs: build-installer
|
||||||
|
if: always() && startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Get Build Job Status
|
- name: Get Build Job Status
|
||||||
@@ -150,25 +119,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
||||||
|
|
||||||
- name: Download Windows Installer
|
- name: Download Installers
|
||||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v2
|
||||||
with:
|
|
||||||
name: Tautulli-windows-installer
|
|
||||||
|
|
||||||
- name: Download MacOS Installer
|
|
||||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
|
||||||
uses: actions/download-artifact@v2
|
|
||||||
with:
|
|
||||||
name: Tautulli-macos-installer
|
|
||||||
|
|
||||||
- name: Get Changelog
|
- name: Get Changelog
|
||||||
id: get_changelog
|
id: get_changelog
|
||||||
run: echo ::set-output name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md | sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )"
|
run: |
|
||||||
|
echo ::set-output name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md \
|
||||||
|
| sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )"
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
|
id: create_release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -182,23 +145,50 @@ jobs:
|
|||||||
prerelease: ${{ endsWith(steps.get_version.outputs.RELEASE_VERSION, '-beta') }}
|
prerelease: ${{ endsWith(steps.get_version.outputs.RELEASE_VERSION, '-beta') }}
|
||||||
|
|
||||||
- name: Upload Windows Installer
|
- name: Upload Windows Installer
|
||||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
|
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||||
env:
|
env:
|
||||||
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-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
asset_path: Tautulli-windows-installer/Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||||
asset_name: 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
|
asset_content_type: application/vnd.microsoft.portable-executable
|
||||||
|
|
||||||
- name: Upload MacOS Installer
|
- name: Upload MacOS Installer
|
||||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
|
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||||
env:
|
env:
|
||||||
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-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
asset_path: Tautulli-macos-installer/Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||||
asset_name: 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
|
asset_content_type: application/vnd.apple.installer+xml
|
||||||
|
|
||||||
|
discord:
|
||||||
|
name: Discord Notification
|
||||||
|
needs: [build-installer, release]
|
||||||
|
if: always() && github.event_name != 'pull_request'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get Build Job Status
|
||||||
|
uses: technote-space/workflow-conclusion-action@v1
|
||||||
|
|
||||||
|
- name: Combine Job Status
|
||||||
|
id: status
|
||||||
|
run: |
|
||||||
|
failures=(neutral, skipped, timed_out, action_required)
|
||||||
|
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||||
|
echo ::set-output name=status::failure
|
||||||
|
else
|
||||||
|
echo ::set-output name=status::$WORKFLOW_CONCLUSION
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post Status to Discord
|
||||||
|
uses: sarisia/actions-status-discord@v1
|
||||||
|
with:
|
||||||
|
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
status: ${{ steps.status.outputs.status }}
|
||||||
|
title: ${{ github.workflow }}
|
||||||
|
nofail: true
|
94
.github/workflows/publish-snap.yml
vendored
Normal file
94
.github/workflows/publish-snap.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
name: Publish Snap
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, beta, nightly]
|
||||||
|
tags: [v*]
|
||||||
|
pull_request: ~
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-snap:
|
||||||
|
name: Build Snap Package (${{ matrix.architecture }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
architecture:
|
||||||
|
- i386
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- armhf
|
||||||
|
- ppc64el
|
||||||
|
#- s390x # broken at the moment
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
id: prepare
|
||||||
|
run: |
|
||||||
|
git fetch --prune --unshallow --tags
|
||||||
|
if [[ $GITHUB_REF == refs/tags/*-beta || $GITHUB_REF == refs/heads/beta ]]; then
|
||||||
|
echo ::set-output name=RELEASE::beta
|
||||||
|
elif [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
|
||||||
|
echo ::set-output name=RELEASE::stable
|
||||||
|
else
|
||||||
|
echo ::set-output name=RELEASE::edge
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Set Up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Build Snap Package
|
||||||
|
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||||
|
id: build
|
||||||
|
with:
|
||||||
|
architecture: ${{ matrix.architecture }}
|
||||||
|
|
||||||
|
- name: Upload Snap Package
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: Tautulli-snap-package-${{ matrix.architecture }}
|
||||||
|
path: ${{ steps.build.outputs.snap }}
|
||||||
|
|
||||||
|
- name: Review Snap Package
|
||||||
|
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||||
|
with:
|
||||||
|
snap: ${{ steps.build.outputs.snap }}
|
||||||
|
|
||||||
|
- name: Publish Snap Package
|
||||||
|
uses: snapcore/action-publish@v1
|
||||||
|
if: >
|
||||||
|
github.event_name != 'pull_request' &&
|
||||||
|
(startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/nightly')
|
||||||
|
with:
|
||||||
|
store_login: ${{ secrets.SNAP_LOGIN }}
|
||||||
|
snap: ${{ steps.build.outputs.snap }}
|
||||||
|
release: ${{ steps.prepare.outputs.RELEASE }}
|
||||||
|
|
||||||
|
discord:
|
||||||
|
name: Discord Notification
|
||||||
|
needs: build-snap
|
||||||
|
if: always() && github.event_name != 'pull_request'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get Build Job Status
|
||||||
|
uses: technote-space/workflow-conclusion-action@v1
|
||||||
|
|
||||||
|
- name: Combine Job Status
|
||||||
|
id: status
|
||||||
|
run: |
|
||||||
|
failures=(neutral, skipped, timed_out, action_required)
|
||||||
|
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||||
|
echo ::set-output name=status::failure
|
||||||
|
else
|
||||||
|
echo ::set-output name=status::$WORKFLOW_CONCLUSION
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post Status to Discord
|
||||||
|
uses: sarisia/actions-status-discord@v1
|
||||||
|
with:
|
||||||
|
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
status: ${{ steps.status.outputs.status }}
|
||||||
|
title: ${{ github.workflow }}
|
||||||
|
nofail: true
|
28
.github/workflows/pull-requests.yml
vendored
Normal file
28
.github/workflows/pull-requests.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: Pull Requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, edited, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-branch:
|
||||||
|
name: Check Pull Request
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Comment on Pull Request
|
||||||
|
uses: mshick/add-pr-comment@v1
|
||||||
|
if: github.base_ref != 'nightly'
|
||||||
|
with:
|
||||||
|
message: Pull requests must be made to the `nightly` branch. Thanks.
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
repo-token-user-login: 'github-actions[bot]'
|
||||||
|
|
||||||
|
- name: Fail Workflow
|
||||||
|
if: github.base_ref != 'nightly'
|
||||||
|
run: |
|
||||||
|
echo Base: ${{ github.base_ref }}
|
||||||
|
echo Head: ${{ github.head_ref }}
|
||||||
|
exit 1
|
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
# Compiled source #
|
# Compiled source #
|
||||||
###################
|
###################
|
||||||
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
*.py~
|
*.py~
|
||||||
*.pyproj
|
*.pyproj
|
||||||
@@ -64,7 +65,6 @@ Thumbs.db
|
|||||||
*.bak
|
*.bak
|
||||||
*.cache
|
*.cache
|
||||||
*.ilk
|
*.ilk
|
||||||
*.log
|
|
||||||
[Bb]in
|
[Bb]in
|
||||||
[Dd]ebug*/
|
[Dd]ebug*/
|
||||||
*.lib
|
*.lib
|
||||||
@@ -81,3 +81,12 @@ _ReSharper*/
|
|||||||
#Ignore files generated by pyinstaller
|
#Ignore files generated by pyinstaller
|
||||||
/build
|
/build
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
|
#snapcraft specifics
|
||||||
|
/parts/
|
||||||
|
/stage/
|
||||||
|
/prime/
|
||||||
|
*.snap
|
||||||
|
.snapcraft
|
||||||
|
*_source.tar.bz2
|
||||||
|
snap/.snapcraft
|
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.6.5 (2021-01-09)
|
||||||
|
|
||||||
|
* Other:
|
||||||
|
* Fix: Some IP addresses not being masked in the logs.
|
||||||
|
* New: Auto-updater for Windows exe installer.
|
||||||
|
* Change: Allow Snap package to access the user home directory.
|
||||||
|
* Change: Migrate Snap user data to a persistent location that is retained if Tautulli is reinstalled.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.6.4 (2020-12-20)
|
||||||
|
|
||||||
|
* Other:
|
||||||
|
* Fix: Restore Snap data folder from previous installs.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.6.3 (2020-12-19)
|
||||||
|
|
||||||
|
* Announcements:
|
||||||
|
* This is the last Tautulli version to support Python 2. Python 3 will be required to continue receiving updates. You can check your Python version on the settings page.
|
||||||
|
* Exporter:
|
||||||
|
* Fix: Accessible and exists attributes were blank for media info export level 9.
|
||||||
|
* UI:
|
||||||
|
* Fix: Guest usernames were not masked on mouse hover.
|
||||||
|
* Other:
|
||||||
|
* Fix: macOS menu bar icon for light and dark mode.
|
||||||
|
* New: Tautulli can officially be installed on Linux using a Snap package. See the installation wiki for details.
|
||||||
|
|
||||||
|
|
||||||
## v2.6.2 (2020-12-05)
|
## v2.6.2 (2020-12-05)
|
||||||
|
|
||||||
* Notifications:
|
* Notifications:
|
||||||
|
@@ -38,7 +38,8 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Release | [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/releases/latest) | [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/commits/beta) | [](https://github.com/Tautulli/Tautulli/commits/nightly) <br> [](https://github.com/Tautulli/Tautulli/commits/nightly) |
|
| Release | [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/releases/latest) | [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/commits/beta) | [](https://github.com/Tautulli/Tautulli/commits/nightly) <br> [](https://github.com/Tautulli/Tautulli/commits/nightly) |
|
||||||
| Docker | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Amaster) | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Abeta) | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Anightly) |
|
| Docker | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Amaster) | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Abeta) | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Anightly) |
|
||||||
| Installer | [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Amaster) | [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Abeta) | [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Anightly) |
|
| Snap | [](https://snapcraft.io/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Snap"+branch%3Amaster) | [](https://snapcraft.io/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Snap"+branch%3Abeta) | [](https://snapcraft.io/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Snap"+branch%3Anightly) |
|
||||||
|
| Installer | [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Installers"+branch%3Amaster) | [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Installers"+branch%3Abeta) | [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Installers"+branch%3Anightly) |
|
||||||
|
|
||||||
[](https://github.com/Tautulli/Tautulli-Wiki/wiki)
|
[](https://github.com/Tautulli/Tautulli-Wiki/wiki)
|
||||||
[](https://tautulli.com/discord)
|
[](https://tautulli.com/discord)
|
||||||
|
12
Tautulli.py
12
Tautulli.py
@@ -31,6 +31,7 @@ import datetime
|
|||||||
import locale
|
import locale
|
||||||
import pytz
|
import pytz
|
||||||
import signal
|
import signal
|
||||||
|
import shutil
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import tzlocal
|
import tzlocal
|
||||||
@@ -124,6 +125,8 @@ def main():
|
|||||||
|
|
||||||
if helpers.bool_true(os.getenv('TAUTULLI_DOCKER', False)):
|
if helpers.bool_true(os.getenv('TAUTULLI_DOCKER', False)):
|
||||||
plexpy.DOCKER = True
|
plexpy.DOCKER = True
|
||||||
|
if helpers.bool_true(os.getenv('TAUTULLI_SNAP', False)):
|
||||||
|
plexpy.SNAP = True
|
||||||
|
|
||||||
if args.dev:
|
if args.dev:
|
||||||
plexpy.DEV = True
|
plexpy.DEV = True
|
||||||
@@ -186,6 +189,15 @@ def main():
|
|||||||
else:
|
else:
|
||||||
plexpy.DATA_DIR = plexpy.PROG_DIR
|
plexpy.DATA_DIR = plexpy.PROG_DIR
|
||||||
|
|
||||||
|
# Migrate Snap data dir
|
||||||
|
if plexpy.SNAP:
|
||||||
|
snap_common = os.environ['SNAP_COMMON']
|
||||||
|
old_data_dir = os.path.join(snap_common, 'Tautulli')
|
||||||
|
if os.path.exists(old_data_dir) and os.listdir(old_data_dir):
|
||||||
|
plexpy.SNAP_MIGRATE = True
|
||||||
|
logger.info("Migrating Snap user data.")
|
||||||
|
shutil.move(old_data_dir, plexpy.DATA_DIR)
|
||||||
|
|
||||||
if args.config:
|
if args.config:
|
||||||
config_file = args.config
|
config_file = args.config
|
||||||
else:
|
else:
|
||||||
|
@@ -59,7 +59,9 @@
|
|||||||
% endif
|
% endif
|
||||||
% if plexpy.INSTALL_TYPE == 'docker':
|
% if plexpy.INSTALL_TYPE == 'docker':
|
||||||
Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>
|
Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>
|
||||||
% elif plexpy.INSTALL_TYPE in ('windows', 'macos'):
|
% elif plexpy.INSTALL_TYPE == 'snap':
|
||||||
|
Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>
|
||||||
|
% elif plexpy.INSTALL_TYPE == 'macos':
|
||||||
<a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>
|
<a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>
|
||||||
% else:
|
% else:
|
||||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||||
@@ -235,6 +237,7 @@ ${next.modalIncludes()}
|
|||||||
<li class="active"><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="#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>
|
||||||
|
<li><a href="#crypto-donation" role="tab" data-toggle="tab">Crypto</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="github-donation" style="text-align: center">
|
<div role="tabpanel" class="tab-pane active" id="github-donation" style="text-align: center">
|
||||||
@@ -261,6 +264,14 @@ ${next.modalIncludes()}
|
|||||||
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
|
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="crypto-donation" style="text-align: center">
|
||||||
|
<p>
|
||||||
|
Click the button below to continue to Coinbase.
|
||||||
|
</p>
|
||||||
|
<a href="https://blankrefer.com/?https://commerce.coinbase.com/checkout/8a9fa08c-8a38-409e-9220-868124c4ba0c" target="_blank" rel="noreferrer" class="donate-with-crypto">
|
||||||
|
<span>Donate with Crypto</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -337,7 +348,9 @@ ${next.modalIncludes()}
|
|||||||
}
|
}
|
||||||
if (result.install_type === 'docker') {
|
if (result.install_type === 'docker') {
|
||||||
msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>';
|
msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||||
} else if (result.install_type === 'windows' || result.install_type === 'macos') {
|
} else if (result.install_type === 'snap') {
|
||||||
|
msg += 'Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||||
|
} else if (result.install_type === 'macos') {
|
||||||
msg += '<a href="' + result.release_url + '" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>'
|
msg += '<a href="' + result.release_url + '" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>'
|
||||||
} else {
|
} else {
|
||||||
msg += '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
msg += '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||||
|
@@ -4368,3 +4368,66 @@ a[data-tab-destination] {
|
|||||||
.news-body a:hover {
|
.news-body a:hover {
|
||||||
color: #f9be03;
|
color: #f9be03;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.donate-with-crypto,
|
||||||
|
a.donate-with-crypto > span {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
clear: none;
|
||||||
|
clip: auto;
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
float: none;
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
max-height: none;
|
||||||
|
min-height: none;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 1;
|
||||||
|
text-shadow: none;
|
||||||
|
vertical-align: baseline;
|
||||||
|
visibility: visible;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
a.donate-with-crypto {
|
||||||
|
user-select: none;
|
||||||
|
user-drag: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
text-decoration: none;
|
||||||
|
background: #1652f0 linear-gradient(#1652f0, #0655ab);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease-in-out, padding 0.2s;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 40px;
|
||||||
|
padding: 9px 15px 11px 15px;
|
||||||
|
position: relative;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
a.donate-with-crypto:hover {
|
||||||
|
background: #1652f0;
|
||||||
|
}
|
||||||
|
a.donate-with-crypto > span {
|
||||||
|
color: white;
|
||||||
|
font: normal 500 14px/20px -apple-system, BlinkMacSystemFont, '.SFNSText-Regular', 'San Francisco', 'Roboto', 'Segoe UI', 'Helvetica Neue', 'Lucida Grande', sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.15);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
a.donate-with-crypto::after {
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
@@ -212,6 +212,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% from plexpy.helpers import anon_url %>
|
||||||
|
<div id="python2-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="python2-modal">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title">Unable to Update</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="text-align: center;">
|
||||||
|
<p>Tautulli is still running using Python 2 and cannot be updated past v2.6.3.</p>
|
||||||
|
<p>Python 3 is required to continue receiving updates.</p>
|
||||||
|
<p>
|
||||||
|
<strong>Please see the <a href="${anon_url('https://github.com/Tautulli/Tautulli-Wiki/wiki/Upgrading-to-Python-3-%28Tautulli-v2.5%29')}" target="_blank" rel="noreferrer">wiki</a>
|
||||||
|
for instructions on how to upgrade to Python 3.</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="button" class="btn btn-bright" data-dismiss="modal" value="Close">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||||
@@ -258,22 +280,32 @@
|
|||||||
|
|
||||||
var error_msg = 'There was an error communicating with your Plex Server.' + msg_settings;
|
var error_msg = 'There was an error communicating with your Plex Server.' + msg_settings;
|
||||||
|
|
||||||
|
% if 'current_activity' in config['home_sections'] or 'recently_added' in config['home_sections']:
|
||||||
var server_status;
|
var server_status;
|
||||||
server_status = setInterval(function() {
|
server_status = setInterval(function() {
|
||||||
$.getJSON('server_status', function (data) {
|
$.getJSON('server_status', function (data) {
|
||||||
if (data.connected === true) {
|
if (data.connected === true) {
|
||||||
clearInterval(server_status);
|
clearInterval(server_status);
|
||||||
|
% if 'current_activity' in config['home_sections']:
|
||||||
$('#currentActivity').html('<div id="dashboard-checking-activity" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>');
|
$('#currentActivity').html('<div id="dashboard-checking-activity" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>');
|
||||||
$('#recentlyAdded').html('<div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>');
|
|
||||||
activityConnected();
|
activityConnected();
|
||||||
|
% endif
|
||||||
|
% if 'recently_added' in config['home_sections']:
|
||||||
|
$('#recentlyAdded').html('<div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>');
|
||||||
recentlyAddedConnected();
|
recentlyAddedConnected();
|
||||||
|
% endif
|
||||||
} else if (data.connected === false) {
|
} else if (data.connected === false) {
|
||||||
clearInterval(server_status);
|
clearInterval(server_status);
|
||||||
|
% if 'current_activity' in config['home_sections']:
|
||||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>');
|
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>');
|
||||||
|
% endif
|
||||||
|
% if 'recently_added' in config['home_sections']:
|
||||||
$('#recentlyAdded').html('<div id="dashboard-no-recently-added" class="text-muted">' + error_msg + '</div>');
|
$('#recentlyAdded').html('<div id="dashboard-no-recently-added" class="text-muted">' + error_msg + '</div>');
|
||||||
|
% endif
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
% endif
|
||||||
</script>
|
</script>
|
||||||
% if 'current_activity' in config['home_sections']:
|
% if 'current_activity' in config['home_sections']:
|
||||||
<script>
|
<script>
|
||||||
@@ -1010,4 +1042,16 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
% endif
|
% endif
|
||||||
|
% if _session['user_group'] == 'admin':
|
||||||
|
<script>
|
||||||
|
const queryString = window.location.search;
|
||||||
|
const urlParams = new URLSearchParams(queryString);
|
||||||
|
if (urlParams.get('update') === 'python2') {
|
||||||
|
$("#python2-modal").modal({
|
||||||
|
backdrop: 'static',
|
||||||
|
keyboard: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
</%def>
|
</%def>
|
@@ -220,7 +220,7 @@
|
|||||||
<p class="help-block">Check for Tautulli updates periodically.</p>
|
<p class="help-block">Check for Tautulli updates periodically.</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="git_update_options">
|
<div id="git_update_options">
|
||||||
% if not plexpy.FROZEN:
|
% if not plexpy.SNAP and not (plexpy.FROZEN and common.PLATFORM == 'Darwin'):
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="plexpy_auto_update" name="plexpy_auto_update" value="1" ${config['plexpy_auto_update']} ${docker_setting}> Update Automatically ${docker_msg | n}
|
<input type="checkbox" id="plexpy_auto_update" name="plexpy_auto_update" value="1" ${config['plexpy_auto_update']} ${docker_setting}> Update Automatically ${docker_msg | n}
|
||||||
@@ -3105,7 +3105,7 @@ $(document).ready(function() {
|
|||||||
if (news_item.subtitle) { content.append(subtitle); }
|
if (news_item.subtitle) { content.append(subtitle); }
|
||||||
content.append(body);
|
content.append(body);
|
||||||
var li = $('<li/>').append(header).append(content)
|
var li = $('<li/>').append(header).append(content)
|
||||||
if (index === 0 && Math.abs(now.diff(date, 'days')) < 7) {
|
if (index === 0 && Math.abs(now.diff(date, 'days')) <= 30) {
|
||||||
li.addClass('open');
|
li.addClass('open');
|
||||||
content.css('display', 'block');
|
content.css('display', 'block');
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
import sys
|
import sys
|
||||||
sys.modules['FixTk'] = None
|
sys.modules['FixTk'] = None
|
||||||
|
|
||||||
|
excludes = ['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter']
|
||||||
block_cipher = None
|
block_cipher = None
|
||||||
|
|
||||||
analysis = Analysis(
|
analysis = Analysis(
|
||||||
@@ -12,13 +13,27 @@ analysis = Analysis(
|
|||||||
('..\\data', 'data'),
|
('..\\data', 'data'),
|
||||||
('..\\CHANGELOG.md', '.'),
|
('..\\CHANGELOG.md', '.'),
|
||||||
('..\\LICENSE', '.'),
|
('..\\LICENSE', '.'),
|
||||||
|
('..\\branch.txt', '.'),
|
||||||
('..\\version.txt', '.'),
|
('..\\version.txt', '.'),
|
||||||
('..\\lib\\ipwhois\\data', 'data')
|
('..\\lib\\ipwhois\\data', 'data'),
|
||||||
|
('TautulliUpdateTask.xml', '.')
|
||||||
],
|
],
|
||||||
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'],
|
excludes=excludes,
|
||||||
hiddenimports=['pkg_resources.py2_warn', 'cheroot.ssl', 'cheroot.ssl.builtin'],
|
hiddenimports=['pkg_resources.py2_warn', 'cheroot.ssl', 'cheroot.ssl.builtin'],
|
||||||
cipher=block_cipher,
|
cipher=block_cipher
|
||||||
)
|
)
|
||||||
|
updater_analysis = Analysis(
|
||||||
|
['updater-windows.py'],
|
||||||
|
pathex=['lib'],
|
||||||
|
excludes=excludes,
|
||||||
|
cipher=block_cipher
|
||||||
|
)
|
||||||
|
|
||||||
|
MERGE(
|
||||||
|
(analysis, 'Tautulli', 'Tautulli'),
|
||||||
|
(updater_analysis, 'updater', 'updater')
|
||||||
|
)
|
||||||
|
|
||||||
pyz = PYZ(
|
pyz = PYZ(
|
||||||
analysis.pure,
|
analysis.pure,
|
||||||
analysis.zipped_data,
|
analysis.zipped_data,
|
||||||
@@ -39,3 +54,24 @@ coll = COLLECT(
|
|||||||
analysis.datas,
|
analysis.datas,
|
||||||
name='Tautulli'
|
name='Tautulli'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
updater_pyz = PYZ(
|
||||||
|
updater_analysis.pure,
|
||||||
|
updater_analysis.zipped_data,
|
||||||
|
cipher=block_cipher
|
||||||
|
)
|
||||||
|
updater_exe = EXE(
|
||||||
|
updater_pyz,
|
||||||
|
updater_analysis.scripts,
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='updater',
|
||||||
|
console=False,
|
||||||
|
icon='..\\data\\interfaces\\default\\images\\logo-circle.ico'
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
updater_exe,
|
||||||
|
updater_analysis.binaries,
|
||||||
|
updater_analysis.zipfiles,
|
||||||
|
updater_analysis.datas,
|
||||||
|
name='updater'
|
||||||
|
)
|
||||||
|
@@ -32,6 +32,7 @@ VIAddVersionKey "FileVersion" "${VERSION}"
|
|||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
Unicode True
|
||||||
SetCompressor ZLIB
|
SetCompressor ZLIB
|
||||||
Name "${APP_NAME}"
|
Name "${APP_NAME}"
|
||||||
Caption "${APP_NAME}"
|
Caption "${APP_NAME}"
|
||||||
@@ -39,7 +40,7 @@ OutFile "${INSTALLER_NAME}"
|
|||||||
BrandingText "${APP_NAME}"
|
BrandingText "${APP_NAME}"
|
||||||
XPStyle on
|
XPStyle on
|
||||||
InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" ""
|
InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" ""
|
||||||
InstallDir "$PROGRAMFILES\${APP_NAME}"
|
InstallDir "$PROGRAMFILES64\${APP_NAME}"
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
@@ -76,9 +77,13 @@ InstallDir "$PROGRAMFILES\${APP_NAME}"
|
|||||||
|
|
||||||
!include Sections.nsh
|
!include Sections.nsh
|
||||||
|
|
||||||
|
Var /GLOBAL norun
|
||||||
Var /GLOBAL nolaunch
|
Var /GLOBAL nolaunch
|
||||||
|
|
||||||
!include "MUI.nsh"
|
!include "MUI.nsh"
|
||||||
|
!include "FileFunc.nsh"
|
||||||
|
!insertmacro GetParameters
|
||||||
|
!insertmacro GetOptions
|
||||||
|
|
||||||
!define MUI_ABORTWARNING
|
!define MUI_ABORTWARNING
|
||||||
!define MUI_UNABORTWARNING
|
!define MUI_UNABORTWARNING
|
||||||
@@ -99,6 +104,7 @@ Var /GLOBAL nolaunch
|
|||||||
!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder
|
!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
!insertmacro MUI_PAGE_INSTFILES
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
|
||||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\${MAIN_APP_EXE}"
|
!define MUI_FINISHPAGE_RUN "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
@@ -119,10 +125,14 @@ Section -MainProgram
|
|||||||
Call UninstallPrevious
|
Call UninstallPrevious
|
||||||
|
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
SetOverwrite ifnewer
|
SetOverwrite on
|
||||||
SetOutPath "$INSTDIR"
|
SetOutPath "$INSTDIR"
|
||||||
File /nonfatal /a /r "..\dist\${APP_NAME}\"
|
File /nonfatal /a /r "..\dist\${APP_NAME}\"
|
||||||
|
|
||||||
|
nsExec::Exec "$INSTDIR\updater.exe --xml"
|
||||||
|
nsExec::Exec '$SYSDIR\SCHTASKS /Create /TN TautulliUpdateTask /XML "$INSTDIR\TautulliUpdateTask.xml" /F'
|
||||||
|
|
||||||
|
StrCmp $norun 1 +3 0
|
||||||
IfSilent 0 +2
|
IfSilent 0 +2
|
||||||
ExecShell "" "$INSTDIR\${MAIN_APP_EXE}" $nolaunch
|
ExecShell "" "$INSTDIR\${MAIN_APP_EXE}" $nolaunch
|
||||||
SectionEnd
|
SectionEnd
|
||||||
@@ -208,11 +218,20 @@ RmDir "$SMPROGRAMS\${APP_NAME}"
|
|||||||
|
|
||||||
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
||||||
|
|
||||||
|
nsExec::Exec "$SYSDIR\SCHTASKS /Delete /TN TautulliUpdateTask /F"
|
||||||
|
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
Function .onInit
|
Function .onInit
|
||||||
|
StrCpy $norun 0
|
||||||
|
${GetParameters} $CMDLINE
|
||||||
|
${GetOptions} "$CMDLINE" "/NORUN" $R0
|
||||||
|
IfErrors +2 0
|
||||||
|
StrCpy $norun 1
|
||||||
|
|
||||||
IfSilent 0 +2
|
IfSilent 0 +2
|
||||||
StrCpy $nolaunch "--nolaunch"
|
StrCpy $nolaunch "--nolaunch"
|
||||||
|
|
||||||
|
BIN
package/TautulliUpdateTask.xml
Normal file
BIN
package/TautulliUpdateTask.xml
Normal file
Binary file not shown.
Binary file not shown.
BIN
package/nsis-plugins/x86-unicode/nsProcess.dll
Normal file
BIN
package/nsis-plugins/x86-unicode/nsProcess.dll
Normal file
Binary file not shown.
@@ -1,4 +1,5 @@
|
|||||||
pyinstaller==3.6
|
apscheduler==3.6.3
|
||||||
|
pyinstaller==4.1
|
||||||
pyopenssl==20.0.0
|
pyopenssl==20.0.0
|
||||||
pycryptodomex==3.9.9
|
pycryptodomex==3.9.9
|
||||||
pyobjc-framework-Cocoa==6.2.2
|
pyobjc-framework-Cocoa==6.2.2
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
pyinstaller==3.6
|
apscheduler==3.6.3
|
||||||
|
psutil==5.8.0
|
||||||
|
pyinstaller==4.1
|
||||||
pyopenssl==20.0.0
|
pyopenssl==20.0.0
|
||||||
pycryptodomex==3.9.9
|
pycryptodomex==3.9.9
|
||||||
pywin32==300
|
pywin32==300
|
||||||
|
194
package/updater-windows.py
Normal file
194
package/updater-windows.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
|
#
|
||||||
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Tautulli is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from logging import handlers
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
CREATE_NO_WINDOW = 0x08000000
|
||||||
|
REPO_URL = 'https://api.github.com/repos/Tautulli/Tautulli'
|
||||||
|
|
||||||
|
LOGFILE = 'updater.log'
|
||||||
|
LOGPATH = os.path.join(SCRIPT_PATH, LOGFILE)
|
||||||
|
MAX_SIZE = 1000000 # 1MB
|
||||||
|
MAX_FILES = 1
|
||||||
|
|
||||||
|
|
||||||
|
def init_logger():
|
||||||
|
log = logging.getLogger('updater')
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
file_formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(levelname)-7s :: %(threadName)s : Tautulli Updater :: %(message)s',
|
||||||
|
'%Y-%m-%d %H:%M:%S')
|
||||||
|
file_handler = handlers.RotatingFileHandler(
|
||||||
|
LOGPATH, maxBytes=MAX_SIZE, backupCount=MAX_FILES, encoding='utf-8')
|
||||||
|
file_handler.setFormatter(file_formatter)
|
||||||
|
log.addHandler(file_handler)
|
||||||
|
return log
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(file_path):
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
return f.read().strip(' \n\r')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('Read file error: %s', e)
|
||||||
|
raise Exception(1)
|
||||||
|
|
||||||
|
|
||||||
|
def request_json(url):
|
||||||
|
try:
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('Request error: %s', e)
|
||||||
|
raise Exception(2)
|
||||||
|
|
||||||
|
|
||||||
|
def kill_and_get_processes(process_name):
|
||||||
|
processes = []
|
||||||
|
for process in psutil.process_iter():
|
||||||
|
if process.name() == process_name:
|
||||||
|
processes.append(process.cmdline())
|
||||||
|
logger.info('Sending SIGTERM to %s (PID=%d)', process.name(), process.pid)
|
||||||
|
process.terminate()
|
||||||
|
return processes
|
||||||
|
|
||||||
|
|
||||||
|
def update_tautulli():
|
||||||
|
logger.info('Starting Tautulli update check')
|
||||||
|
|
||||||
|
branch = read_file(os.path.join(SCRIPT_PATH, 'branch.txt'))
|
||||||
|
logger.info('Branch: %s', branch)
|
||||||
|
|
||||||
|
current_version = read_file(os.path.join(SCRIPT_PATH, 'version.txt'))
|
||||||
|
logger.info('Current version: %s', current_version)
|
||||||
|
|
||||||
|
logger.info('Retrieving latest version from GitHub')
|
||||||
|
commits = request_json('{}/commits/{}'.format(REPO_URL, branch))
|
||||||
|
latest_version = commits['sha']
|
||||||
|
logger.info('Latest version: %s', latest_version)
|
||||||
|
|
||||||
|
if current_version == latest_version:
|
||||||
|
logger.info('Tautulli is already up to date')
|
||||||
|
return 0
|
||||||
|
|
||||||
|
logger.info('Comparing version on GitHub')
|
||||||
|
compare = request_json('{}/compare/{}...{}'.format(REPO_URL, latest_version, current_version))
|
||||||
|
commits_behind = compare['behind_by']
|
||||||
|
logger.info('Commits behind: %s', commits_behind)
|
||||||
|
|
||||||
|
if commits_behind <= 0:
|
||||||
|
logger.info('Tautulli is already up to date')
|
||||||
|
return 0
|
||||||
|
|
||||||
|
logger.info('Retrieving releases on GitHub')
|
||||||
|
releases = request_json('{}/releases'.format(REPO_URL))
|
||||||
|
|
||||||
|
if branch == 'master':
|
||||||
|
release = next((r for r in releases if not r['prerelease']), releases[0])
|
||||||
|
else:
|
||||||
|
release = next((r for r in releases), releases[0])
|
||||||
|
|
||||||
|
version = release['tag_name']
|
||||||
|
logger.info('Release: %s', version)
|
||||||
|
|
||||||
|
win_exe = 'application/vnd.microsoft.portable-executable'
|
||||||
|
asset = next((a for a in release['assets'] if a['content_type'] == win_exe), None)
|
||||||
|
download_url = asset['browser_download_url']
|
||||||
|
download_file = asset['name']
|
||||||
|
|
||||||
|
file_path = os.path.join(tempfile.gettempdir(), download_file)
|
||||||
|
logger.info('Downloading installer to temporary directory: %s', file_path)
|
||||||
|
try:
|
||||||
|
with requests.get(download_url, stream=True) as r:
|
||||||
|
with open(file_path, 'wb') as f:
|
||||||
|
shutil.copyfileobj(r.raw, f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('Failed to download %s: %s', download_file, e)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
logger.info('Stopping Tautulli processes')
|
||||||
|
try:
|
||||||
|
processes = kill_and_get_processes('Tautulli.exe')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('Failed to stop Tautulli: %s', e)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
logger.info('Running %s', download_file)
|
||||||
|
try:
|
||||||
|
subprocess.call([file_path, '/S', '/NORUN', '/D=' + SCRIPT_PATH], creationflags=CREATE_NO_WINDOW)
|
||||||
|
status = 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception('Failed to install Tautulli: %s', e)
|
||||||
|
status = -1
|
||||||
|
|
||||||
|
if status == 0:
|
||||||
|
logger.info('Tautulli updated to %s', version)
|
||||||
|
|
||||||
|
logger.info('Restarting Tautulli processes')
|
||||||
|
for process in processes:
|
||||||
|
logger.info('Starting process: %s', process)
|
||||||
|
subprocess.Popen(process, creationflags=CREATE_NO_WINDOW)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--xml', action='store_true')
|
||||||
|
opts = parser.parse_args()
|
||||||
|
|
||||||
|
if opts.xml:
|
||||||
|
xml_path = os.path.join(SCRIPT_PATH, 'TautulliUpdateTask.xml')
|
||||||
|
tree = ET.parse(xml_path)
|
||||||
|
task = tree.getroot()
|
||||||
|
|
||||||
|
match = re.match(r'{(.*)}', task.tag)
|
||||||
|
namespace = match.group(1)
|
||||||
|
namespaces = {'': namespace}
|
||||||
|
ET.register_namespace('', namespace)
|
||||||
|
|
||||||
|
for elem in task.iterfind('./Actions/Exec/Command', namespaces=namespaces):
|
||||||
|
elem.text = os.path.join(SCRIPT_PATH, 'updater.exe')
|
||||||
|
for elem in task.iterfind('./Actions/Exec/WorkingDirectory', namespaces=namespaces):
|
||||||
|
elem.text = SCRIPT_PATH
|
||||||
|
|
||||||
|
tree.write(xml_path, encoding='UTF-16')
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger = init_logger()
|
||||||
|
|
||||||
|
try:
|
||||||
|
status_code = update_tautulli()
|
||||||
|
except Exception as exc:
|
||||||
|
status_code = exc
|
||||||
|
logger.debug('Update function returned status code %s', status_code)
|
||||||
|
|
||||||
|
sys.exit(status_code)
|
@@ -98,6 +98,8 @@ CREATEPID = False
|
|||||||
PIDFILE = None
|
PIDFILE = None
|
||||||
NOFORK = False
|
NOFORK = False
|
||||||
DOCKER = False
|
DOCKER = False
|
||||||
|
SNAP = False
|
||||||
|
SNAP_MIGRATE = False
|
||||||
FROZEN = False
|
FROZEN = False
|
||||||
|
|
||||||
SCHED = None
|
SCHED = None
|
||||||
@@ -172,6 +174,18 @@ def initialize(config_file):
|
|||||||
if _INITIALIZED:
|
if _INITIALIZED:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if SNAP_MIGRATE:
|
||||||
|
snap_common = os.environ['SNAP_COMMON']
|
||||||
|
old_data_dir = os.path.join(snap_common, 'Tautulli')
|
||||||
|
CONFIG.HTTPS_CERT = CONFIG.HTTPS_CERT.replace(old_data_dir, DATA_DIR)
|
||||||
|
CONFIG.HTTPS_CERT_CHAIN = CONFIG.HTTPS_CERT_CHAIN.replace(old_data_dir, DATA_DIR)
|
||||||
|
CONFIG.HTTPS_KEY = CONFIG.HTTPS_KEY.replace(old_data_dir, DATA_DIR)
|
||||||
|
CONFIG.LOG_DIR = CONFIG.LOG_DIR.replace(old_data_dir, DATA_DIR)
|
||||||
|
CONFIG.BACKUP_DIR = CONFIG.BACKUP_DIR.replace(old_data_dir, DATA_DIR)
|
||||||
|
CONFIG.CACHE_DIR = CONFIG.CACHE_DIR.replace(old_data_dir, DATA_DIR)
|
||||||
|
CONFIG.EXPORT_DIR = CONFIG.EXPORT_DIR.replace(old_data_dir, DATA_DIR)
|
||||||
|
CONFIG.NEWSLETTER_DIR = CONFIG.NEWSLETTER_DIR.replace(old_data_dir, DATA_DIR)
|
||||||
|
|
||||||
if CONFIG.HTTP_PORT < 21 or CONFIG.HTTP_PORT > 65535:
|
if CONFIG.HTTP_PORT < 21 or CONFIG.HTTP_PORT > 65535:
|
||||||
logger.warn("HTTP_PORT out of bounds: 21 < %s < 65535", CONFIG.HTTP_PORT)
|
logger.warn("HTTP_PORT out of bounds: 21 < %s < 65535", CONFIG.HTTP_PORT)
|
||||||
CONFIG.HTTP_PORT = 8181
|
CONFIG.HTTP_PORT = 8181
|
||||||
@@ -194,6 +208,8 @@ def initialize(config_file):
|
|||||||
|
|
||||||
if DOCKER:
|
if DOCKER:
|
||||||
build = '[Docker] '
|
build = '[Docker] '
|
||||||
|
elif SNAP:
|
||||||
|
build = '[Snap] '
|
||||||
elif FROZEN:
|
elif FROZEN:
|
||||||
build = '[Bundle] '
|
build = '[Bundle] '
|
||||||
else:
|
else:
|
||||||
@@ -498,12 +514,16 @@ def schedule_job(func, name, hours=0, minutes=0, seconds=0, args=None):
|
|||||||
SCHED.remove_job(name)
|
SCHED.remove_job(name)
|
||||||
logger.info("Removed background task: %s", name)
|
logger.info("Removed background task: %s", name)
|
||||||
elif job.trigger.interval != datetime.timedelta(hours=hours, minutes=minutes):
|
elif job.trigger.interval != datetime.timedelta(hours=hours, minutes=minutes):
|
||||||
SCHED.reschedule_job(name, trigger=IntervalTrigger(
|
SCHED.reschedule_job(
|
||||||
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC), args=args)
|
name, trigger=IntervalTrigger(
|
||||||
|
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC),
|
||||||
|
args=args)
|
||||||
logger.info("Re-scheduled background task: %s", name)
|
logger.info("Re-scheduled background task: %s", name)
|
||||||
elif hours > 0 or minutes > 0 or seconds > 0:
|
elif hours > 0 or minutes > 0 or seconds > 0:
|
||||||
SCHED.add_job(func, id=name, trigger=IntervalTrigger(
|
SCHED.add_job(
|
||||||
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC), args=args)
|
func, id=name, trigger=IntervalTrigger(
|
||||||
|
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC),
|
||||||
|
args=args, misfire_grace_time=None)
|
||||||
logger.info("Scheduled background task: %s", name)
|
logger.info("Scheduled background task: %s", name)
|
||||||
|
|
||||||
|
|
||||||
@@ -2275,7 +2295,12 @@ def upgrade():
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def shutdown(restart=False, update=False, checkout=False, reset=False):
|
def shutdown(restart=False, update=False, checkout=False, reset=False,
|
||||||
|
_shutdown=True):
|
||||||
|
if FROZEN and common.PLATFORM == 'Windows' and update:
|
||||||
|
restart = False
|
||||||
|
_shutdown = False
|
||||||
|
|
||||||
webstart.stop()
|
webstart.stop()
|
||||||
|
|
||||||
# Shutdown the websocket connection
|
# Shutdown the websocket connection
|
||||||
@@ -2348,14 +2373,15 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
|
|||||||
else:
|
else:
|
||||||
logger.info("Tautulli is shutting down...")
|
logger.info("Tautulli is shutting down...")
|
||||||
|
|
||||||
logger.shutdown()
|
if _shutdown:
|
||||||
|
logger.shutdown()
|
||||||
|
|
||||||
if WIN_SYS_TRAY_ICON:
|
if WIN_SYS_TRAY_ICON:
|
||||||
WIN_SYS_TRAY_ICON.shutdown()
|
WIN_SYS_TRAY_ICON.shutdown()
|
||||||
elif MAC_SYS_TRAY_ICON:
|
elif MAC_SYS_TRAY_ICON:
|
||||||
MAC_SYS_TRAY_ICON.shutdown()
|
MAC_SYS_TRAY_ICON.shutdown()
|
||||||
|
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
|
|
||||||
def generate_uuid():
|
def generate_uuid():
|
||||||
|
@@ -606,7 +606,8 @@ def schedule_callback(id, func=None, remove_job=False, args=None, **kwargs):
|
|||||||
ACTIVITY_SCHED.add_job(
|
ACTIVITY_SCHED.add_job(
|
||||||
func, args=args, id=id, trigger=DateTrigger(
|
func, args=args, id=id, trigger=DateTrigger(
|
||||||
run_date=datetime.datetime.now(pytz.UTC) + datetime.timedelta(**kwargs),
|
run_date=datetime.datetime.now(pytz.UTC) + datetime.timedelta(**kwargs),
|
||||||
timezone=pytz.UTC))
|
timezone=pytz.UTC),
|
||||||
|
misfire_grace_time=None)
|
||||||
|
|
||||||
|
|
||||||
def force_stop_stream(session_key, title, user):
|
def force_stop_stream(session_key, title, user):
|
||||||
|
@@ -257,7 +257,7 @@ def import_tautulli_config(config=None, backup=False):
|
|||||||
# Remove keys that should not be imported
|
# Remove keys that should not be imported
|
||||||
for key in _DO_NOT_IMPORT_KEYS:
|
for key in _DO_NOT_IMPORT_KEYS:
|
||||||
delattr(imported_config, key)
|
delattr(imported_config, key)
|
||||||
if plexpy.DOCKER:
|
if plexpy.DOCKER or plexpy.SNAP:
|
||||||
for key in _DO_NOT_IMPORT_KEYS_DOCKER:
|
for key in _DO_NOT_IMPORT_KEYS_DOCKER:
|
||||||
delattr(imported_config, key)
|
delattr(imported_config, key)
|
||||||
|
|
||||||
@@ -540,3 +540,9 @@ class Config(object):
|
|||||||
self.JWT_UPDATE_SECRET = True
|
self.JWT_UPDATE_SECRET = True
|
||||||
|
|
||||||
self.CONFIG_VERSION = 16
|
self.CONFIG_VERSION = 16
|
||||||
|
|
||||||
|
if self.CONFIG_VERSION == 16:
|
||||||
|
if plexpy.SNAP:
|
||||||
|
self.PLEXPY_AUTO_UPDATE = 0
|
||||||
|
|
||||||
|
self.CONFIG_VERSION = 17
|
||||||
|
@@ -228,7 +228,7 @@ 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)
|
||||||
|
|
||||||
# SQlite verions prior to 3.32.0 (2020-05-22) have maximum variable limit of 999
|
# SQlite versions prior to 3.32.0 (2020-05-22) have maximum variable limit of 999
|
||||||
# https://sqlite.org/limits.html
|
# https://sqlite.org/limits.html
|
||||||
sqlite_max_variable_number = 999
|
sqlite_max_variable_number = 999
|
||||||
|
|
||||||
|
@@ -1885,7 +1885,8 @@ class Export(object):
|
|||||||
elif self.media_type == 'playlist' and 'item' in self._custom_fields:
|
elif self.media_type == 'playlist' and 'item' in self._custom_fields:
|
||||||
export_attrs_set.update(self._custom_fields['item'])
|
export_attrs_set.update(self._custom_fields['item'])
|
||||||
|
|
||||||
if 'media.parts.accessible' in export_attrs_set or 'media.parts.exists' in export_attrs_set:
|
if 'media.parts.accessible' in export_attrs_set or 'media.parts.exists' in export_attrs_set or \
|
||||||
|
self.media_info_level == 9:
|
||||||
self._reload_check_files = True
|
self._reload_check_files = True
|
||||||
|
|
||||||
for attr in export_attrs_set:
|
for attr in export_attrs_set:
|
||||||
|
@@ -19,11 +19,11 @@ from __future__ import unicode_literals
|
|||||||
from future.builtins import object
|
from future.builtins import object
|
||||||
from future.builtins import str
|
from future.builtins import str
|
||||||
|
|
||||||
from functools import partial
|
|
||||||
from multiprocessing.dummy import Pool as ThreadPool
|
from multiprocessing.dummy import Pool as ThreadPool
|
||||||
from future.moves.urllib.parse import urljoin
|
from future.moves.urllib.parse import urljoin
|
||||||
|
|
||||||
import certifi
|
import certifi
|
||||||
|
import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
@@ -41,6 +41,7 @@ class HTTPHandler(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, urls, headers=None, token=None, timeout=10, ssl_verify=True, silent=False):
|
def __init__(self, urls, headers=None, token=None, timeout=10, ssl_verify=True, silent=False):
|
||||||
|
self._valid_request_types = {'GET', 'POST', 'PUT', 'DELETE'}
|
||||||
self._silent = silent
|
self._silent = silent
|
||||||
|
|
||||||
if isinstance(urls, str):
|
if isinstance(urls, str):
|
||||||
@@ -51,24 +52,34 @@ class HTTPHandler(object):
|
|||||||
if headers:
|
if headers:
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
else:
|
else:
|
||||||
self.headers = {'X-Plex-Product': plexpy.common.PRODUCT,
|
self.headers = {
|
||||||
'X-Plex-Version': plexpy.common.RELEASE,
|
'X-Plex-Product': plexpy.common.PRODUCT,
|
||||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
'X-Plex-Version': plexpy.common.RELEASE,
|
||||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
|
'X-Plex-Platform': plexpy.common.PLATFORM,
|
||||||
'X-Plex-Device': '{} {}'.format(plexpy.common.PLATFORM,
|
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
|
||||||
plexpy.common.PLATFORM_RELEASE),
|
'X-Plex-Device': '{} {}'.format(plexpy.common.PLATFORM,
|
||||||
'X-Plex-Device-Name': plexpy.common.PLATFORM_DEVICE_NAME
|
plexpy.common.PLATFORM_RELEASE),
|
||||||
}
|
'X-Plex-Device-Name': plexpy.common.PLATFORM_DEVICE_NAME
|
||||||
|
}
|
||||||
|
|
||||||
self.token = token
|
self.token = token
|
||||||
if self.token:
|
if self.token:
|
||||||
self.headers['X-Plex-Token'] = self.token
|
self.headers['X-Plex-Token'] = self.token
|
||||||
|
|
||||||
|
self._session = requests.Session()
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.ssl_verify = ssl_verify
|
self.ssl_verify = certifi.where() if ssl_verify else False
|
||||||
|
if not self.ssl_verify:
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
self.valid_request_types = ('GET', 'POST', 'PUT', 'DELETE')
|
self.uri = None
|
||||||
|
self.data = None
|
||||||
|
self.request_type = 'GET'
|
||||||
|
self.output_format = 'raw'
|
||||||
|
self.return_type = False
|
||||||
|
self.callback = None
|
||||||
|
self.request_kwargs = {}
|
||||||
|
|
||||||
def make_request(self,
|
def make_request(self,
|
||||||
uri=None,
|
uri=None,
|
||||||
@@ -96,7 +107,7 @@ class HTTPHandler(object):
|
|||||||
self.timeout = timeout or self.timeout
|
self.timeout = timeout or self.timeout
|
||||||
self.request_kwargs = request_kwargs
|
self.request_kwargs = request_kwargs
|
||||||
|
|
||||||
if self.request_type not in self.valid_request_types:
|
if self.request_type not in self._valid_request_types:
|
||||||
logger.debug("HTTP request made but unsupported request type given.")
|
logger.debug("HTTP request made but unsupported request type given.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -115,7 +126,7 @@ class HTTPHandler(object):
|
|||||||
return responses[0]
|
return responses[0]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug("HTTP request made but no enpoint given.")
|
logger.debug("HTTP request made but no uri endpoint provided.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _http_requests_pool(self, urls, workers=10, chunk=None):
|
def _http_requests_pool(self, urls, workers=10, chunk=None):
|
||||||
@@ -128,20 +139,13 @@ class HTTPHandler(object):
|
|||||||
if len(urls) == 0:
|
if len(urls) == 0:
|
||||||
chunk = 0
|
chunk = 0
|
||||||
|
|
||||||
if self.ssl_verify:
|
|
||||||
session = urllib3.PoolManager(cert_reqs=2, ca_certs=certifi.where()) # ssl.CERT_REQUIRED = 2
|
|
||||||
else:
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
||||||
session = urllib3.PoolManager()
|
|
||||||
part = partial(self._http_requests_urllib3, session=session)
|
|
||||||
|
|
||||||
if len(urls) == 1:
|
if len(urls) == 1:
|
||||||
yield part(urls[0])
|
yield self._http_requests_single(urls[0])
|
||||||
else:
|
else:
|
||||||
pool = ThreadPool(workers)
|
pool = ThreadPool(workers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for work in pool.imap_unordered(part, urls, chunk):
|
for work in pool.imap_unordered(self._http_requests_single, urls, chunk):
|
||||||
yield work
|
yield work
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not self._silent:
|
if not self._silent:
|
||||||
@@ -150,34 +154,40 @@ class HTTPHandler(object):
|
|||||||
pool.close()
|
pool.close()
|
||||||
pool.join()
|
pool.join()
|
||||||
|
|
||||||
def _http_requests_urllib3(self, url, session):
|
def _http_requests_single(self, url):
|
||||||
"""Request the data from the url"""
|
"""Request the data from the url"""
|
||||||
|
error_msg = "Failed to access uri endpoint %s. " % self.uri
|
||||||
try:
|
try:
|
||||||
r = session.request(self.request_type, url, headers=self.headers, fields=self.data,
|
r = self._session.request(self.request_type, url, headers=self.headers, data=self.data,
|
||||||
timeout=self.timeout, **self.request_kwargs)
|
timeout=self.timeout, verify=self.ssl_verify, **self.request_kwargs)
|
||||||
except IOError as e:
|
r.raise_for_status()
|
||||||
|
except requests.exceptions.Timeout as e:
|
||||||
if not self._silent:
|
if not self._silent:
|
||||||
logger.warn("Failed to access uri endpoint %s with error %s" % (self.uri, e))
|
logger.error(error_msg + "Request timed out: %s", e)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except requests.exceptions.SSLError as e:
|
||||||
if not self._silent:
|
if not self._silent:
|
||||||
logger.warn("Failed to access uri endpoint %s. Is your server maybe accepting SSL connections only? %s" % (self.uri, e))
|
logger.error(error_msg + "Is your server maybe accepting SSL connections only? %s", e)
|
||||||
return None
|
return None
|
||||||
except:
|
except requests.exceptions.HTTPError as e:
|
||||||
if not self._silent:
|
if not self._silent:
|
||||||
logger.warn("Failed to access uri endpoint %s with Uncaught exception." % self.uri)
|
logger.error(error_msg + "Status code %s", e)
|
||||||
|
return None
|
||||||
|
except requests.exceptions.ConnectionError as e:
|
||||||
|
if not self._silent:
|
||||||
|
logger.error(error_msg + "Connection error: %s", e)
|
||||||
|
return None
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
if not self._silent:
|
||||||
|
logger.error(error_msg + "Uncaught exception: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
response_status = r.status
|
response_status = r.status_code
|
||||||
response_content = r.data
|
response_content = r.content
|
||||||
response_headers = r.headers
|
response_headers = r.headers
|
||||||
|
|
||||||
if response_status in (200, 201):
|
if response_status in (200, 201):
|
||||||
return self._http_format_output(response_content, response_headers)
|
return self._http_format_output(response_content, response_headers)
|
||||||
else:
|
|
||||||
if not self._silent:
|
|
||||||
logger.warn("Failed to access uri endpoint %s. Status code %r" % (self.uri, response_status))
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _http_format_output(self, response_content, response_headers):
|
def _http_format_output(self, response_content, response_headers):
|
||||||
"""Formats the request response to the desired type"""
|
"""Formats the request response to the desired type"""
|
||||||
|
@@ -103,11 +103,21 @@ class BlacklistFilter(logging.Filter):
|
|||||||
try:
|
try:
|
||||||
if item in record.msg:
|
if item in record.msg:
|
||||||
record.msg = record.msg.replace(item, 16 * '*')
|
record.msg = record.msg.replace(item, 16 * '*')
|
||||||
if any(item in str(arg) for arg in record.args):
|
|
||||||
record.args = tuple(arg.replace(item, 16 * '*') if isinstance(arg, str) else arg
|
args = []
|
||||||
for arg in record.args)
|
for arg in record.args:
|
||||||
|
try:
|
||||||
|
arg_str = str(arg)
|
||||||
|
if item in arg_str:
|
||||||
|
arg_str = arg_str.replace(item, 16 * '*')
|
||||||
|
arg = arg_str
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
args.append(arg)
|
||||||
|
record.args = tuple(args)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -131,9 +141,15 @@ class RegexFilter(logging.Filter):
|
|||||||
|
|
||||||
args = []
|
args = []
|
||||||
for arg in record.args:
|
for arg in record.args:
|
||||||
matches = self.regex.findall(arg) if isinstance(arg, str) else []
|
try:
|
||||||
for match in matches:
|
arg_str = str(arg)
|
||||||
arg = self.replace(arg, match)
|
matches = self.regex.findall(arg_str)
|
||||||
|
if matches:
|
||||||
|
for match in matches:
|
||||||
|
arg_str = self.replace(arg_str, match)
|
||||||
|
arg = arg_str
|
||||||
|
except:
|
||||||
|
pass
|
||||||
args.append(arg)
|
args.append(arg)
|
||||||
record.args = tuple(args)
|
record.args = tuple(args)
|
||||||
except:
|
except:
|
||||||
|
@@ -66,7 +66,8 @@ class MacOSSystemTray(object):
|
|||||||
self.menu[2].state = plexpy.CONFIG.LAUNCH_STARTUP
|
self.menu[2].state = plexpy.CONFIG.LAUNCH_STARTUP
|
||||||
self.menu[3].state = plexpy.CONFIG.LAUNCH_BROWSER
|
self.menu[3].state = plexpy.CONFIG.LAUNCH_BROWSER
|
||||||
|
|
||||||
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, template=True,
|
||||||
|
menu=self.menu, quit_button=None)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
logger.info("Launching MacOS menu bar icon.")
|
logger.info("Launching MacOS menu bar icon.")
|
||||||
@@ -161,10 +162,11 @@ def set_startup():
|
|||||||
plist_file_path = os.path.join(launch_agents, plist_file)
|
plist_file_path = os.path.join(launch_agents, plist_file)
|
||||||
|
|
||||||
exe = sys.executable
|
exe = sys.executable
|
||||||
|
run_args = [arg for arg in plexpy.ARGS if arg != '--nolaunch']
|
||||||
if plexpy.FROZEN:
|
if plexpy.FROZEN:
|
||||||
args = [exe]
|
args = [exe] + run_args
|
||||||
else:
|
else:
|
||||||
args = [exe, plexpy.FULL_PATH]
|
args = [exe, plexpy.FULL_PATH] + run_args
|
||||||
|
|
||||||
plist_dict = {
|
plist_dict = {
|
||||||
'Label': common.PRODUCT,
|
'Label': common.PRODUCT,
|
||||||
|
@@ -82,7 +82,8 @@ def schedule_newsletter_job(newsletter_job_id, name='', func=None, remove_job=Fa
|
|||||||
logger.info("Tautulli NewsletterHandler :: Re-scheduled newsletter: %s" % name)
|
logger.info("Tautulli NewsletterHandler :: Re-scheduled newsletter: %s" % name)
|
||||||
elif not remove_job:
|
elif not remove_job:
|
||||||
NEWSLETTER_SCHED.add_job(
|
NEWSLETTER_SCHED.add_job(
|
||||||
func, args=args, id=newsletter_job_id, trigger=CronTrigger.from_crontab(cron))
|
func, args=args, id=newsletter_job_id, trigger=CronTrigger.from_crontab(cron),
|
||||||
|
misfire_grace_time=None)
|
||||||
logger.info("Tautulli NewsletterHandler :: Scheduled newsletter: %s" % name)
|
logger.info("Tautulli NewsletterHandler :: Scheduled newsletter: %s" % name)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -854,8 +854,8 @@ class Notifier(object):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
verify_msg = ""
|
verify_msg = ""
|
||||||
if response is not None and response.status_code >= 400 and response.status_code < 500:
|
if response is not None and 400 <= response.status_code < 500:
|
||||||
verify_msg = " Verify you notification agent settings are correct."
|
verify_msg = " Verify your notification agent settings are correct."
|
||||||
|
|
||||||
logger.error("Tautulli Notifiers :: {name} notification failed.{msg}".format(msg=verify_msg, name=self.NAME))
|
logger.error("Tautulli Notifiers :: {name} notification failed.{msg}".format(msg=verify_msg, name=self.NAME))
|
||||||
|
|
||||||
|
@@ -213,6 +213,7 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
|
|||||||
|
|
||||||
keys_to_mask = {'user_id': '',
|
keys_to_mask = {'user_id': '',
|
||||||
'user': 'Plex User',
|
'user': 'Plex User',
|
||||||
|
'username': 'Plex User',
|
||||||
'friendly_name': 'Plex User',
|
'friendly_name': 'Plex User',
|
||||||
'user_thumb': common.DEFAULT_USER_THUMB,
|
'user_thumb': common.DEFAULT_USER_THUMB,
|
||||||
'ip_address': 'N/A',
|
'ip_address': 'N/A',
|
||||||
|
@@ -18,4 +18,4 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
PLEXPY_BRANCH = "master"
|
PLEXPY_BRANCH = "master"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.6.2"
|
PLEXPY_RELEASE_VERSION = "v2.6.5"
|
||||||
|
@@ -133,7 +133,13 @@ def get_version():
|
|||||||
return cur_commit_hash, remote_name, branch_name
|
return cur_commit_hash, remote_name, branch_name
|
||||||
|
|
||||||
else:
|
else:
|
||||||
plexpy.INSTALL_TYPE = 'docker' if plexpy.DOCKER else 'source'
|
if plexpy.DOCKER:
|
||||||
|
plexpy.INSTALL_TYPE = 'docker'
|
||||||
|
elif plexpy.SNAP:
|
||||||
|
plexpy.INSTALL_TYPE = 'snap'
|
||||||
|
else:
|
||||||
|
plexpy.INSTALL_TYPE = 'source'
|
||||||
|
|
||||||
current_version, current_branch = get_version_from_file()
|
current_version, current_branch = get_version_from_file()
|
||||||
return current_version, 'origin', current_branch
|
return current_version, 'origin', current_branch
|
||||||
|
|
||||||
@@ -162,10 +168,13 @@ def check_update(scheduler=False, notify=False, use_cache=False):
|
|||||||
|
|
||||||
if not plexpy.CURRENT_VERSION:
|
if not plexpy.CURRENT_VERSION:
|
||||||
plexpy.UPDATE_AVAILABLE = None
|
plexpy.UPDATE_AVAILABLE = None
|
||||||
elif plexpy.COMMITS_BEHIND > 0 and (plexpy.common.BRANCH in ('master', 'beta') or plexpy.FROZEN) and \
|
elif plexpy.COMMITS_BEHIND > 0 and \
|
||||||
|
(plexpy.common.BRANCH in ('master', 'beta') or plexpy.SNAP or plexpy.FROZEN) and \
|
||||||
plexpy.common.RELEASE != plexpy.LATEST_RELEASE:
|
plexpy.common.RELEASE != plexpy.LATEST_RELEASE:
|
||||||
plexpy.UPDATE_AVAILABLE = 'release'
|
plexpy.UPDATE_AVAILABLE = 'release'
|
||||||
elif plexpy.COMMITS_BEHIND > 0 and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and not plexpy.FROZEN:
|
elif plexpy.COMMITS_BEHIND > 0 and \
|
||||||
|
not plexpy.SNAP and not plexpy.FROZEN and \
|
||||||
|
plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION:
|
||||||
plexpy.UPDATE_AVAILABLE = 'commit'
|
plexpy.UPDATE_AVAILABLE = 'commit'
|
||||||
else:
|
else:
|
||||||
plexpy.UPDATE_AVAILABLE = False
|
plexpy.UPDATE_AVAILABLE = False
|
||||||
@@ -265,7 +274,12 @@ def check_github(scheduler=False, notify=False, use_cache=False):
|
|||||||
'plexpy_update_commit': plexpy.LATEST_VERSION,
|
'plexpy_update_commit': plexpy.LATEST_VERSION,
|
||||||
'plexpy_update_behind': plexpy.COMMITS_BEHIND})
|
'plexpy_update_behind': plexpy.COMMITS_BEHIND})
|
||||||
|
|
||||||
if scheduler and plexpy.CONFIG.PLEXPY_AUTO_UPDATE and not plexpy.DOCKER and not plexpy.FROZEN:
|
if plexpy.PYTHON2:
|
||||||
|
logger.warn('Tautulli is running using Python 2. Unable to run automatic update.')
|
||||||
|
|
||||||
|
elif scheduler and plexpy.CONFIG.PLEXPY_AUTO_UPDATE and \
|
||||||
|
not plexpy.DOCKER and not plexpy.SNAP and \
|
||||||
|
not (plexpy.FROZEN and common.PLATFORM == 'Darwin'):
|
||||||
logger.info('Running automatic update.')
|
logger.info('Running automatic update.')
|
||||||
plexpy.shutdown(restart=True, update=True)
|
plexpy.shutdown(restart=True, update=True)
|
||||||
|
|
||||||
@@ -276,12 +290,22 @@ def check_github(scheduler=False, notify=False, use_cache=False):
|
|||||||
|
|
||||||
|
|
||||||
def update():
|
def update():
|
||||||
|
if plexpy.PYTHON2:
|
||||||
|
logger.warn('Tautulli is running using Python 2. Unable to update.')
|
||||||
|
return
|
||||||
|
|
||||||
if not plexpy.UPDATE_AVAILABLE:
|
if not plexpy.UPDATE_AVAILABLE:
|
||||||
return
|
return
|
||||||
|
|
||||||
if plexpy.INSTALL_TYPE in ('docker', 'windows', 'macos'):
|
if plexpy.INSTALL_TYPE in ('docker', 'snap', 'macos'):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
elif plexpy.INSTALL_TYPE == 'windows':
|
||||||
|
logger.info('Calling Windows scheduled task to update Tautulli')
|
||||||
|
CREATE_NO_WINDOW = 0x08000000
|
||||||
|
subprocess.Popen(['SCHTASKS', '/Run', '/TN', 'TautulliUpdateTask'],
|
||||||
|
creationflags=CREATE_NO_WINDOW)
|
||||||
|
|
||||||
elif plexpy.INSTALL_TYPE == 'git':
|
elif plexpy.INSTALL_TYPE == 'git':
|
||||||
output, err = runGit('pull --ff-only {} {}'.format(plexpy.CONFIG.GIT_REMOTE,
|
output, err = runGit('pull --ff-only {} {}'.format(plexpy.CONFIG.GIT_REMOTE,
|
||||||
plexpy.CONFIG.GIT_BRANCH))
|
plexpy.CONFIG.GIT_BRANCH))
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
# http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions
|
# https://github.com/cherrypy/tools/blob/master/AuthenticationAndAccessRestrictions
|
||||||
# Form based authentication for CherryPy. Requires the
|
# Form based authentication for CherryPy. Requires the
|
||||||
# Session tool to be loaded.
|
# Session tool to be loaded.
|
||||||
|
|
||||||
|
@@ -4309,7 +4309,7 @@ class WebInterface(object):
|
|||||||
plexpy.CONFIG.GIT_REPO,
|
plexpy.CONFIG.GIT_REPO,
|
||||||
plexpy.CURRENT_VERSION,
|
plexpy.CURRENT_VERSION,
|
||||||
plexpy.LATEST_VERSION))
|
plexpy.LATEST_VERSION))
|
||||||
}
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
update = {'result': 'success',
|
update = {'result': 'success',
|
||||||
@@ -4317,7 +4317,7 @@ class WebInterface(object):
|
|||||||
'message': 'Tautulli is up to date.'
|
'message': 'Tautulli is up to date.'
|
||||||
}
|
}
|
||||||
|
|
||||||
if plexpy.DOCKER or plexpy.FROZEN:
|
if plexpy.DOCKER or plexpy.SNAP or plexpy.FROZEN:
|
||||||
update['install_type'] = plexpy.INSTALL_TYPE
|
update['install_type'] = plexpy.INSTALL_TYPE
|
||||||
|
|
||||||
return update
|
return update
|
||||||
@@ -4351,7 +4351,9 @@ class WebInterface(object):
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def update(self, **kwargs):
|
def update(self, **kwargs):
|
||||||
if plexpy.DOCKER:
|
if plexpy.PYTHON2:
|
||||||
|
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home?update=python2")
|
||||||
|
if plexpy.DOCKER or plexpy.SNAP:
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
|
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
|
||||||
|
|
||||||
# Show changelog after updating
|
# Show changelog after updating
|
||||||
|
@@ -148,27 +148,18 @@ def set_startup():
|
|||||||
startup_reg_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
|
startup_reg_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
|
||||||
|
|
||||||
exe = sys.executable
|
exe = sys.executable
|
||||||
|
run_args = [arg for arg in plexpy.ARGS if arg != '--nolaunch']
|
||||||
if plexpy.FROZEN:
|
if plexpy.FROZEN:
|
||||||
args = [exe]
|
args = [exe] + run_args
|
||||||
else:
|
else:
|
||||||
args = [exe, plexpy.FULL_PATH]
|
args = [exe, plexpy.FULL_PATH] + run_args
|
||||||
|
|
||||||
|
registry_key_name = '{}_{}'.format(common.PRODUCT, plexpy.CONFIG.PMS_UUID)
|
||||||
|
|
||||||
cmd = ' '.join(cmd_quote(arg) for arg in args).replace('python.exe', 'pythonw.exe').replace("'", '"')
|
cmd = ' '.join(cmd_quote(arg) for arg in args).replace('python.exe', 'pythonw.exe').replace("'", '"')
|
||||||
|
|
||||||
if plexpy.CONFIG.LAUNCH_STARTUP:
|
if plexpy.CONFIG.LAUNCH_STARTUP:
|
||||||
try:
|
# Rename old Tautulli registry key
|
||||||
winreg.CreateKey(winreg.HKEY_CURRENT_USER, startup_reg_path)
|
|
||||||
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_WRITE)
|
|
||||||
winreg.SetValueEx(registry_key, common.PRODUCT, 0, winreg.REG_SZ, cmd)
|
|
||||||
winreg.CloseKey(registry_key)
|
|
||||||
logger.info("Added Tautulli to Windows system startup registry key.")
|
|
||||||
return True
|
|
||||||
except WindowsError as e:
|
|
||||||
logger.error("Failed to create Windows system startup registry key: %s", e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Check if registry value exists
|
|
||||||
try:
|
try:
|
||||||
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_ALL_ACCESS)
|
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_ALL_ACCESS)
|
||||||
winreg.QueryValueEx(registry_key, common.PRODUCT)
|
winreg.QueryValueEx(registry_key, common.PRODUCT)
|
||||||
@@ -180,6 +171,33 @@ def set_startup():
|
|||||||
try:
|
try:
|
||||||
winreg.DeleteValue(registry_key, common.PRODUCT)
|
winreg.DeleteValue(registry_key, common.PRODUCT)
|
||||||
winreg.CloseKey(registry_key)
|
winreg.CloseKey(registry_key)
|
||||||
|
except WindowsError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
winreg.CreateKey(winreg.HKEY_CURRENT_USER, startup_reg_path)
|
||||||
|
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_WRITE)
|
||||||
|
winreg.SetValueEx(registry_key, registry_key_name, 0, winreg.REG_SZ, cmd)
|
||||||
|
winreg.CloseKey(registry_key)
|
||||||
|
logger.info("Added Tautulli to Windows system startup registry key.")
|
||||||
|
return True
|
||||||
|
except WindowsError as e:
|
||||||
|
logger.error("Failed to create Windows system startup registry key: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Check if registry value exists
|
||||||
|
try:
|
||||||
|
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_ALL_ACCESS)
|
||||||
|
winreg.QueryValueEx(registry_key, registry_key_name)
|
||||||
|
reg_value_exists = True
|
||||||
|
except WindowsError:
|
||||||
|
reg_value_exists = False
|
||||||
|
|
||||||
|
if reg_value_exists:
|
||||||
|
try:
|
||||||
|
winreg.DeleteValue(registry_key, registry_key_name)
|
||||||
|
winreg.CloseKey(registry_key)
|
||||||
logger.info("Removed Tautulli from Windows system startup registry key.")
|
logger.info("Removed Tautulli from Windows system startup registry key.")
|
||||||
return True
|
return True
|
||||||
except WindowsError as e:
|
except WindowsError as e:
|
||||||
|
66
snap/snapcraft.yaml
Normal file
66
snap/snapcraft.yaml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: tautulli
|
||||||
|
adopt-info: tautulli
|
||||||
|
summary: A Python based monitoring and tracking tool for Plex Media Server.
|
||||||
|
description: >
|
||||||
|
Tautulli is a 3rd party application that you can run alongside your Plex Media Server to monitor activity and track various statistics.
|
||||||
|
Most importantly, these statistics include what has been watched, who watched it, when and where they watched it, and how it was watched.
|
||||||
|
The only thing missing is "why they watched it", but who am I to question your 42 plays of Frozen.
|
||||||
|
All statistics are presented in a nice and clean interface with many tables and graphs, which makes it easy to brag about your server to everyone else.
|
||||||
|
|
||||||
|
base: core18
|
||||||
|
confinement: strict
|
||||||
|
|
||||||
|
parts:
|
||||||
|
tautulli:
|
||||||
|
plugin: dump
|
||||||
|
source: .
|
||||||
|
stage-packages:
|
||||||
|
- python3
|
||||||
|
- python3-openssl
|
||||||
|
- python3-pycryptodome
|
||||||
|
- python3-setuptools
|
||||||
|
build-packages:
|
||||||
|
- git
|
||||||
|
override-pull: |
|
||||||
|
snapcraftctl pull
|
||||||
|
TAG_FULL=$(git describe --tag)
|
||||||
|
TAG=$(echo $TAG_FULL | grep -oP '(v\d+\.\d+\.\d+(?>-beta)?)')
|
||||||
|
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
COMMIT=$(git rev-parse HEAD)
|
||||||
|
if [ "$TAG" = "$TAG_FULL" ]; then
|
||||||
|
VERSION=$TAG
|
||||||
|
else
|
||||||
|
VERSION=$(echo $COMMIT | head -c 7)
|
||||||
|
fi
|
||||||
|
if [ ! "$VERSION" = "$TAG" ] || [ ! "$VERSION" = "${VERSION%-beta}" ]; then
|
||||||
|
GRADE=devel
|
||||||
|
else
|
||||||
|
GRADE=stable
|
||||||
|
fi
|
||||||
|
if [ "$VERSION" = "$TAG" ] && [ ! "$VERSION" = "${VERSION%-beta}" ]; then
|
||||||
|
BRANCH=beta
|
||||||
|
elif [ "$VERSION" = "$TAG" ]; then
|
||||||
|
BRANCH=master
|
||||||
|
fi
|
||||||
|
echo $BRANCH > branch.txt
|
||||||
|
echo $COMMIT > version.txt
|
||||||
|
snapcraftctl set-version "$VERSION"
|
||||||
|
snapcraftctl set-grade "$GRADE"
|
||||||
|
|
||||||
|
apps:
|
||||||
|
tautulli:
|
||||||
|
command: >
|
||||||
|
usr/bin/python3 $SNAP/Tautulli.py
|
||||||
|
--datadir $SNAP_USER_COMMON/Tautulli
|
||||||
|
--config $SNAP_USER_COMMON/Tautulli/config.ini
|
||||||
|
--quiet
|
||||||
|
--nolaunch
|
||||||
|
daemon: simple
|
||||||
|
restart-condition: on-abnormal
|
||||||
|
restart-delay: 5s
|
||||||
|
plugs:
|
||||||
|
- home
|
||||||
|
- network
|
||||||
|
- network-bind
|
||||||
|
environment:
|
||||||
|
TAUTULLI_SNAP: "True"
|
Reference in New Issue
Block a user