plexpy/plexpy/versioncheck.py
2024-09-23 16:52:17 -07:00

557 lines
22 KiB
Python

# -*- 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/>.
import json
import os
import platform
import re
import shutil
import subprocess
import tarfile
import plexpy
from plexpy import common
from plexpy import helpers
from plexpy import logger
from plexpy import request
def runGit(args):
if plexpy.CONFIG.GIT_PATH:
git_locations = ['"' + plexpy.CONFIG.GIT_PATH + '"']
else:
git_locations = ['git']
if platform.system().lower() == 'darwin':
git_locations.append('/usr/local/git/bin/git')
output = err = None
for cur_git in git_locations:
cmd = cur_git + ' ' + args
try:
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + plexpy.PROG_DIR)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=plexpy.PROG_DIR)
output, err = p.communicate()
output = output.strip().decode()
logger.debug('Git output: ' + output)
except OSError:
logger.debug('Command failed: %s', cmd)
continue
if 'not found' in output or "not recognized as an internal or external command" in output:
logger.debug('Unable to find git with command ' + cmd)
output = None
elif 'fatal:' in output or err:
logger.error('Git returned bad info. Are you sure this is a git installation?')
output = None
elif output:
break
return output, err
def get_version():
if plexpy.FROZEN and common.PLATFORM == 'Windows':
plexpy.INSTALL_TYPE = 'windows'
current_version, current_branch = get_version_from_file()
return current_version, 'origin', current_branch
elif plexpy.FROZEN and common.PLATFORM == 'Darwin':
plexpy.INSTALL_TYPE = 'macos'
current_version, current_branch = get_version_from_file()
return current_version, 'origin', current_branch
elif os.path.isdir(os.path.join(plexpy.PROG_DIR, '.git')):
plexpy.INSTALL_TYPE = 'git'
output, err = runGit('rev-parse HEAD')
if not output:
logger.error('Could not find latest installed version.')
cur_commit_hash = None
else:
cur_commit_hash = str(output)
if not re.match('^[a-z0-9]+$', cur_commit_hash):
logger.error('Output does not look like a hash, not using it.')
cur_commit_hash = None
if plexpy.CONFIG.DO_NOT_OVERRIDE_GIT_BRANCH and plexpy.CONFIG.GIT_BRANCH:
remote_name = None
branch_name = plexpy.CONFIG.GIT_BRANCH
else:
remote_branch, err = runGit('rev-parse --abbrev-ref --symbolic-full-name @{u}')
remote_branch = remote_branch.rsplit('/', 1) if remote_branch else []
if len(remote_branch) == 2:
remote_name, branch_name = remote_branch
else:
remote_name = branch_name = None
if not remote_name and plexpy.CONFIG.GIT_REMOTE:
logger.error('Could not retrieve remote name from git. Falling back to %s.' % plexpy.CONFIG.GIT_REMOTE)
remote_name = plexpy.CONFIG.GIT_REMOTE
if not remote_name:
logger.error('Could not retrieve remote name from git. Defaulting to origin.')
branch_name = 'origin'
if not branch_name and plexpy.CONFIG.GIT_BRANCH:
logger.error('Could not retrieve branch name from git. Falling back to %s.' % plexpy.CONFIG.GIT_BRANCH)
branch_name = plexpy.CONFIG.GIT_BRANCH
if not branch_name:
logger.error('Could not retrieve branch name from git. Defaulting to master.')
branch_name = 'master'
return cur_commit_hash, remote_name, branch_name
else:
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()
return current_version, 'origin', current_branch
def get_version_from_file():
version_file = os.path.join(plexpy.PROG_DIR, 'version.txt')
branch_file = os.path.join(plexpy.PROG_DIR, 'branch.txt')
if os.path.isfile(version_file):
with open(version_file, 'r') as f:
current_version = f.read().strip(' \n\r')
else:
current_version = None
if os.path.isfile(branch_file):
with open(branch_file, 'r') as f:
current_branch = f.read().strip(' \n\r')
else:
current_branch = common.BRANCH
return current_version, current_branch
def check_update(scheduler=False, notify=False, use_cache=False):
check_github(scheduler=scheduler, notify=notify, use_cache=use_cache)
if not plexpy.CURRENT_VERSION:
plexpy.UPDATE_AVAILABLE = None
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.UPDATE_AVAILABLE = 'release'
elif plexpy.COMMITS_BEHIND > 0 and \
not plexpy.SNAP and not plexpy.FROZEN and \
plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION:
plexpy.UPDATE_AVAILABLE = 'commit'
else:
plexpy.UPDATE_AVAILABLE = False
if plexpy.WIN_SYS_TRAY_ICON:
plexpy.WIN_SYS_TRAY_ICON.change_tray_update_icon()
elif plexpy.MAC_SYS_TRAY_ICON:
plexpy.MAC_SYS_TRAY_ICON.change_tray_update_icon()
def check_github(scheduler=False, notify=False, use_cache=False):
plexpy.COMMITS_BEHIND = 0
if plexpy.CONFIG.GIT_TOKEN:
headers = {'Authorization': 'token {}'.format(plexpy.CONFIG.GIT_TOKEN)}
else:
headers = {}
version = github_cache('version', use_cache=use_cache)
if not version:
# Get the latest version available from github
logger.info('Retrieving latest version information from GitHub')
url = 'https://api.github.com/repos/%s/%s/commits/%s' % (plexpy.CONFIG.GIT_USER,
plexpy.CONFIG.GIT_REPO,
plexpy.CONFIG.GIT_BRANCH)
version = request.request_json(url, headers=headers, timeout=20,
validator=lambda x: type(x) == dict)
github_cache('version', github_data=version)
if version is None:
logger.warn('Could not get the latest version from GitHub. Are you running a local development version?')
return plexpy.CURRENT_VERSION
plexpy.LATEST_VERSION = version['sha']
logger.debug("Latest version is %s", plexpy.LATEST_VERSION)
# See how many commits behind we are
if not plexpy.CURRENT_VERSION:
logger.info('You are running an unknown version of Tautulli. Run the updater to identify your version')
return plexpy.LATEST_VERSION
if plexpy.LATEST_VERSION == plexpy.CURRENT_VERSION:
logger.info('Tautulli is up to date')
return plexpy.LATEST_VERSION
commits = github_cache('commits', use_cache=use_cache)
if not commits:
logger.info('Comparing currently installed version with latest GitHub version')
# Need to compare CURRENT << LATEST to get a list of commits
url = 'https://api.github.com/repos/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER,
plexpy.CONFIG.GIT_REPO,
plexpy.CURRENT_VERSION,
plexpy.LATEST_VERSION)
commits = request.request_json(url, headers=headers, timeout=20, whitelist_status_code=404,
validator=lambda x: type(x) == dict)
github_cache('commits', github_data=commits)
if commits is None:
logger.warn('Could not get commits behind from GitHub.')
return plexpy.LATEST_VERSION
try:
ahead_by = int(commits['ahead_by'])
logger.debug("In total, %d commits behind", ahead_by)
# Do not count [skip ci] commits for Docker or Snap on the nightly branch
if (plexpy.DOCKER or plexpy.SNAP) and plexpy.CONFIG.GIT_BRANCH == 'nightly':
for commit in reversed(commits['commits']):
if '[skip ci]' not in commit['commit']['message']:
plexpy.LATEST_VERSION = commit['sha']
break
ahead_by -= 1
install = 'Docker container' if plexpy.DOCKER else 'Snap package'
logger.debug("%s %d commits behind", install, ahead_by)
plexpy.COMMITS_BEHIND = ahead_by
except KeyError:
logger.info('Cannot compare versions. Are you running a local development version?')
plexpy.COMMITS_BEHIND = 0
if plexpy.COMMITS_BEHIND > 0:
logger.info('New version is available. You are %s commits behind' % plexpy.COMMITS_BEHIND)
releases = github_cache('releases', use_cache=use_cache)
if not releases:
url = 'https://api.github.com/repos/%s/%s/releases' % (plexpy.CONFIG.GIT_USER,
plexpy.CONFIG.GIT_REPO)
releases = request.request_json(url, timeout=20, whitelist_status_code=404,
validator=lambda x: type(x) == list)
github_cache('releases', github_data=releases)
if releases is None:
logger.warn('Could not get releases from GitHub.')
return plexpy.LATEST_VERSION
if plexpy.CONFIG.GIT_BRANCH == 'master':
release = next((r for r in releases if not r['prerelease']), releases[0])
elif plexpy.CONFIG.GIT_BRANCH == 'beta':
release = next((r for r in releases if not r['tag_name'].endswith('-nightly')), releases[0])
elif plexpy.CONFIG.GIT_BRANCH == 'nightly':
release = next((r for r in releases), releases[0])
else:
release = releases[0]
plexpy.LATEST_RELEASE = release['tag_name']
if plexpy.CONFIG.GIT_BRANCH in ('master', 'beta') and release['target_commitish'] == plexpy.CURRENT_VERSION:
logger.info('Tautulli is up to date')
return plexpy.CURRENT_VERSION
if notify:
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate',
'plexpy_download_info': release,
'plexpy_update_commit': plexpy.LATEST_VERSION,
'plexpy_update_behind': plexpy.COMMITS_BEHIND})
if scheduler and plexpy.CONFIG.PLEXPY_AUTO_UPDATE and \
not plexpy.DOCKER and not plexpy.SNAP and not plexpy.FROZEN:
logger.info('Running automatic update.')
plexpy.shutdown(restart=True, update=True)
elif plexpy.COMMITS_BEHIND == 0:
logger.info('Tautulli is up to date')
return plexpy.LATEST_VERSION
def update():
if not plexpy.UPDATE_AVAILABLE:
return
if plexpy.INSTALL_TYPE in ('docker', 'snap', 'windows', 'macos'):
return
elif plexpy.INSTALL_TYPE == 'git':
output, err = runGit('pull --ff-only {} {}'.format(plexpy.CONFIG.GIT_REMOTE,
plexpy.CONFIG.GIT_BRANCH))
if not output:
logger.error('Unable to download latest version')
return
for line in output.split('\n'):
if 'Already up-to-date.' in line or 'Already up to date.' in line:
logger.info('No update available, not updating')
return
elif line.endswith(('Aborting', 'Aborting.')):
logger.error('Unable to update from git: ' + line)
return
clean_pyc()
elif plexpy.INSTALL_TYPE == 'source':
tar_download_url = 'https://github.com/{}/{}/tarball/{}'.format(plexpy.CONFIG.GIT_USER,
plexpy.CONFIG.GIT_REPO,
plexpy.CONFIG.GIT_BRANCH)
update_dir = os.path.join(plexpy.DATA_DIR, 'update')
version_path = os.path.join(plexpy.PROG_DIR, 'version.txt')
logger.info('Downloading update from: ' + tar_download_url)
data = request.request_content(tar_download_url)
if not data:
logger.error("Unable to retrieve new version from '%s', can't update", tar_download_url)
return
download_name = plexpy.CONFIG.GIT_BRANCH + '-github'
tar_download_path = os.path.join(plexpy.DATA_DIR, download_name)
# Save tar to disk
with open(tar_download_path, 'wb') as f:
f.write(data)
# Extract the tar to update folder
logger.info('Extracting file: ' + tar_download_path)
tar = tarfile.open(tar_download_path)
tar.extractall(update_dir)
tar.close()
# Delete the tar.gz
logger.info('Deleting file: ' + tar_download_path)
os.remove(tar_download_path)
# Find update dir name
update_dir_contents = [x for x in os.listdir(update_dir) if os.path.isdir(os.path.join(update_dir, x))]
if len(update_dir_contents) != 1:
logger.error("Invalid update data, update failed: " + str(update_dir_contents))
return
content_dir = os.path.join(update_dir, update_dir_contents[0])
# walk temp folder and move files to main folder
for dirname, dirnames, filenames in os.walk(content_dir):
dirname = dirname[len(content_dir) + 1:]
for curfile in filenames:
old_path = os.path.join(content_dir, dirname, curfile)
new_path = os.path.join(plexpy.PROG_DIR, dirname, curfile)
if os.path.isfile(new_path):
os.remove(new_path)
os.renames(old_path, new_path)
# Update version.txt
try:
with open(version_path, 'w') as f:
f.write(str(plexpy.LATEST_VERSION))
except IOError as e:
logger.error(
"Unable to write current version to version.txt, update not complete: %s",
e
)
return
clean_pyc()
def reset_git_install():
if plexpy.INSTALL_TYPE == 'git':
logger.info('Attempting to reset git install to "{}/{}/{}"'.format(plexpy.CONFIG.GIT_REMOTE,
plexpy.CONFIG.GIT_BRANCH,
common.RELEASE))
output, err = runGit('remote set-url {} https://github.com/{}/{}.git'.format(plexpy.CONFIG.GIT_REMOTE,
plexpy.CONFIG.GIT_USER,
plexpy.CONFIG.GIT_REPO))
output, err = runGit('fetch {}'.format(plexpy.CONFIG.GIT_REMOTE))
output, err = runGit('checkout {}'.format(plexpy.CONFIG.GIT_BRANCH))
output, err = runGit('branch -u {}/{}'.format(plexpy.CONFIG.GIT_REMOTE,
plexpy.CONFIG.GIT_BRANCH))
output, err = runGit('reset --hard {}'.format(common.RELEASE))
_, _ = runGit('clean -fd')
if not output:
logger.error('Unable to reset Tautulli installation.')
return False
for line in output.split('\n'):
if 'Already up-to-date.' in line or 'Already up to date.' in line:
logger.info('Tautulli installation reset successfully.')
return True
elif line.endswith(('Aborting', 'Aborting.')):
logger.error('Unable to reset Tautulli installation: ' + line)
return False
def checkout_git_branch():
if plexpy.INSTALL_TYPE == 'git':
logger.info('Attempting to checkout git branch "{}/{}"'.format(plexpy.CONFIG.GIT_REMOTE,
plexpy.CONFIG.GIT_BRANCH))
output, err = runGit('fetch {}'.format(plexpy.CONFIG.GIT_REMOTE))
output, err = runGit('checkout {}'.format(plexpy.CONFIG.GIT_BRANCH))
if not output:
logger.error('Unable to change git branch.')
return
for line in output.split('\n'):
if line.endswith(('Aborting', 'Aborting.')):
logger.error('Unable to checkout from git: ' + line)
return
output, err = runGit('pull {} {}'.format(plexpy.CONFIG.GIT_REMOTE,
plexpy.CONFIG.GIT_BRANCH))
def github_cache(cache, github_data=None, use_cache=True):
timestamp = helpers.timestamp()
cache_filepath = os.path.join(plexpy.CONFIG.CACHE_DIR, 'github_{}.json'.format(cache))
if github_data:
cache_data = {'github_data': github_data,
'_cache_time': timestamp,
'_release_version': common.RELEASE}
try:
with open(cache_filepath, 'w', encoding='utf-8') as cache_file:
json.dump(cache_data, cache_file)
except:
pass
else:
if not use_cache:
return
try:
with open(cache_filepath, 'r', encoding='utf-8') as cache_file:
cache_data = json.load(cache_file)
if (
timestamp - cache_data['_cache_time'] < plexpy.CONFIG.CHECK_GITHUB_CACHE_SECONDS and
cache_data['_release_version'] == common.RELEASE
):
logger.debug('Using cached GitHub %s data', cache)
return cache_data['github_data']
except:
pass
def read_changelog(latest_only=False, since_prev_release=False):
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
if not os.path.isfile(changelog_file):
return '<h4>Missing changelog file</h4>'
try:
output = ['']
prev_level = 0
latest_version_found = False
header_pattern = re.compile(r'(^#+)\s(.+)')
list_pattern = re.compile(r'(^[ \t]*\*\s)(.+)')
beta_release = False
prev_release = str(plexpy.PREV_RELEASE)
with open(changelog_file, "r") as logfile:
for line in logfile:
line_header_match = re.search(header_pattern, line)
line_list_match = re.search(list_pattern, line)
if line_header_match:
header_level = str(len(line_header_match.group(1)))
header_text = line_header_match.group(2)
if header_text.lower() == 'changelog':
continue
if latest_version_found:
break
elif latest_only:
latest_version_found = True
# Add a space to the end of the release to match tags
elif since_prev_release:
if prev_release.endswith('-beta') and not beta_release:
if prev_release + ' ' in header_text:
break
elif prev_release.replace('-beta', '') + ' ' in header_text:
beta_release = True
elif prev_release.endswith('-beta') and beta_release:
break
elif prev_release + ' ' in header_text:
break
output[-1] += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
elif line_list_match:
line_level = len(line_list_match.group(1)) // 2
line_text = line_list_match.group(2)
if line_level > prev_level:
output[-1] += '<ul>' * (line_level - prev_level) + '<li>' + line_text + '</li>'
elif line_level < prev_level:
output[-1] += '</ul>' * (prev_level - line_level) + '<li>' + line_text + '</li>'
else:
output[-1] += '<li>' + line_text + '</li>'
prev_level = line_level
elif line.strip() == '' and prev_level:
output[-1] += '</ul>' * (prev_level)
output.append('')
prev_level = 0
if since_prev_release:
output.reverse()
return ''.join(output)
except IOError as e:
logger.error('Tautulli Version Checker :: Unable to open changelog file. %s' % e)
return '<h4>Unable to open changelog file</h4>'
def clean_pyc():
logger.debug('Cleaning __pycache__ and .pyc files.')
for root, dirs, files in os.walk(plexpy.PROG_DIR):
for _dir in dirs:
if _dir.lower() == '__pycache__':
dirpath = os.path.join(root, _dir)
try:
shutil.rmtree(dirpath)
except OSError as e:
logger.error('Failed to remove directory %s: %s', dirpath, e)
for _file in files:
if _file.lower().endswith('.pyc'):
filepath = os.path.join(root, _file)
try:
os.remove(filepath)
except OSError as e:
logger.error('Failed to remove file %s: %s', filepath, e)