Compare commits
33 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 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,3 +1,3 @@
|
||||
github: JonnyWong16
|
||||
patreon: Tautulli
|
||||
custom: ["https://bit.ly/2InPp15"]
|
||||
custom: ["https://bit.ly/2InPp15", "https://bit.ly/2WTq83m"]
|
17
.github/workflows/publish-installers.yml
vendored
17
.github/workflows/publish-installers.yml
vendored
@@ -41,6 +41,13 @@ jobs:
|
||||
echo ::set-output name=VERSION::0.0.0
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
|
||||
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
|
||||
|
||||
- name: Set Up Python
|
||||
@@ -64,16 +71,20 @@ jobs:
|
||||
run: |
|
||||
pyinstaller -y ./package/Tautulli-${{ matrix.os }}.spec
|
||||
|
||||
- name: Move Windows Updater Files
|
||||
if: matrix.os == 'windows'
|
||||
run: |
|
||||
Move-Item dist\updater\* dist\Tautulli\ -Force
|
||||
|
||||
- name: Create Windows Installer
|
||||
uses: joncloud/makensis-action@v1.2
|
||||
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
|
||||
include-more-plugins: true
|
||||
include-custom-plugins-path: package/nsis-plugins
|
||||
additional-plugin-paths: package/nsis-plugins
|
||||
|
||||
- name: Create MacOS Installer
|
||||
if: matrix.os == 'macos'
|
||||
|
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
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
|
||||
# Compiled source #
|
||||
###################
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.py~
|
||||
*.pyproj
|
||||
@@ -64,7 +65,6 @@ Thumbs.db
|
||||
*.bak
|
||||
*.cache
|
||||
*.ilk
|
||||
*.log
|
||||
[Bb]in
|
||||
[Dd]ebug*/
|
||||
*.lib
|
||||
@@ -86,11 +86,7 @@ _ReSharper*/
|
||||
/parts/
|
||||
/stage/
|
||||
/prime/
|
||||
|
||||
*.snap
|
||||
|
||||
.snapcraft
|
||||
__pycache__
|
||||
*.pyc
|
||||
*_source.tar.bz2
|
||||
snap/.snapcraft
|
@@ -1,5 +1,14 @@
|
||||
# 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:
|
||||
|
10
Tautulli.py
10
Tautulli.py
@@ -31,6 +31,7 @@ import datetime
|
||||
import locale
|
||||
import pytz
|
||||
import signal
|
||||
import shutil
|
||||
import time
|
||||
import threading
|
||||
import tzlocal
|
||||
@@ -188,6 +189,15 @@ def main():
|
||||
else:
|
||||
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:
|
||||
config_file = args.config
|
||||
else:
|
||||
|
@@ -61,7 +61,7 @@
|
||||
Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% elif plexpy.INSTALL_TYPE == 'snap':
|
||||
Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% elif plexpy.INSTALL_TYPE in ('windows', 'macos'):
|
||||
% 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>
|
||||
% else:
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
@@ -237,6 +237,7 @@ ${next.modalIncludes()}
|
||||
<li class="active"><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li>
|
||||
<li><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
|
||||
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
|
||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab">Crypto</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="github-donation" style="text-align: center">
|
||||
@@ -263,6 +264,14 @@ ${next.modalIncludes()}
|
||||
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
|
||||
</a>
|
||||
</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 class="modal-footer">
|
||||
@@ -341,7 +350,7 @@ ${next.modalIncludes()}
|
||||
msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
} else if (result.install_type === 'snap') {
|
||||
msg += 'Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
} else if (result.install_type === 'windows' || result.install_type === 'macos') {
|
||||
} 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>'
|
||||
} else {
|
||||
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 {
|
||||
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;
|
||||
}
|
@@ -280,22 +280,32 @@
|
||||
|
||||
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;
|
||||
server_status = setInterval(function() {
|
||||
$.getJSON('server_status', function (data) {
|
||||
if (data.connected === true) {
|
||||
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>');
|
||||
$('#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();
|
||||
% 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();
|
||||
% endif
|
||||
} else if (data.connected === false) {
|
||||
clearInterval(server_status);
|
||||
% if 'current_activity' in config['home_sections']:
|
||||
$('#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>');
|
||||
% endif
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
% endif
|
||||
</script>
|
||||
% if 'current_activity' in config['home_sections']:
|
||||
<script>
|
||||
|
@@ -220,7 +220,7 @@
|
||||
<p class="help-block">Check for Tautulli updates periodically.</p>
|
||||
</div>
|
||||
<div id="git_update_options">
|
||||
% if not plexpy.SNAP and not plexpy.FROZEN:
|
||||
% if not plexpy.SNAP and not (plexpy.FROZEN and common.PLATFORM == 'Darwin'):
|
||||
<div class="checkbox">
|
||||
<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}
|
||||
@@ -3105,7 +3105,7 @@ $(document).ready(function() {
|
||||
if (news_item.subtitle) { content.append(subtitle); }
|
||||
content.append(body);
|
||||
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');
|
||||
content.css('display', 'block');
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
import sys
|
||||
sys.modules['FixTk'] = None
|
||||
|
||||
excludes = ['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter']
|
||||
block_cipher = None
|
||||
|
||||
analysis = Analysis(
|
||||
@@ -12,13 +13,27 @@ analysis = Analysis(
|
||||
('..\\data', 'data'),
|
||||
('..\\CHANGELOG.md', '.'),
|
||||
('..\\LICENSE', '.'),
|
||||
('..\\branch.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'],
|
||||
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(
|
||||
analysis.pure,
|
||||
analysis.zipped_data,
|
||||
@@ -39,3 +54,24 @@ coll = COLLECT(
|
||||
analysis.datas,
|
||||
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
|
||||
Name "${APP_NAME}"
|
||||
Caption "${APP_NAME}"
|
||||
@@ -39,7 +40,7 @@ OutFile "${INSTALLER_NAME}"
|
||||
BrandingText "${APP_NAME}"
|
||||
XPStyle on
|
||||
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
|
||||
|
||||
Var /GLOBAL norun
|
||||
Var /GLOBAL nolaunch
|
||||
|
||||
!include "MUI.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
!insertmacro GetParameters
|
||||
!insertmacro GetOptions
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
!define MUI_UNABORTWARNING
|
||||
@@ -99,6 +104,7 @@ Var /GLOBAL nolaunch
|
||||
!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder
|
||||
!endif
|
||||
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
|
||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\${MAIN_APP_EXE}"
|
||||
@@ -119,10 +125,14 @@ Section -MainProgram
|
||||
Call UninstallPrevious
|
||||
|
||||
${INSTALL_TYPE}
|
||||
SetOverwrite ifnewer
|
||||
SetOverwrite on
|
||||
SetOutPath "$INSTDIR"
|
||||
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
|
||||
ExecShell "" "$INSTDIR\${MAIN_APP_EXE}" $nolaunch
|
||||
SectionEnd
|
||||
@@ -208,11 +218,20 @@ RmDir "$SMPROGRAMS\${APP_NAME}"
|
||||
|
||||
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
||||
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
||||
|
||||
nsExec::Exec "$SYSDIR\SCHTASKS /Delete /TN TautulliUpdateTask /F"
|
||||
|
||||
SectionEnd
|
||||
|
||||
######################################################################
|
||||
|
||||
Function .onInit
|
||||
StrCpy $norun 0
|
||||
${GetParameters} $CMDLINE
|
||||
${GetOptions} "$CMDLINE" "/NORUN" $R0
|
||||
IfErrors +2 0
|
||||
StrCpy $norun 1
|
||||
|
||||
IfSilent 0 +2
|
||||
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
|
||||
pycryptodomex==3.9.9
|
||||
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
|
||||
pycryptodomex==3.9.9
|
||||
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)
|
@@ -99,6 +99,7 @@ PIDFILE = None
|
||||
NOFORK = False
|
||||
DOCKER = False
|
||||
SNAP = False
|
||||
SNAP_MIGRATE = False
|
||||
FROZEN = False
|
||||
|
||||
SCHED = None
|
||||
@@ -173,6 +174,18 @@ def initialize(config_file):
|
||||
if _INITIALIZED:
|
||||
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:
|
||||
logger.warn("HTTP_PORT out of bounds: 21 < %s < 65535", CONFIG.HTTP_PORT)
|
||||
CONFIG.HTTP_PORT = 8181
|
||||
@@ -501,12 +514,16 @@ def schedule_job(func, name, hours=0, minutes=0, seconds=0, args=None):
|
||||
SCHED.remove_job(name)
|
||||
logger.info("Removed background task: %s", name)
|
||||
elif job.trigger.interval != datetime.timedelta(hours=hours, minutes=minutes):
|
||||
SCHED.reschedule_job(name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC), args=args)
|
||||
SCHED.reschedule_job(
|
||||
name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC),
|
||||
args=args)
|
||||
logger.info("Re-scheduled background task: %s", name)
|
||||
elif hours > 0 or minutes > 0 or seconds > 0:
|
||||
SCHED.add_job(func, id=name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes, seconds=seconds, timezone=pytz.UTC), args=args)
|
||||
SCHED.add_job(
|
||||
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)
|
||||
|
||||
|
||||
@@ -2278,7 +2295,12 @@ def upgrade():
|
||||
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()
|
||||
|
||||
# Shutdown the websocket connection
|
||||
@@ -2351,14 +2373,15 @@ def shutdown(restart=False, update=False, checkout=False, reset=False):
|
||||
else:
|
||||
logger.info("Tautulli is shutting down...")
|
||||
|
||||
logger.shutdown()
|
||||
if _shutdown:
|
||||
logger.shutdown()
|
||||
|
||||
if WIN_SYS_TRAY_ICON:
|
||||
WIN_SYS_TRAY_ICON.shutdown()
|
||||
elif MAC_SYS_TRAY_ICON:
|
||||
MAC_SYS_TRAY_ICON.shutdown()
|
||||
if WIN_SYS_TRAY_ICON:
|
||||
WIN_SYS_TRAY_ICON.shutdown()
|
||||
elif MAC_SYS_TRAY_ICON:
|
||||
MAC_SYS_TRAY_ICON.shutdown()
|
||||
|
||||
os._exit(0)
|
||||
os._exit(0)
|
||||
|
||||
|
||||
def generate_uuid():
|
||||
|
@@ -606,7 +606,8 @@ def schedule_callback(id, func=None, remove_job=False, args=None, **kwargs):
|
||||
ACTIVITY_SCHED.add_job(
|
||||
func, args=args, id=id, trigger=DateTrigger(
|
||||
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):
|
||||
|
@@ -228,7 +228,7 @@ def delete_rows_from_table(table, row_ids):
|
||||
if row_ids:
|
||||
logger.info("Tautulli Database :: Deleting row ids %s from %s database table", row_ids, table)
|
||||
|
||||
# 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
|
||||
sqlite_max_variable_number = 999
|
||||
|
||||
|
@@ -103,11 +103,21 @@ class BlacklistFilter(logging.Filter):
|
||||
try:
|
||||
if item in record.msg:
|
||||
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
|
||||
for arg in record.args)
|
||||
|
||||
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:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -131,9 +141,15 @@ class RegexFilter(logging.Filter):
|
||||
|
||||
args = []
|
||||
for arg in record.args:
|
||||
matches = self.regex.findall(arg) if isinstance(arg, str) else []
|
||||
for match in matches:
|
||||
arg = self.replace(arg, match)
|
||||
try:
|
||||
arg_str = str(arg)
|
||||
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)
|
||||
record.args = tuple(args)
|
||||
except:
|
||||
|
@@ -162,10 +162,11 @@ def set_startup():
|
||||
plist_file_path = os.path.join(launch_agents, plist_file)
|
||||
|
||||
exe = sys.executable
|
||||
run_args = [arg for arg in plexpy.ARGS if arg != '--nolaunch']
|
||||
if plexpy.FROZEN:
|
||||
args = [exe]
|
||||
args = [exe] + run_args
|
||||
else:
|
||||
args = [exe, plexpy.FULL_PATH]
|
||||
args = [exe, plexpy.FULL_PATH] + run_args
|
||||
|
||||
plist_dict = {
|
||||
'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)
|
||||
elif not remove_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)
|
||||
|
||||
|
||||
|
@@ -18,4 +18,4 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.6.4"
|
||||
PLEXPY_RELEASE_VERSION = "v2.6.5"
|
||||
|
@@ -278,7 +278,8 @@ def check_github(scheduler=False, notify=False, use_cache=False):
|
||||
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:
|
||||
not plexpy.DOCKER and not plexpy.SNAP and \
|
||||
not (plexpy.FROZEN and common.PLATFORM == 'Darwin'):
|
||||
logger.info('Running automatic update.')
|
||||
plexpy.shutdown(restart=True, update=True)
|
||||
|
||||
@@ -296,9 +297,15 @@ def update():
|
||||
if not plexpy.UPDATE_AVAILABLE:
|
||||
return
|
||||
|
||||
if plexpy.INSTALL_TYPE in ('docker', 'snap', 'windows', 'macos'):
|
||||
if plexpy.INSTALL_TYPE in ('docker', 'snap', 'macos'):
|
||||
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':
|
||||
output, err = runGit('pull --ff-only {} {}'.format(plexpy.CONFIG.GIT_REMOTE,
|
||||
plexpy.CONFIG.GIT_BRANCH))
|
||||
|
@@ -148,27 +148,18 @@ def set_startup():
|
||||
startup_reg_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
|
||||
|
||||
exe = sys.executable
|
||||
run_args = [arg for arg in plexpy.ARGS if arg != '--nolaunch']
|
||||
if plexpy.FROZEN:
|
||||
args = [exe]
|
||||
args = [exe] + run_args
|
||||
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("'", '"')
|
||||
|
||||
if plexpy.CONFIG.LAUNCH_STARTUP:
|
||||
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, 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
|
||||
# Rename old Tautulli registry key
|
||||
try:
|
||||
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_ALL_ACCESS)
|
||||
winreg.QueryValueEx(registry_key, common.PRODUCT)
|
||||
@@ -180,6 +171,33 @@ def set_startup():
|
||||
try:
|
||||
winreg.DeleteValue(registry_key, common.PRODUCT)
|
||||
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.")
|
||||
return True
|
||||
except WindowsError as e:
|
||||
|
@@ -51,14 +51,15 @@ apps:
|
||||
tautulli:
|
||||
command: >
|
||||
usr/bin/python3 $SNAP/Tautulli.py
|
||||
--datadir $SNAP_COMMON/Tautulli
|
||||
--config $SNAP_COMMON/Tautulli/config.ini
|
||||
--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:
|
||||
|
Reference in New Issue
Block a user