bazarr/bazarr/app/check_update.py

277 lines
12 KiB
Python

# coding=utf-8
import os
import re
import logging
import json
import requests
import semver
import sys
from shutil import rmtree
from zipfile import ZipFile
from .get_args import args
from .config import settings
def deprecated_python_version():
# return True if Python version is deprecated
return sys.version_info.major == 2 or (sys.version_info.major == 3 and sys.version_info.minor < 8)
def check_releases():
releases = []
url_releases = 'https://api.github.com/repos/morpheus65535/Bazarr/releases?per_page=100'
try:
logging.debug(f'BAZARR getting releases from Github: {url_releases}')
r = requests.get(url_releases, allow_redirects=True)
r.raise_for_status()
except requests.exceptions.HTTPError:
logging.exception("Error trying to get releases from Github. Http error.")
except requests.exceptions.ConnectionError:
logging.exception("Error trying to get releases from Github. Connection Error.")
except requests.exceptions.Timeout:
logging.exception("Error trying to get releases from Github. Timeout Error.")
except requests.exceptions.RequestException:
logging.exception("Error trying to get releases from Github.")
else:
for release in r.json():
download_link = None
for asset in release['assets']:
if asset['name'] == 'bazarr.zip':
download_link = asset['browser_download_url']
if not download_link:
continue
releases.append({'name': release['name'],
'body': release['body'],
'date': release['published_at'],
'prerelease': release['prerelease'],
'download_link': download_link})
with open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'w') as f:
json.dump(releases, f)
logging.debug(f'BAZARR saved {len(r.json())} releases to releases.txt')
def check_if_new_update():
if settings.general.branch == 'master':
use_prerelease = False
elif settings.general.branch == 'development':
use_prerelease = True
else:
logging.error(f'BAZARR unknown branch provided to updater: {settings.general.branch}')
return
logging.debug(f'BAZARR updater is using {settings.general.branch} branch')
check_releases()
with open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'r') as f:
data = json.load(f)
if not args.no_update:
release = None
if use_prerelease:
if deprecated_python_version():
release = next((item['name'].lstrip('v') for item in data if
semver.VersionInfo.parse('1.3.1') > semver.VersionInfo.parse(item['name'].lstrip('v'))))
else:
release = next((item for item in data), None)
else:
if deprecated_python_version():
next((item['name'].lstrip('v') for item in data if
not item['prerelease'] and semver.VersionInfo.parse('1.3.1') > semver.VersionInfo.parse(
item['name'].lstrip('v'))))
else:
release = next((item for item in data if not item["prerelease"]), None)
if release and 'name' in release:
logging.debug(f'BAZARR last release available is {release["name"]}')
if deprecated_python_version():
logging.warning('BAZARR is using a deprecated Python version, you must update Python to get latest '
'version available.')
current_version = None
try:
current_version = semver.VersionInfo.parse(os.environ["BAZARR_VERSION"])
semver.VersionInfo.parse(release['name'].lstrip('v'))
except ValueError:
new_version = True
else:
new_version = True if semver.compare(release['name'].lstrip('v'), os.environ["BAZARR_VERSION"]) > 0 \
else False
# skip update process if latest release is v0.9.1.1 which is the latest pre-semver compatible release
if new_version and release['name'] != 'v0.9.1.1':
logging.debug(f'BAZARR newer release available and will be downloaded: {release["name"]}')
download_release(url=release['download_link'])
# rolling back from nightly to stable release
elif current_version:
if current_version.prerelease and not use_prerelease:
logging.debug(f'BAZARR previous stable version will be downloaded: {release["name"]}')
download_release(url=release['download_link'])
else:
logging.debug('BAZARR no newer release have been found')
else:
logging.debug('BAZARR no release found')
else:
logging.debug('BAZARR --no_update have been used as an argument')
def download_release(url):
r = None
update_dir = os.path.join(args.config_dir, 'update')
try:
os.makedirs(update_dir, exist_ok=True)
except Exception:
logging.debug(f'BAZARR unable to create update directory {update_dir}')
else:
logging.debug(f'BAZARR downloading release from Github: {url}')
r = requests.get(url, allow_redirects=True)
if r:
try:
with open(os.path.join(update_dir, 'bazarr.zip'), 'wb') as f:
f.write(r.content)
except Exception:
logging.exception('BAZARR unable to download new release and save it to disk')
else:
apply_update()
def apply_update():
is_updated = False
update_dir = os.path.join(args.config_dir, 'update')
bazarr_zip = os.path.join(update_dir, 'bazarr.zip')
bazarr_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
build_dir = os.path.join(bazarr_dir, 'frontend', 'build')
if os.path.isdir(update_dir):
if os.path.isfile(bazarr_zip):
logging.debug(f'BAZARR is trying to unzip this release to {bazarr_dir}: {bazarr_zip}')
try:
with ZipFile(bazarr_zip, 'r') as archive:
zip_root_directory = ''
if len({item.split('/')[0] for item in archive.namelist()}) == 1:
zip_root_directory = archive.namelist()[0]
if os.path.isdir(build_dir):
try:
rmtree(build_dir, ignore_errors=True)
except Exception:
logging.exception(
'BAZARR was unable to delete the previous build directory during upgrade process.')
for file in archive.namelist():
if file.startswith(zip_root_directory) and file != zip_root_directory and not \
file.endswith('bazarr.py'):
file_path = os.path.join(bazarr_dir, file[len(zip_root_directory):])
parent_dir = os.path.dirname(file_path)
os.makedirs(parent_dir, exist_ok=True)
if not os.path.isdir(file_path):
with open(file_path, 'wb+') as f:
f.write(archive.read(file))
except Exception:
logging.exception('BAZARR unable to unzip release')
else:
is_updated = True
try:
logging.debug('BAZARR successfully unzipped new release and will now try to delete the leftover '
'files.')
update_cleaner(zipfile=bazarr_zip, bazarr_dir=bazarr_dir, config_dir=args.config_dir)
except Exception:
logging.exception('BAZARR unable to cleanup leftover files after upgrade.')
else:
logging.debug('BAZARR successfully deleted leftover files.')
finally:
logging.debug('BAZARR now deleting release archive')
os.remove(bazarr_zip)
else:
return
if is_updated:
logging.debug('BAZARR new release have been installed, now we restart')
from .server import webserver
webserver.restart()
def update_cleaner(zipfile, bazarr_dir, config_dir):
with ZipFile(zipfile, 'r') as archive:
file_in_zip = archive.namelist()
logging.debug(f'BAZARR zip file contain {len(file_in_zip)} directories and files')
separator = os.path.sep
if os.path.sep == '\\':
logging.debug('BAZARR upgrade leftover cleaner is running on Windows. We\'ll fix the zip file separator '
'accordingly.')
for i, item in enumerate(file_in_zip):
file_in_zip[i] = item.replace('/', '\\')
separator += os.path.sep
else:
logging.debug('BAZARR upgrade leftover cleaner is running on something else than Windows. The zip file '
'separator are fine.')
dir_to_ignore = [f'^.{separator}',
f'^bin{separator}',
f'^venv{separator}',
f'^WinPython{separator}',
f'{separator}__pycache__{separator}$']
if os.path.abspath(bazarr_dir).lower() == os.path.abspath(config_dir).lower():
# for users who installed Bazarr inside the config directory (ie: `%programdata%\Bazarr` on windows)
dir_to_ignore.append(f'^backup{separator}')
dir_to_ignore.append(f'^cache{separator}')
dir_to_ignore.append(f'^config{separator}')
dir_to_ignore.append(f'^db{separator}')
dir_to_ignore.append(f'^log{separator}')
dir_to_ignore.append(f'^restore{separator}')
dir_to_ignore.append(f'^update{separator}')
elif os.path.abspath(bazarr_dir).lower() in os.path.abspath(config_dir).lower():
# when config directory is a child of Bazarr installation directory
dir_to_ignore.append(f'^{os.path.relpath(config_dir, bazarr_dir)}{separator}')
dir_to_ignore_regex_string = '(?:% s)' % '|'.join(dir_to_ignore)
logging.debug(f'BAZARR upgrade leftover cleaner will ignore directories matching this '
f'regex: {dir_to_ignore_regex_string}')
dir_to_ignore_regex = re.compile(dir_to_ignore_regex_string)
file_to_ignore = ['nssm.exe', '7za.exe', 'unins000.exe', 'unins000.dat']
logging.debug(f'BAZARR upgrade leftover cleaner will ignore those files: {", ".join(file_to_ignore)}')
extension_to_ignore = ['.pyc']
logging.debug(
f'BAZARR upgrade leftover cleaner will ignore files with those extensions: {", ".join(extension_to_ignore)}')
file_on_disk = []
folder_list = []
for foldername, subfolders, filenames in os.walk(bazarr_dir):
relative_foldername = os.path.relpath(foldername, bazarr_dir) + os.path.sep
if not dir_to_ignore_regex.findall(relative_foldername):
if relative_foldername not in folder_list:
folder_list.append(relative_foldername)
for file in filenames:
if file in file_to_ignore:
continue
elif os.path.splitext(file)[1] in extension_to_ignore:
continue
elif foldername == bazarr_dir:
file_on_disk.append(file)
else:
current_dir = relative_foldername
filepath = os.path.join(current_dir, file)
if not dir_to_ignore_regex.findall(filepath):
file_on_disk.append(filepath)
logging.debug(f'BAZARR directory contain {len(file_on_disk)} files')
logging.debug(f'BAZARR directory contain {len(folder_list)} directories')
file_on_disk += folder_list
logging.debug(f'BAZARR directory contain {len(file_on_disk)} directories and files')
file_to_remove = list(set(file_on_disk) - set(file_in_zip))
logging.debug(f'BAZARR will delete {len(file_to_remove)} directories and files')
logging.debug(f'BAZARR will delete this: {", ".join(file_to_remove)}')
for file in file_to_remove:
filepath = os.path.join(bazarr_dir, file)
try:
if os.path.isdir(filepath):
rmtree(filepath, ignore_errors=True)
else:
os.remove(filepath)
except Exception:
logging.debug(f'BAZARR upgrade leftover cleaner cannot delete {filepath}')