summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authormorpheus65535 <[email protected]>2021-02-26 10:57:47 -0500
committerGitHub <[email protected]>2021-02-26 10:57:47 -0500
commit8208c4893e687da0a4c02bd77fd07efdabf4fb18 (patch)
tree6362511e5a8f75b49548ba37439f18a0bbf7ef29
parentf6a9cee3c0329a11cf9cfdcf3868de2afd2f0f78 (diff)
downloadbazarr-8208c4893e687da0a4c02bd77fd07efdabf4fb18.tar.gz
bazarr-8208c4893e687da0a4c02bd77fd07efdabf4fb18.zip
Refactored the upgrade mechanism to use only Github releases
-rw-r--r--.github/workflows/release_beta_to_dev.yaml41
-rw-r--r--.github/workflows/release_major_and_merge.yaml59
-rw-r--r--.github/workflows/release_minor_and_merge.yaml59
-rw-r--r--.github/workflows/release_patch_and_merge.yaml59
-rw-r--r--.release-it.json21
-rw-r--r--VERSION1
-rw-r--r--bazarr/check_update.py335
-rw-r--r--bazarr/config.py1
-rw-r--r--bazarr/init.py1
-rw-r--r--bazarr/main.py37
-rw-r--r--bazarr/scheduler.py12
-rw-r--r--changelog.hbs6
-rw-r--r--libs/semver.py1259
-rw-r--r--views/_main.html23
-rw-r--r--views/settingsgeneral.html15
15 files changed, 1647 insertions, 282 deletions
diff --git a/.github/workflows/release_beta_to_dev.yaml b/.github/workflows/release_beta_to_dev.yaml
new file mode 100644
index 000000000..642c5d189
--- /dev/null
+++ b/.github/workflows/release_beta_to_dev.yaml
@@ -0,0 +1,41 @@
+name: release_beta_to_dev
+on:
+ push:
+ branches: [ development ]
+ pull_request:
+ branches: [ development ]
+
+jobs:
+ Release:
+ runs-on: ubuntu-latest
+ env:
+ ACTIONS_ALLOW_UNSECURE_COMMANDS: true
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ ref: development
+
+ - name: Setup NodeJS
+ uses: actions/setup-node@v2
+ with:
+ node-version: '15.x'
+ - run: npm install -D release-it
+ - run: npm install -D @release-it/bumper
+
+ - id: latest_release
+ uses: pozetroninc/github-action-get-latest-release@master
+ with:
+ repository: ${{ github.repository }}
+ excludes: draft
+
+ - name: Define LAST_VERSION environment variable
+ run: |
+ echo "LAST_VERSION=${{steps.latest_release.outputs.release}}" >> $GITHUB_ENV
+
+ - name: Update version and create release
+ uses: TheRealWaldo/[email protected]
+ with:
+ json-opts: '{"preRelease": true, "increment": "prepatch", "preReleaseId": "beta"}' \ No newline at end of file
diff --git a/.github/workflows/release_major_and_merge.yaml b/.github/workflows/release_major_and_merge.yaml
new file mode 100644
index 000000000..4563a50ee
--- /dev/null
+++ b/.github/workflows/release_major_and_merge.yaml
@@ -0,0 +1,59 @@
+name: release_major_and_merge
+on:
+ workflow_dispatch
+
+jobs:
+ Release:
+ runs-on: ubuntu-latest
+ env:
+ ACTIONS_ALLOW_UNSECURE_COMMANDS: true
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ steps:
+ - name: Validate branch
+ if: ${{ github.ref != 'refs/heads/development' }}
+ run: |
+ echo This action can only be run on development branch, not ${{ github.ref }}
+ exit 1
+
+ - name: Checkout source code
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ ref: development
+
+ - name: Setup NodeJS
+ uses: actions/setup-node@v2
+ with:
+ node-version: '15.x'
+ - run: npm install -D release-it
+ - run: npm install -D @release-it/bumper
+ - run: npm install -D auto-changelog
+
+ - id: latest_release
+ uses: pozetroninc/github-action-get-latest-release@master
+ with:
+ repository: ${{ github.repository }}
+ excludes: prerelease, draft
+
+ - name: Define LAST_VERSION environment variable
+ run: |
+ echo "LAST_VERSION=${{steps.latest_release.outputs.release}}" >> $GITHUB_ENV
+
+ - name: Update version and create release
+ uses: TheRealWaldo/[email protected]
+ with:
+ json-opts: '{"increment": "major"}'
+ Merge:
+ needs: Release
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v2
+
+ - name: Merge development -> master
+ uses: devmasx/[email protected]
+ with:
+ type: now
+ from_branch: development
+ target_branch: master
+ github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file
diff --git a/.github/workflows/release_minor_and_merge.yaml b/.github/workflows/release_minor_and_merge.yaml
new file mode 100644
index 000000000..c47fe3e4b
--- /dev/null
+++ b/.github/workflows/release_minor_and_merge.yaml
@@ -0,0 +1,59 @@
+name: release_minor_and_merge
+on:
+ workflow_dispatch
+
+jobs:
+ Release:
+ runs-on: ubuntu-latest
+ env:
+ ACTIONS_ALLOW_UNSECURE_COMMANDS: true
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ steps:
+ - name: Validate branch
+ if: ${{ github.ref != 'refs/heads/development' }}
+ run: |
+ echo This action can only be run on development branch, not ${{ github.ref }}
+ exit 1
+
+ - name: Checkout source code
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ ref: development
+
+ - name: Setup NodeJS
+ uses: actions/setup-node@v2
+ with:
+ node-version: '15.x'
+ - run: npm install -D release-it
+ - run: npm install -D @release-it/bumper
+ - run: npm install -D auto-changelog
+
+ - id: latest_release
+ uses: pozetroninc/github-action-get-latest-release@master
+ with:
+ repository: ${{ github.repository }}
+ excludes: prerelease, draft
+
+ - name: Define LAST_VERSION environment variable
+ run: |
+ echo "LAST_VERSION=${{steps.latest_release.outputs.release}}" >> $GITHUB_ENV
+
+ - name: Update version and create release
+ uses: TheRealWaldo/[email protected]
+ with:
+ json-opts: '{"increment": "minor"}'
+ Merge:
+ needs: Release
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v2
+
+ - name: Merge development -> master
+ uses: devmasx/[email protected]
+ with:
+ type: now
+ from_branch: development
+ target_branch: master
+ github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file
diff --git a/.github/workflows/release_patch_and_merge.yaml b/.github/workflows/release_patch_and_merge.yaml
new file mode 100644
index 000000000..0f31f3c36
--- /dev/null
+++ b/.github/workflows/release_patch_and_merge.yaml
@@ -0,0 +1,59 @@
+name: release_patch_and_merge
+on:
+ workflow_dispatch
+
+jobs:
+ Release:
+ runs-on: ubuntu-latest
+ env:
+ ACTIONS_ALLOW_UNSECURE_COMMANDS: true
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ steps:
+ - name: Validate branch
+ if: ${{ github.ref != 'refs/heads/development' }}
+ run: |
+ echo This action can only be run on development branch, not ${{ github.ref }}
+ exit 1
+
+ - name: Checkout source code
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ ref: development
+
+ - name: Setup NodeJS
+ uses: actions/setup-node@v2
+ with:
+ node-version: '15.x'
+ - run: npm install -D release-it
+ - run: npm install -D @release-it/bumper
+ - run: npm install -D auto-changelog
+
+ - id: latest_release
+ uses: pozetroninc/github-action-get-latest-release@master
+ with:
+ repository: ${{ github.repository }}
+ excludes: prerelease, draft
+
+ - name: Define LAST_VERSION environment variable
+ run: |
+ echo "LAST_VERSION=${{steps.latest_release.outputs.release}}" >> $GITHUB_ENV
+
+ - name: Update version and create release
+ uses: TheRealWaldo/[email protected]
+ with:
+ json-opts: '{"increment": "patch"}'
+ Merge:
+ needs: Release
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v2
+
+ - name: Merge development -> master
+ uses: devmasx/[email protected]
+ with:
+ type: now
+ from_branch: development
+ target_branch: master
+ github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file
diff --git a/.release-it.json b/.release-it.json
new file mode 100644
index 000000000..00fcfac9f
--- /dev/null
+++ b/.release-it.json
@@ -0,0 +1,21 @@
+{
+ "github": {
+ "release": true,
+ "releaseName": "v${version}",
+ "releaseNotes": "echo \"From newest to oldest:\" && git log --pretty=format:\"- %s [%h](${repo.protocol}://${repo.host}/${repo.owner}/${repo.project}/commit/%H)\" --no-merges --grep \"^Release\" --invert-grep $LAST_VERSION..HEAD"
+ },
+ "npm": {
+ "publish": false,
+ "ignoreVersion": true
+ },
+ "plugins": {
+ "@release-it/bumper": {
+ "in": {
+ "file": "VERSION"
+ },
+ "out": {
+ "file": "VERSION"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/VERSION b/VERSION
new file mode 100644
index 000000000..f514a2f0b
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.9.1 \ No newline at end of file
diff --git a/bazarr/check_update.py b/bazarr/check_update.py
index 06c454d22..451004211 100644
--- a/bazarr/check_update.py
+++ b/bazarr/check_update.py
@@ -4,124 +4,19 @@ import os
import logging
import json
import requests
-import tarfile
+import semver
+from zipfile import ZipFile
from get_args import args
from config import settings
-from database import database
-
-if not args.no_update and not args.release_update:
- import git
-
-current_working_directory = os.path.dirname(os.path.dirname(__file__))
-
-
-def gitconfig():
- g = git.Repo.init(current_working_directory)
- config_read = g.config_reader()
- config_write = g.config_writer()
-
- try:
- username = config_read.get_value("user", "name")
- except:
- logging.debug('BAZARR Settings git username')
- config_write.set_value("user", "name", "Bazarr")
-
- try:
- email = config_read.get_value("user", "email")
- except:
- logging.debug('BAZARR Settings git email')
- config_write.set_value("user", "email", "[email protected]")
-
- config_write.release()
-
-
-def check_and_apply_update():
- check_releases()
- if not args.release_update:
- gitconfig()
- branch = settings.general.branch
- g = git.cmd.Git(current_working_directory)
- g.fetch('origin')
- result = g.diff('--shortstat', 'origin/' + branch)
- if len(result) == 0:
- logging.info('BAZARR No new version of Bazarr available.')
- else:
- g.reset('--hard', 'HEAD')
- g.checkout(branch)
- g.reset('--hard', 'origin/' + branch)
- g.pull()
- logging.info('BAZARR Updated to latest version. Restart required. ' + result)
- updated()
- else:
- url = 'https://api.github.com/repos/morpheus65535/bazarr/releases/latest'
- release = request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == list)
-
- if release is None:
- logging.warning('BAZARR Could not get releases from GitHub.')
- return
- else:
- latest_release = release['tag_name']
-
- if ('v' + os.environ["BAZARR_VERSION"]) != latest_release:
- update_from_source(tar_download_url=release['tarball_url'])
- else:
- logging.info('BAZARR is up to date')
-
-
-def update_from_source(tar_download_url):
- update_dir = os.path.join(os.path.dirname(__file__), '..', 'update')
-
- logging.info('BAZARR Downloading update from: ' + tar_download_url)
- data = request_content(tar_download_url)
-
- if not data:
- logging.error("BAZARR Unable to retrieve new version from '%s', can't update", tar_download_url)
- return
-
- download_name = settings.general.branch + '-github'
- tar_download_path = os.path.join(os.path.dirname(__file__), '..', download_name)
-
- # Save tar to disk
- with open(tar_download_path, 'wb') as f:
- f.write(data)
-
- # Extract the tar to update folder
- logging.info('BAZARR Extracting file: ' + tar_download_path)
- tar = tarfile.open(tar_download_path)
- tar.extractall(update_dir)
- tar.close()
-
- # Delete the tar.gz
- logging.info('BAZARR 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:
- logging.error("BAZARR 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(os.path.dirname(__file__), '..', dirname, curfile)
-
- if os.path.isfile(new_path):
- os.remove(new_path)
- os.renames(old_path, new_path)
- updated()
def check_releases():
releases = []
url_releases = 'https://api.github.com/repos/morpheus65535/Bazarr/releases'
try:
- r = requests.get(url_releases, timeout=15)
+ logging.debug('BAZARR getting releases from Github: {}'.format(url_releases))
+ r = requests.get(url_releases, allow_redirects=True)
r.raise_for_status()
except requests.exceptions.HTTPError as errh:
logging.exception("Error trying to get releases from Github. Http error.")
@@ -136,156 +31,108 @@ def check_releases():
releases.append({'name': release['name'],
'body': release['body'],
'date': release['published_at'],
- 'prerelease': release['prerelease']})
+ 'prerelease': release['prerelease'],
+ 'download_link': release['zipball_url']})
with open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'w') as f:
json.dump(releases, f)
+ logging.debug('BAZARR saved {} releases to releases.txt'.format(len(r.json())))
-class FakeLock(object):
- """
- If no locking or request throttling is needed, use this
- """
-
- def __enter__(self):
- """
- Do nothing on enter
- """
- pass
-
- def __exit__(self, type, value, traceback):
- """
- Do nothing on exit
- """
- pass
-
-
-fake_lock = FakeLock()
+def check_if_new_update():
+ if settings.general.branch == 'master':
+ use_prerelease = False
+ elif settings.general.branch == 'development':
+ use_prerelease = True
+ else:
+ logging.error('BAZARR unknown branch provided to updater: {}'.format(settings.general.branch))
+ return
+ logging.debug('BAZARR updater is using {} branch'.format(settings.general.branch))
+ check_releases()
-def request_content(url, **kwargs):
- """
- Wrapper for `request_response', which will return the raw content.
- """
-
- response = request_response(url, **kwargs)
-
- if response is not None:
- return response.content
+ with open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'r') as f:
+ data = json.load(f)
+ if not args.no_update:
+ if use_prerelease:
+ release = next((item for item in data), None)
+ else:
+ release = next((item for item in data if not item["prerelease"]), None)
+ if release:
+ logging.debug('BAZARR last release available is {}'.format(release['name']))
-def request_response(url, method="get", auto_raise=True,
- whitelist_status_code=None, lock=fake_lock, **kwargs):
- """
- Convenient wrapper for `requests.get', which will capture the exceptions
- and log them. On success, the Response object is returned. In case of a
- exception, None is returned.
+ try:
+ semver.parse(os.environ["BAZARR_VERSION"])
+ semver.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
- Additionally, there is support for rate limiting. To use this feature,
- supply a tuple of (lock, request_limit). The lock is used to make sure no
- other request with the same lock is executed. The request limit is the
- minimal time between two requests (and so 1/request_limit is the number of
- requests per seconds).
- """
-
- # Convert whitelist_status_code to a list if needed
- if whitelist_status_code and type(whitelist_status_code) != list:
- whitelist_status_code = [whitelist_status_code]
-
- # Disable verification of SSL certificates if requested. Note: this could
- # pose a security issue!
- kwargs["verify"] = True
-
- # Map method to the request.XXX method. This is a simple hack, but it
- # allows requests to apply more magic per method. See lib/requests/api.py.
- request_method = getattr(requests, method.lower())
-
- try:
- # Request URL and wait for response
- with lock:
- logging.debug(
- "BAZARR Requesting URL via %s method: %s", method.upper(), url)
- response = request_method(url, **kwargs)
-
- # If status code != OK, then raise exception, except if the status code
- # is white listed.
- if whitelist_status_code and auto_raise:
- if response.status_code not in whitelist_status_code:
- try:
- response.raise_for_status()
- except:
- logging.debug(
- "BAZARR Response status code %d is not white "
- "listed, raised exception", response.status_code)
- raise
- elif auto_raise:
- response.raise_for_status()
-
- return response
- except requests.exceptions.SSLError as e:
- if kwargs["verify"]:
- logging.error(
- "BAZARR Unable to connect to remote host because of a SSL error. "
- "It is likely that your system cannot verify the validity"
- "of the certificate. The remote certificate is either "
- "self-signed, or the remote server uses SNI. See the wiki for "
- "more information on this topic.")
- else:
- logging.error(
- "BAZARR SSL error raised during connection, with certificate "
- "verification turned off: %s", e)
- except requests.ConnectionError:
- logging.error(
- "BAZARR Unable to connect to remote host. Check if the remote "
- "host is up and running.")
- except requests.Timeout:
- logging.error(
- "BAZARR Request timed out. The remote host did not respond timely.")
- except requests.HTTPError as e:
- if e.response is not None:
- if e.response.status_code >= 500:
- cause = "remote server error"
- elif e.response.status_code >= 400:
- cause = "local client error"
+ # 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('BAZARR newer release available and will be downloaded: {}'.format(release['name']))
+ download_release(url=release['download_link'])
else:
- # I don't think we will end up here, but for completeness
- cause = "unknown"
-
- logging.error(
- "BAZARR Request raise HTTP error with status code %d (%s).",
- e.response.status_code, cause)
+ logging.debug('BAZARR no newer release have been found')
else:
- logging.error("BAZARR Request raised HTTP error.")
- except requests.RequestException as e:
- logging.error("BAZARR Request raised exception: %s", e)
-
+ logging.debug('BAZARR no release found')
+ else:
+ logging.debug('BAZARR --no_update have been used as an argument')
-def request_json(url, **kwargs):
- """
- Wrapper for `request_response', which will decode the response as JSON
- object and return the result, if no exceptions are raised.
- As an option, a validator callback can be given, which should return True
- if the result is valid.
- """
-
- validator = kwargs.pop("validator", None)
- response = request_response(url, **kwargs)
-
- if response is not None:
+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 as e:
+ logging.debug('BAZARR unable to create update directory {}'.format(update_dir))
+ else:
+ logging.debug('BAZARR downloading release from Github: {}'.format(url))
+ r = requests.get(url, allow_redirects=True)
+ if r:
try:
- result = response.json()
-
- if validator and not validator(result):
- logging.error("BAZARR JSON validation result failed")
+ with open(os.path.join(update_dir, 'bazarr.zip'), 'wb') as f:
+ f.write(r.content)
+ except Exception as e:
+ 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(__file__))
+ if os.path.isdir(update_dir):
+ if os.path.isfile(bazarr_zip):
+ logging.debug('BAZARR is trying to unzip this release to {0}: {1}'.format(bazarr_dir, bazarr_zip))
+ try:
+ with ZipFile(bazarr_zip, 'r') as archive:
+ zip_root_directory = archive.namelist()[0]
+ 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 as e:
+ logging.exception('BAZARR unable to unzip release')
else:
- return result
- except ValueError:
- logging.error("BAZARR Response returned invalid JSON data")
-
+ is_updated = True
+ finally:
+ logging.debug('BAZARR now deleting release archive')
+ os.remove(bazarr_zip)
+ else:
+ return
-def updated(restart=True):
- if settings.general.getboolean('update_restart') and restart:
+ if is_updated:
+ logging.debug('BAZARR new release have been installed, now we restart')
from server import webserver
webserver.restart()
- else:
- database.execute("UPDATE system SET updated='1'")
diff --git a/bazarr/config.py b/bazarr/config.py
index 9e20b1925..db5e30849 100644
--- a/bazarr/config.py
+++ b/bazarr/config.py
@@ -49,7 +49,6 @@ defaults = {
'chmod': '0640',
'subfolder': 'current',
'subfolder_custom': '',
- 'update_restart': 'True',
'upgrade_subs': 'True',
'upgrade_frequency': '12',
'days_to_upgrade_subs': '7',
diff --git a/bazarr/init.py b/bazarr/init.py
index f503b4170..4ef90ffdb 100644
--- a/bazarr/init.py
+++ b/bazarr/init.py
@@ -135,6 +135,7 @@ if settings.analytics.visitor:
# Clean unused settings from config.ini
with open(os.path.normpath(os.path.join(args.config_dir, 'config', 'config.ini')), 'w+') as handle:
settings.remove_option('general', 'throtteled_providers')
+ settings.remove_option('general', 'update_restart')
settings.write(handle)
diff --git a/bazarr/main.py b/bazarr/main.py
index 2f63b1473..d81b8df88 100644
--- a/bazarr/main.py
+++ b/bazarr/main.py
@@ -1,9 +1,14 @@
# coding=utf-8
-bazarr_version = '0.9.1.1'
-
import os
+bazarr_version = ''
+
+version_file = os.path.join(os.path.dirname(__file__), '..', 'VERSION')
+if os.path.isfile(version_file):
+ with open(version_file, 'r') as f:
+ bazarr_version = f.read()
+
os.environ["BAZARR_VERSION"] = bazarr_version
import gc
@@ -31,21 +36,19 @@ from get_series import *
from get_episodes import *
from get_movies import *
-from check_update import check_and_apply_update, check_releases
+from check_update import apply_update, check_if_new_update, check_releases
from server import app, webserver
from functools import wraps
-# Check and install update on startup when running on Windows from installer
-if args.release_update:
- check_and_apply_update()
-# If not, update releases cache instead.
-else:
- check_releases()
+# Install downloaded update
+if bazarr_version != '':
+ apply_update()
+check_releases()
configure_proxy_func()
-# Reset restart required warning on start
-database.execute("UPDATE system SET configured='0', updated='0'")
+# Reset the updated once Bazarr have been restarted after an update
+database.execute("UPDATE system SET updated='0'")
# Load languages in database
load_language_in_db()
@@ -126,7 +129,13 @@ def login_page():
@app.context_processor
def template_variable_processor():
- return dict(settings=settings, args=args)
+ updated = None
+ try:
+ updated = database.execute("SELECT updated FROM system", only_one=True)['updated']
+ except:
+ pass
+ finally:
+ return dict(settings=settings, args=args, updated=updated)
def api_authorize():
@@ -360,8 +369,8 @@ def settingsscheduler():
@app.route('/check_update')
@login_required
def check_update():
- if not args.no_update:
- check_and_apply_update()
+ if not args.no_update and bazarr_version != '':
+ check_if_new_update()
return '', 200
diff --git a/bazarr/scheduler.py b/bazarr/scheduler.py
index acbd4dba7..c24635f38 100644
--- a/bazarr/scheduler.py
+++ b/bazarr/scheduler.py
@@ -9,7 +9,7 @@ from get_subtitle import wanted_search_missing_subtitles_series, wanted_search_m
from utils import cache_maintenance
from get_args import args
if not args.no_update:
- from check_update import check_and_apply_update, check_releases
+ from check_update import check_if_new_update, check_releases
else:
from check_update import check_releases
from apscheduler.schedulers.background import BackgroundScheduler
@@ -201,18 +201,16 @@ class Scheduler:
id='update_all_movies', name='Update all Movie Subtitles from disk', replace_existing=True)
def __update_bazarr_task(self):
- if not args.no_update:
- task_name = 'Update Bazarr from source on Github'
- if args.release_update:
- task_name = 'Update Bazarr from release on Github'
+ if not args.no_update and os.environ["BAZARR_VERSION"] != '':
+ task_name = 'Update Bazarr'
if settings.general.getboolean('auto_update'):
self.aps_scheduler.add_job(
- check_and_apply_update, IntervalTrigger(hours=6), max_instances=1, coalesce=True,
+ check_if_new_update, IntervalTrigger(hours=6), max_instances=1, coalesce=True,
misfire_grace_time=15, id='update_bazarr', name=task_name, replace_existing=True)
else:
self.aps_scheduler.add_job(
- check_and_apply_update, CronTrigger(year='2100'), hour=4, id='update_bazarr', name=task_name,
+ check_if_new_update, CronTrigger(year='2100'), hour=4, id='update_bazarr', name=task_name,
replace_existing=True)
self.aps_scheduler.add_job(
check_releases, IntervalTrigger(hours=3), max_instances=1, coalesce=True, misfire_grace_time=15,
diff --git a/changelog.hbs b/changelog.hbs
new file mode 100644
index 000000000..eb0156c46
--- /dev/null
+++ b/changelog.hbs
@@ -0,0 +1,6 @@
+From newest to oldest:
+{{#each releases}}
+ {{#each commits}}
+ - {{subject}}{{#if href}} [`{{shorthash}}`]({{href}}){{/if}}
+ {{/each}}
+{{/each}} \ No newline at end of file
diff --git a/libs/semver.py b/libs/semver.py
new file mode 100644
index 000000000..ce8816afb
--- /dev/null
+++ b/libs/semver.py
@@ -0,0 +1,1259 @@
+"""Python helper for Semantic Versioning (http://semver.org/)"""
+from __future__ import print_function
+
+import argparse
+import collections
+from functools import wraps, partial
+import inspect
+import re
+import sys
+import warnings
+
+
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+
+__version__ = "2.13.0"
+__author__ = "Kostiantyn Rybnikov"
+__author_email__ = "[email protected]"
+__maintainer__ = ["Sebastien Celles", "Tom Schraitle"]
+__maintainer_email__ = "[email protected]"
+
+#: Our public interface
+__all__ = (
+ #
+ # Module level function:
+ "bump_build",
+ "bump_major",
+ "bump_minor",
+ "bump_patch",
+ "bump_prerelease",
+ "compare",
+ "deprecated",
+ "finalize_version",
+ "format_version",
+ "match",
+ "max_ver",
+ "min_ver",
+ "parse",
+ "parse_version_info",
+ "replace",
+ #
+ # CLI interface
+ "cmd_bump",
+ "cmd_check",
+ "cmd_compare",
+ "createparser",
+ "main",
+ "process",
+ #
+ # Constants and classes
+ "SEMVER_SPEC_VERSION",
+ "VersionInfo",
+)
+
+#: Contains the implemented semver.org version of the spec
+SEMVER_SPEC_VERSION = "2.0.0"
+
+
+if not hasattr(__builtins__, "cmp"):
+
+ def cmp(a, b):
+ """Return negative if a<b, zero if a==b, positive if a>b."""
+ return (a > b) - (a < b)
+
+
+if PY3: # pragma: no cover
+ string_types = str, bytes
+ text_type = str
+ binary_type = bytes
+
+ def b(s):
+ return s.encode("latin-1")
+
+ def u(s):
+ return s
+
+
+else: # pragma: no cover
+ string_types = unicode, str
+ text_type = unicode
+ binary_type = str
+
+ def b(s):
+ return s
+
+ # Workaround for standalone backslash
+ def u(s):
+ return unicode(s.replace(r"\\", r"\\\\"), "unicode_escape")
+
+
+def ensure_str(s, encoding="utf-8", errors="strict"):
+ # Taken from six project
+ """
+ Coerce *s* to `str`.
+
+ For Python 2:
+ - `unicode` -> encoded to `str`
+ - `str` -> `str`
+
+ For Python 3:
+ - `str` -> `str`
+ - `bytes` -> decoded to `str`
+ """
+ if not isinstance(s, (text_type, binary_type)):
+ raise TypeError("not expecting type '%s'" % type(s))
+ if PY2 and isinstance(s, text_type):
+ s = s.encode(encoding, errors)
+ elif PY3 and isinstance(s, binary_type):
+ s = s.decode(encoding, errors)
+ return s
+
+
+def deprecated(func=None, replace=None, version=None, category=DeprecationWarning):
+ """
+ Decorates a function to output a deprecation warning.
+
+ :param func: the function to decorate (or None)
+ :param str replace: the function to replace (use the full qualified
+ name like ``semver.VersionInfo.bump_major``.
+ :param str version: the first version when this function was deprecated.
+ :param category: allow you to specify the deprecation warning class
+ of your choice. By default, it's :class:`DeprecationWarning`, but
+ you can choose :class:`PendingDeprecationWarning` or a custom class.
+ """
+
+ if func is None:
+ return partial(deprecated, replace=replace, version=version, category=category)
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ msg = ["Function '{m}.{f}' is deprecated."]
+
+ if version:
+ msg.append("Deprecated since version {v}. ")
+ msg.append("This function will be removed in semver 3.")
+ if replace:
+ msg.append("Use {r!r} instead.")
+ else:
+ msg.append("Use the respective 'semver.VersionInfo.{r}' instead.")
+
+ # hasattr is needed for Python2 compatibility:
+ f = func.__qualname__ if hasattr(func, "__qualname__") else func.__name__
+ r = replace or f
+
+ frame = inspect.currentframe().f_back
+
+ msg = " ".join(msg)
+ warnings.warn_explicit(
+ msg.format(m=func.__module__, f=f, r=r, v=version),
+ category=category,
+ filename=inspect.getfile(frame.f_code),
+ lineno=frame.f_lineno,
+ )
+ # As recommended in the Python documentation
+ # https://docs.python.org/3/library/inspect.html#the-interpreter-stack
+ # better remove the interpreter stack:
+ del frame
+ return func(*args, **kwargs)
+
+ return wrapper
+
+
+@deprecated(version="2.10.0")
+def parse(version):
+ """
+ Parse version to major, minor, patch, pre-release, build parts.
+
+ .. deprecated:: 2.10.0
+ Use :func:`semver.VersionInfo.parse` instead.
+
+ :param version: version string
+ :return: dictionary with the keys 'build', 'major', 'minor', 'patch',
+ and 'prerelease'. The prerelease or build keys can be None
+ if not provided
+ :rtype: dict
+
+ >>> ver = semver.parse('3.4.5-pre.2+build.4')
+ >>> ver['major']
+ 3
+ >>> ver['minor']
+ 4
+ >>> ver['patch']
+ 5
+ >>> ver['prerelease']
+ 'pre.2'
+ >>> ver['build']
+ 'build.4'
+ """
+ return VersionInfo.parse(version).to_dict()
+
+
+def comparator(operator):
+ """Wrap a VersionInfo binary op method in a type-check."""
+
+ @wraps(operator)
+ def wrapper(self, other):
+ comparable_types = (VersionInfo, dict, tuple, list, text_type, binary_type)
+ if not isinstance(other, comparable_types):
+ raise TypeError(
+ "other type %r must be in %r" % (type(other), comparable_types)
+ )
+ return operator(self, other)
+
+ return wrapper
+
+
+class VersionInfo(object):
+ """
+ A semver compatible version class.
+
+ :param int major: version when you make incompatible API changes.
+ :param int minor: version when you add functionality in
+ a backwards-compatible manner.
+ :param int patch: version when you make backwards-compatible bug fixes.
+ :param str prerelease: an optional prerelease string
+ :param str build: an optional build string
+ """
+
+ __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build")
+ #: Regex for number in a prerelease
+ _LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+")
+ #: Regex for a semver version
+ _REGEX = re.compile(
+ r"""
+ ^
+ (?P<major>0|[1-9]\d*)
+ \.
+ (?P<minor>0|[1-9]\d*)
+ \.
+ (?P<patch>0|[1-9]\d*)
+ (?:-(?P<prerelease>
+ (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
+ (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
+ ))?
+ (?:\+(?P<build>
+ [0-9a-zA-Z-]+
+ (?:\.[0-9a-zA-Z-]+)*
+ ))?
+ $
+ """,
+ re.VERBOSE,
+ )
+
+ def __init__(self, major, minor=0, patch=0, prerelease=None, build=None):
+ # Build a dictionary of the arguments except prerelease and build
+ version_parts = {
+ "major": major,
+ "minor": minor,
+ "patch": patch,
+ }
+
+ for name, value in version_parts.items():
+ value = int(value)
+ version_parts[name] = value
+ if value < 0:
+ raise ValueError(
+ "{!r} is negative. A version can only be positive.".format(name)
+ )
+
+ self._major = version_parts["major"]
+ self._minor = version_parts["minor"]
+ self._patch = version_parts["patch"]
+ self._prerelease = None if prerelease is None else str(prerelease)
+ self._build = None if build is None else str(build)
+
+ @property
+ def major(self):
+ """The major part of a version (read-only)."""
+ return self._major
+
+ @major.setter
+ def major(self, value):
+ raise AttributeError("attribute 'major' is readonly")
+
+ @property
+ def minor(self):
+ """The minor part of a version (read-only)."""
+ return self._minor
+
+ @minor.setter
+ def minor(self, value):
+ raise AttributeError("attribute 'minor' is readonly")
+
+ @property
+ def patch(self):
+ """The patch part of a version (read-only)."""
+ return self._patch
+
+ @patch.setter
+ def patch(self, value):
+ raise AttributeError("attribute 'patch' is readonly")
+
+ @property
+ def prerelease(self):
+ """The prerelease part of a version (read-only)."""
+ return self._prerelease
+
+ @prerelease.setter
+ def prerelease(self, value):
+ raise AttributeError("attribute 'prerelease' is readonly")
+
+ @property
+ def build(self):
+ """The build part of a version (read-only)."""
+ return self._build
+
+ @build.setter
+ def build(self, value):
+ raise AttributeError("attribute 'build' is readonly")
+
+ def to_tuple(self):
+ """
+ Convert the VersionInfo object to a tuple.
+
+ .. versionadded:: 2.10.0
+ Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to
+ make this function available in the public API.
+
+ :return: a tuple with all the parts
+ :rtype: tuple
+
+ >>> semver.VersionInfo(5, 3, 1).to_tuple()
+ (5, 3, 1, None, None)
+ """
+ return (self.major, self.minor, self.patch, self.prerelease, self.build)
+
+ def to_dict(self):
+ """
+ Convert the VersionInfo object to an OrderedDict.
+
+ .. versionadded:: 2.10.0
+ Renamed ``VersionInfo._asdict`` to ``VersionInfo.to_dict`` to
+ make this function available in the public API.
+
+ :return: an OrderedDict with the keys in the order ``major``, ``minor``,
+ ``patch``, ``prerelease``, and ``build``.
+ :rtype: :class:`collections.OrderedDict`
+
+ >>> semver.VersionInfo(3, 2, 1).to_dict()
+ OrderedDict([('major', 3), ('minor', 2), ('patch', 1), \
+('prerelease', None), ('build', None)])
+ """
+ return collections.OrderedDict(
+ (
+ ("major", self.major),
+ ("minor", self.minor),
+ ("patch", self.patch),
+ ("prerelease", self.prerelease),
+ ("build", self.build),
+ )
+ )
+
+ # For compatibility reasons:
+ @deprecated(replace="semver.VersionInfo.to_tuple", version="2.10.0")
+ def _astuple(self):
+ return self.to_tuple() # pragma: no cover
+
+ _astuple.__doc__ = to_tuple.__doc__
+
+ @deprecated(replace="semver.VersionInfo.to_dict", version="2.10.0")
+ def _asdict(self):
+ return self.to_dict() # pragma: no cover
+
+ _asdict.__doc__ = to_dict.__doc__
+
+ def __iter__(self):
+ """Implement iter(self)."""
+ # As long as we support Py2.7, we can't use the "yield from" syntax
+ for v in self.to_tuple():
+ yield v
+
+ @staticmethod
+ def _increment_string(string):
+ """
+ Look for the last sequence of number(s) in a string and increment.
+
+ :param str string: the string to search for.
+ :return: the incremented string
+
+ Source:
+ http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1
+ """
+ match = VersionInfo._LAST_NUMBER.search(string)
+ if match:
+ next_ = str(int(match.group(1)) + 1)
+ start, end = match.span(1)
+ string = string[: max(end - len(next_), start)] + next_ + string[end:]
+ return string
+
+ def bump_major(self):
+ """
+ Raise the major part of the version, return a new object but leave self
+ untouched.
+
+ :return: new object with the raised major part
+ :rtype: :class:`VersionInfo`
+
+ >>> ver = semver.VersionInfo.parse("3.4.5")
+ >>> ver.bump_major()
+ VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None)
+ """
+ cls = type(self)
+ return cls(self._major + 1)
+
+ def bump_minor(self):
+ """
+ Raise the minor part of the version, return a new object but leave self
+ untouched.
+
+ :return: new object with the raised minor part
+ :rtype: :class:`VersionInfo`
+
+ >>> ver = semver.VersionInfo.parse("3.4.5")
+ >>> ver.bump_minor()
+ VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None)
+ """
+ cls = type(self)
+ return cls(self._major, self._minor + 1)
+
+ def bump_patch(self):
+ """
+ Raise the patch part of the version, return a new object but leave self
+ untouched.
+
+ :return: new object with the raised patch part
+ :rtype: :class:`VersionInfo`
+
+ >>> ver = semver.VersionInfo.parse("3.4.5")
+ >>> ver.bump_patch()
+ VersionInfo(major=3, minor=4, patch=6, prerelease=None, build=None)
+ """
+ cls = type(self)
+ return cls(self._major, self._minor, self._patch + 1)
+
+ def bump_prerelease(self, token="rc"):
+ """
+ Raise the prerelease part of the version, return a new object but leave
+ self untouched.
+
+ :param token: defaults to 'rc'
+ :return: new object with the raised prerelease part
+ :rtype: :class:`VersionInfo`
+
+ >>> ver = semver.VersionInfo.parse("3.4.5-rc.1")
+ >>> ver.bump_prerelease()
+ VersionInfo(major=3, minor=4, patch=5, prerelease='rc.2', \
+build=None)
+ """
+ cls = type(self)
+ prerelease = cls._increment_string(self._prerelease or (token or "rc") + ".0")
+ return cls(self._major, self._minor, self._patch, prerelease)
+
+ def bump_build(self, token="build"):
+ """
+ Raise the build part of the version, return a new object but leave self
+ untouched.
+
+ :param token: defaults to 'build'
+ :return: new object with the raised build part
+ :rtype: :class:`VersionInfo`
+
+ >>> ver = semver.VersionInfo.parse("3.4.5-rc.1+build.9")
+ >>> ver.bump_build()
+ VersionInfo(major=3, minor=4, patch=5, prerelease='rc.1', \
+build='build.10')
+ """
+ cls = type(self)
+ build = cls._increment_string(self._build or (token or "build") + ".0")
+ return cls(self._major, self._minor, self._patch, self._prerelease, build)
+
+ def compare(self, other):
+ """
+ Compare self with other.
+
+ :param other: the second version (can be string, a dict, tuple/list, or
+ a VersionInfo instance)
+ :return: The return value is negative if ver1 < ver2,
+ zero if ver1 == ver2 and strictly positive if ver1 > ver2
+ :rtype: int
+
+ >>> semver.VersionInfo.parse("1.0.0").compare("2.0.0")
+ -1
+ >>> semver.VersionInfo.parse("2.0.0").compare("1.0.0")
+ 1
+ >>> semver.VersionInfo.parse("2.0.0").compare("2.0.0")
+ 0
+ >>> semver.VersionInfo.parse("2.0.0").compare(dict(major=2, minor=0, patch=0))
+ 0
+ """
+ cls = type(self)
+ if isinstance(other, string_types):
+ other = cls.parse(other)
+ elif isinstance(other, dict):
+ other = cls(**other)
+ elif isinstance(other, (tuple, list)):
+ other = cls(*other)
+ elif not isinstance(other, cls):
+ raise TypeError(
+ "Expected str or {} instance, but got {}".format(
+ cls.__name__, type(other)
+ )
+ )
+
+ v1 = self.to_tuple()[:3]
+ v2 = other.to_tuple()[:3]
+ x = cmp(v1, v2)
+ if x:
+ return x
+
+ rc1, rc2 = self.prerelease, other.prerelease
+ rccmp = _nat_cmp(rc1, rc2)
+
+ if not rccmp:
+ return 0
+ if not rc1:
+ return 1
+ elif not rc2:
+ return -1
+
+ return rccmp
+
+ def next_version(self, part, prerelease_token="rc"):
+ """
+ Determines next version, preserving natural order.
+
+ .. versionadded:: 2.10.0
+
+ This function is taking prereleases into account.
+ The "major", "minor", and "patch" raises the respective parts like
+ the ``bump_*`` functions. The real difference is using the
+ "preprelease" part. It gives you the next patch version of the prerelease,
+ for example:
+
+ >>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease"))
+ '0.1.5-rc.1'
+
+ :param part: One of "major", "minor", "patch", or "prerelease"
+ :param prerelease_token: prefix string of prerelease, defaults to 'rc'
+ :return: new object with the appropriate part raised
+ :rtype: :class:`VersionInfo`
+ """
+ validparts = {
+ "major",
+ "minor",
+ "patch",
+ "prerelease",
+ # "build", # currently not used
+ }
+ if part not in validparts:
+ raise ValueError(
+ "Invalid part. Expected one of {validparts}, but got {part!r}".format(
+ validparts=validparts, part=part
+ )
+ )
+ version = self
+ if (version.prerelease or version.build) and (
+ part == "patch"
+ or (part == "minor" and version.patch == 0)
+ or (part == "major" and version.minor == version.patch == 0)
+ ):
+ return version.replace(prerelease=None, build=None)
+
+ if part in ("major", "minor", "patch"):
+ return getattr(version, "bump_" + part)()
+
+ if not version.prerelease:
+ version = version.bump_patch()
+ return version.bump_prerelease(prerelease_token)
+
+ @comparator
+ def __eq__(self, other):
+ return self.compare(other) == 0
+
+ @comparator
+ def __ne__(self, other):
+ return self.compare(other) != 0
+
+ @comparator
+ def __lt__(self, other):
+ return self.compare(other) < 0
+
+ @comparator
+ def __le__(self, other):
+ return self.compare(other) <= 0
+
+ @comparator
+ def __gt__(self, other):
+ return self.compare(other) > 0
+
+ @comparator
+ def __ge__(self, other):
+ return self.compare(other) >= 0
+
+ def __getitem__(self, index):
+ """
+ self.__getitem__(index) <==> self[index]
+
+ Implement getitem. If the part requested is undefined, or a part of the
+ range requested is undefined, it will throw an index error.
+ Negative indices are not supported
+
+ :param Union[int, slice] index: a positive integer indicating the
+ offset or a :func:`slice` object
+ :raises: IndexError, if index is beyond the range or a part is None
+ :return: the requested part of the version at position index
+
+ >>> ver = semver.VersionInfo.parse("3.4.5")
+ >>> ver[0], ver[1], ver[2]
+ (3, 4, 5)
+ """
+ if isinstance(index, int):
+ index = slice(index, index + 1)
+
+ if (
+ isinstance(index, slice)
+ and (index.start is not None and index.start < 0)
+ or (index.stop is not None and index.stop < 0)
+ ):
+ raise IndexError("Version index cannot be negative")
+
+ part = tuple(filter(lambda p: p is not None, self.to_tuple()[index]))
+
+ if len(part) == 1:
+ part = part[0]
+ elif not part:
+ raise IndexError("Version part undefined")
+ return part
+
+ def __repr__(self):
+ s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items())
+ return "%s(%s)" % (type(self).__name__, s)
+
+ def __str__(self):
+ """str(self)"""
+ version = "%d.%d.%d" % (self.major, self.minor, self.patch)
+ if self.prerelease:
+ version += "-%s" % self.prerelease
+ if self.build:
+ version += "+%s" % self.build
+ return version
+
+ def __hash__(self):
+ return hash(self.to_tuple()[:4])
+
+ def finalize_version(self):
+ """
+ Remove any prerelease and build metadata from the version.
+
+ :return: a new instance with the finalized version string
+ :rtype: :class:`VersionInfo`
+
+ >>> str(semver.VersionInfo.parse('1.2.3-rc.5').finalize_version())
+ '1.2.3'
+ """
+ cls = type(self)
+ return cls(self.major, self.minor, self.patch)
+
+ def match(self, match_expr):
+ """
+ Compare self to match a match expression.
+
+ :param str match_expr: operator and version; valid operators are
+ < smaller than
+ > greater than
+ >= greator or equal than
+ <= smaller or equal than
+ == equal
+ != not equal
+ :return: True if the expression matches the version, otherwise False
+ :rtype: bool
+
+ >>> semver.VersionInfo.parse("2.0.0").match(">=1.0.0")
+ True
+ >>> semver.VersionInfo.parse("1.0.0").match(">1.0.0")
+ False
+ """
+ prefix = match_expr[:2]
+ if prefix in (">=", "<=", "==", "!="):
+ match_version = match_expr[2:]
+ elif prefix and prefix[0] in (">", "<"):
+ prefix = prefix[0]
+ match_version = match_expr[1:]
+ else:
+ raise ValueError(
+ "match_expr parameter should be in format <op><ver>, "
+ "where <op> is one of "
+ "['<', '>', '==', '<=', '>=', '!=']. "
+ "You provided: %r" % match_expr
+ )
+
+ possibilities_dict = {
+ ">": (1,),
+ "<": (-1,),
+ "==": (0,),
+ "!=": (-1, 1),
+ ">=": (0, 1),
+ "<=": (-1, 0),
+ }
+
+ possibilities = possibilities_dict[prefix]
+ cmp_res = self.compare(match_version)
+
+ return cmp_res in possibilities
+
+ @classmethod
+ def parse(cls, version):
+ """
+ Parse version string to a VersionInfo instance.
+
+ :param version: version string
+ :return: a :class:`VersionInfo` instance
+ :raises: :class:`ValueError`
+ :rtype: :class:`VersionInfo`
+
+ .. versionchanged:: 2.11.0
+ Changed method from static to classmethod to
+ allow subclasses.
+
+ >>> semver.VersionInfo.parse('3.4.5-pre.2+build.4')
+ VersionInfo(major=3, minor=4, patch=5, \
+prerelease='pre.2', build='build.4')
+ """
+ match = cls._REGEX.match(ensure_str(version))
+ if match is None:
+ raise ValueError("%s is not valid SemVer string" % version)
+
+ version_parts = match.groupdict()
+
+ version_parts["major"] = int(version_parts["major"])
+ version_parts["minor"] = int(version_parts["minor"])
+ version_parts["patch"] = int(version_parts["patch"])
+
+ return cls(**version_parts)
+
+ def replace(self, **parts):
+ """
+ Replace one or more parts of a version and return a new
+ :class:`VersionInfo` object, but leave self untouched
+
+ .. versionadded:: 2.9.0
+ Added :func:`VersionInfo.replace`
+
+ :param dict parts: the parts to be updated. Valid keys are:
+ ``major``, ``minor``, ``patch``, ``prerelease``, or ``build``
+ :return: the new :class:`VersionInfo` object with the changed
+ parts
+ :raises: :class:`TypeError`, if ``parts`` contains invalid keys
+ """
+ version = self.to_dict()
+ version.update(parts)
+ try:
+ return VersionInfo(**version)
+ except TypeError:
+ unknownkeys = set(parts) - set(self.to_dict())
+ error = "replace() got %d unexpected keyword " "argument(s): %s" % (
+ len(unknownkeys),
+ ", ".join(unknownkeys),
+ )
+ raise TypeError(error)
+
+ @classmethod
+ def isvalid(cls, version):
+ """
+ Check if the string is a valid semver version.
+
+ .. versionadded:: 2.9.1
+
+ :param str version: the version string to check
+ :return: True if the version string is a valid semver version, False
+ otherwise.
+ :rtype: bool
+ """
+ try:
+ cls.parse(version)
+ return True
+ except ValueError:
+ return False
+
+
+@deprecated(replace="semver.VersionInfo.parse", version="2.10.0")
+def parse_version_info(version):
+ """
+ Parse version string to a VersionInfo instance.
+
+ .. deprecated:: 2.10.0
+ Use :func:`semver.VersionInfo.parse` instead.
+
+ .. versionadded:: 2.7.2
+ Added :func:`semver.parse_version_info`
+
+ :param version: version string
+ :return: a :class:`VersionInfo` instance
+ :rtype: :class:`VersionInfo`
+
+ >>> version_info = semver.VersionInfo.parse("3.4.5-pre.2+build.4")
+ >>> version_info.major
+ 3
+ >>> version_info.minor
+ 4
+ >>> version_info.patch
+ 5
+ >>> version_info.prerelease
+ 'pre.2'
+ >>> version_info.build
+ 'build.4'
+ """
+ return VersionInfo.parse(version)
+
+
+def _nat_cmp(a, b):
+ def convert(text):
+ return int(text) if re.match("^[0-9]+$", text) else text
+
+ def split_key(key):
+ return [convert(c) for c in key.split(".")]
+
+ def cmp_prerelease_tag(a, b):
+ if isinstance(a, int) and isinstance(b, int):
+ return cmp(a, b)
+ elif isinstance(a, int):
+ return -1
+ elif isinstance(b, int):
+ return 1
+ else:
+ return cmp(a, b)
+
+ a, b = a or "", b or ""
+ a_parts, b_parts = split_key(a), split_key(b)
+ for sub_a, sub_b in zip(a_parts, b_parts):
+ cmp_result = cmp_prerelease_tag(sub_a, sub_b)
+ if cmp_result != 0:
+ return cmp_result
+ else:
+ return cmp(len(a), len(b))
+
+
+@deprecated(version="2.10.0")
+def compare(ver1, ver2):
+ """
+ Compare two versions strings.
+
+ :param ver1: version string 1
+ :param ver2: version string 2
+ :return: The return value is negative if ver1 < ver2,
+ zero if ver1 == ver2 and strictly positive if ver1 > ver2
+ :rtype: int
+
+ >>> semver.compare("1.0.0", "2.0.0")
+ -1
+ >>> semver.compare("2.0.0", "1.0.0")
+ 1
+ >>> semver.compare("2.0.0", "2.0.0")
+ 0
+ """
+ v1 = VersionInfo.parse(ver1)
+ return v1.compare(ver2)
+
+
+@deprecated(version="2.10.0")
+def match(version, match_expr):
+ """
+ Compare two versions strings through a comparison.
+
+ :param str version: a version string
+ :param str match_expr: operator and version; valid operators are
+ < smaller than
+ > greater than
+ >= greator or equal than
+ <= smaller or equal than
+ == equal
+ != not equal
+ :return: True if the expression matches the version, otherwise False
+ :rtype: bool
+
+ >>> semver.match("2.0.0", ">=1.0.0")
+ True
+ >>> semver.match("1.0.0", ">1.0.0")
+ False
+ """
+ ver = VersionInfo.parse(version)
+ return ver.match(match_expr)
+
+
+@deprecated(replace="max", version="2.10.2")
+def max_ver(ver1, ver2):
+ """
+ Returns the greater version of two versions strings.
+
+ :param ver1: version string 1
+ :param ver2: version string 2
+ :return: the greater version of the two
+ :rtype: :class:`VersionInfo`
+
+ >>> semver.max_ver("1.0.0", "2.0.0")
+ '2.0.0'
+ """
+ if isinstance(ver1, string_types):
+ ver1 = VersionInfo.parse(ver1)
+ elif not isinstance(ver1, VersionInfo):
+ raise TypeError()
+ cmp_res = ver1.compare(ver2)
+ if cmp_res >= 0:
+ return str(ver1)
+ else:
+ return ver2
+
+
+@deprecated(replace="min", version="2.10.2")
+def min_ver(ver1, ver2):
+ """
+ Returns the smaller version of two versions strings.
+
+ :param ver1: version string 1
+ :param ver2: version string 2
+ :return: the smaller version of the two
+ :rtype: :class:`VersionInfo`
+
+ >>> semver.min_ver("1.0.0", "2.0.0")
+ '1.0.0'
+ """
+ ver1 = VersionInfo.parse(ver1)
+ cmp_res = ver1.compare(ver2)
+ if cmp_res <= 0:
+ return str(ver1)
+ else:
+ return ver2
+
+
+@deprecated(replace="str(versionobject)", version="2.10.0")
+def format_version(major, minor, patch, prerelease=None, build=None):
+ """
+ Format a version string according to the Semantic Versioning specification.
+
+ .. deprecated:: 2.10.0
+ Use ``str(VersionInfo(VERSION)`` instead.
+
+ :param int major: the required major part of a version
+ :param int minor: the required minor part of a version
+ :param int patch: the required patch part of a version
+ :param str prerelease: the optional prerelease part of a version
+ :param str build: the optional build part of a version
+ :return: the formatted string
+ :rtype: str
+
+ >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4')
+ '3.4.5-pre.2+build.4'
+ """
+ return str(VersionInfo(major, minor, patch, prerelease, build))
+
+
+@deprecated(version="2.10.0")
+def bump_major(version):
+ """
+ Raise the major part of the version string.
+
+ .. deprecated:: 2.10.0
+ Use :func:`semver.VersionInfo.bump_major` instead.
+
+ :param: version string
+ :return: the raised version string
+ :rtype: str
+
+ >>> semver.bump_major("3.4.5")
+ '4.0.0'
+ """
+ return str(VersionInfo.parse(version).bump_major())
+
+
+@deprecated(version="2.10.0")
+def bump_minor(version):
+ """
+ Raise the minor part of the version string.
+
+ .. deprecated:: 2.10.0
+ Use :func:`semver.VersionInfo.bump_minor` instead.
+
+ :param: version string
+ :return: the raised version string
+ :rtype: str
+
+ >>> semver.bump_minor("3.4.5")
+ '3.5.0'
+ """
+ return str(VersionInfo.parse(version).bump_minor())
+
+
+@deprecated(version="2.10.0")
+def bump_patch(version):
+ """
+ Raise the patch part of the version string.
+
+ .. deprecated:: 2.10.0
+ Use :func:`semver.VersionInfo.bump_patch` instead.
+
+ :param: version string
+ :return: the raised version string
+ :rtype: str
+
+ >>> semver.bump_patch("3.4.5")
+ '3.4.6'
+ """
+ return str(VersionInfo.parse(version).bump_patch())
+
+
+@deprecated(version="2.10.0")
+def bump_prerelease(version, token="rc"):
+ """
+ Raise the prerelease part of the version string.
+
+ .. deprecated:: 2.10.0
+ Use :func:`semver.VersionInfo.bump_prerelease` instead.
+
+ :param version: version string
+ :param token: defaults to 'rc'
+ :return: the raised version string
+ :rtype: str
+
+ >>> semver.bump_prerelease('3.4.5', 'dev')
+ '3.4.5-dev.1'
+ """
+ return str(VersionInfo.parse(version).bump_prerelease(token))
+
+
+@deprecated(version="2.10.0")
+def bump_build(version, token="build"):
+ """
+ Raise the build part of the version string.
+
+ .. deprecated:: 2.10.0
+ Use :func:`semver.VersionInfo.bump_build` instead.
+
+ :param version: version string
+ :param token: defaults to 'build'
+ :return: the raised version string
+ :rtype: str
+
+ >>> semver.bump_build('3.4.5-rc.1+build.9')
+ '3.4.5-rc.1+build.10'
+ """
+ return str(VersionInfo.parse(version).bump_build(token))
+
+
+@deprecated(version="2.10.0")
+def finalize_version(version):
+ """
+ Remove any prerelease and build metadata from the version string.
+
+ .. deprecated:: 2.10.0
+ Use :func:`semver.VersionInfo.finalize_version` instead.
+
+ .. versionadded:: 2.7.9
+ Added :func:`finalize_version`
+
+ :param version: version string
+ :return: the finalized version string
+ :rtype: str
+
+ >>> semver.finalize_version('1.2.3-rc.5')
+ '1.2.3'
+ """
+ verinfo = VersionInfo.parse(version)
+ return str(verinfo.finalize_version())
+
+
+@deprecated(version="2.10.0")
+def replace(version, **parts):
+ """
+ Replace one or more parts of a version and return the new string.
+
+ .. deprecated:: 2.10.0
+ Use :func:`semver.VersionInfo.replace` instead.
+
+ .. versionadded:: 2.9.0
+ Added :func:`replace`
+
+ :param str version: the version string to replace
+ :param dict parts: the parts to be updated. Valid keys are:
+ ``major``, ``minor``, ``patch``, ``prerelease``, or ``build``
+ :return: the replaced version string
+ :raises: TypeError, if ``parts`` contains invalid keys
+ :rtype: str
+
+ >>> import semver
+ >>> semver.replace("1.2.3", major=2, patch=10)
+ '2.2.10'
+ """
+ return str(VersionInfo.parse(version).replace(**parts))
+
+
+# ---- CLI
+def cmd_bump(args):
+ """
+ Subcommand: Bumps a version.
+
+ Synopsis: bump <PART> <VERSION>
+ <PART> can be major, minor, patch, prerelease, or build
+
+ :param args: The parsed arguments
+ :type args: :class:`argparse.Namespace`
+ :return: the new, bumped version
+ """
+ maptable = {
+ "major": "bump_major",
+ "minor": "bump_minor",
+ "patch": "bump_patch",
+ "prerelease": "bump_prerelease",
+ "build": "bump_build",
+ }
+ if args.bump is None:
+ # When bump is called without arguments,
+ # print the help and exit
+ args.parser.parse_args(["bump", "-h"])
+
+ ver = VersionInfo.parse(args.version)
+ # get the respective method and call it
+ func = getattr(ver, maptable[args.bump])
+ return str(func())
+
+
+def cmd_check(args):
+ """
+ Subcommand: Checks if a string is a valid semver version.
+
+ Synopsis: check <VERSION>
+
+ :param args: The parsed arguments
+ :type args: :class:`argparse.Namespace`
+ """
+ if VersionInfo.isvalid(args.version):
+ return None
+ raise ValueError("Invalid version %r" % args.version)
+
+
+def cmd_compare(args):
+ """
+ Subcommand: Compare two versions
+
+ Synopsis: compare <VERSION1> <VERSION2>
+
+ :param args: The parsed arguments
+ :type args: :class:`argparse.Namespace`
+ """
+ return str(compare(args.version1, args.version2))
+
+
+def cmd_nextver(args):
+ """
+ Subcommand: Determines the next version, taking prereleases into account.
+
+ Synopsis: nextver <VERSION> <PART>
+
+ :param args: The parsed arguments
+ :type args: :class:`argparse.Namespace`
+ """
+ version = VersionInfo.parse(args.version)
+ return str(version.next_version(args.part))
+
+
+def createparser():
+ """
+ Create an :class:`argparse.ArgumentParser` instance.
+
+ :return: parser instance
+ :rtype: :class:`argparse.ArgumentParser`
+ """
+ parser = argparse.ArgumentParser(prog=__package__, description=__doc__)
+
+ parser.add_argument(
+ "--version", action="version", version="%(prog)s " + __version__
+ )
+
+ s = parser.add_subparsers()
+ # create compare subcommand
+ parser_compare = s.add_parser("compare", help="Compare two versions")
+ parser_compare.set_defaults(func=cmd_compare)
+ parser_compare.add_argument("version1", help="First version")
+ parser_compare.add_argument("version2", help="Second version")
+
+ # create bump subcommand
+ parser_bump = s.add_parser("bump", help="Bumps a version")
+ parser_bump.set_defaults(func=cmd_bump)
+ sb = parser_bump.add_subparsers(title="Bump commands", dest="bump")
+
+ # Create subparsers for the bump subparser:
+ for p in (
+ sb.add_parser("major", help="Bump the major part of the version"),
+ sb.add_parser("minor", help="Bump the minor part of the version"),
+ sb.add_parser("patch", help="Bump the patch part of the version"),
+ sb.add_parser("prerelease", help="Bump the prerelease part of the version"),
+ sb.add_parser("build", help="Bump the build part of the version"),
+ ):
+ p.add_argument("version", help="Version to raise")
+
+ # Create the check subcommand
+ parser_check = s.add_parser(
+ "check", help="Checks if a string is a valid semver version"
+ )
+ parser_check.set_defaults(func=cmd_check)
+ parser_check.add_argument("version", help="Version to check")
+
+ # Create the nextver subcommand
+ parser_nextver = s.add_parser(
+ "nextver", help="Determines the next version, taking prereleases into account."
+ )
+ parser_nextver.set_defaults(func=cmd_nextver)
+ parser_nextver.add_argument("version", help="Version to raise")
+ parser_nextver.add_argument(
+ "part", help="One of 'major', 'minor', 'patch', or 'prerelease'"
+ )
+ return parser
+
+
+def process(args):
+ """
+ Process the input from the CLI.
+
+ :param args: The parsed arguments
+ :type args: :class:`argparse.Namespace`
+ :param parser: the parser instance
+ :type parser: :class:`argparse.ArgumentParser`
+ :return: result of the selected action
+ :rtype: str
+ """
+ if not hasattr(args, "func"):
+ args.parser.print_help()
+ raise SystemExit()
+
+ # Call the respective function object:
+ return args.func(args)
+
+
+def main(cliargs=None):
+ """
+ Entry point for the application script.
+
+ :param list cliargs: Arguments to parse or None (=use :class:`sys.argv`)
+ :return: error code
+ :rtype: int
+ """
+ try:
+ parser = createparser()
+ args = parser.parse_args(args=cliargs)
+ # Save parser instance:
+ args.parser = parser
+ result = process(args)
+ if result is not None:
+ print(result)
+ return 0
+
+ except (ValueError, TypeError) as err:
+ print("ERROR", err, file=sys.stderr)
+ return 2
+
+
+if __name__ == "__main__":
+ import doctest
+
+ doctest.testmod()
diff --git a/views/_main.html b/views/_main.html
index 948e3171a..5cba5c3f7 100644
--- a/views/_main.html
+++ b/views/_main.html
@@ -84,6 +84,11 @@
margin-bottom: 0.5em;
}
+ #sidebar-nav-notif {
+ position: absolute;
+ bottom: 0;
+ }
+
</style>
{% endblock head %}
@@ -174,7 +179,7 @@
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></a>
<div class="dropdown-menu dropdown-menu-right scale-up">
<ul class="dropdown-user">
- <li><a id="restart"><i class="fas fa-redo"></i> Restart</a></li>
+ <li><a class="restart_action"><i class="fas fa-redo"></i> Restart</a></li>
<li><a id="shutdown"><i class="fas fa-power-off"></i> Shutdown</a></li>
{% if settings.auth.type != 'None' %}
<li><a href="{{ url_for('logout') }}"><i class="fas fa-sign-out-alt"></i> Logout</a>
@@ -282,6 +287,20 @@
</ul>
</nav>
<!-- End Sidebar navigation -->
+
+ <!-- Sidebar notification-->
+ <nav class="sidebar-nav" id="sidebar-nav-notif">
+ <ul id="sidebarnotif">
+ {% if (updated == '1') %}
+ <li>
+ <a href='' class="restart_action">
+ <i class="fas fa-redo"></i><span class="hide-menu"> Restart required</span>
+ </a>
+ </li>
+ {% endif %}
+ </ul>
+ </nav>
+ <!-- End Sidebar notification -->
</div>
<!-- End Sidebar scroll-->
</aside>
@@ -487,7 +506,7 @@
events.close();
});
- $('#restart').on('click', function () {
+ $('.restart_action').on('click', function () {
$('#loader_button').prop("hidden", true);
$('#loader_text').text("Bazarr is restarting, please wait...");
$('#reconnect_overlay').show();
diff --git a/views/settingsgeneral.html b/views/settingsgeneral.html
index c0dec5b57..49adf4391 100644
--- a/views/settingsgeneral.html
+++ b/views/settingsgeneral.html
@@ -269,18 +269,6 @@
<label>Automatically download and install updates. You will still be able to install from System: Tasks</label>
</div>
</div>
- <div class="row">
- <div class="col-sm-3 text-right">
- <b>Restart After Update</b>
- </div>
- <div class="form-group col-sm-8">
- <label class="custom-control custom-checkbox">
- <input type="checkbox" class="custom-control-input" id="settings-general-update_restart" name="settings-general-update_restart">
- <span class="custom-control-label" for="settings-general-update_restart"></span>
- </label>
- <label>Automatically restart after downloading and installing updates. You will still be able to restart manually</label>
- </div>
- </div>
</div>
</form>
</div>
@@ -324,7 +312,7 @@
$('#save_button').prop('disabled', true).css('cursor', 'not-allowed');
// Hide update_div if args.no-update
- {% if args.no_update or args.release_update %}
+ {% if args.no_update %}
$('#update_div').hide()
{% endif %}
@@ -357,7 +345,6 @@
$('#settings-general-debug').prop('checked', {{'true' if settings.general.getboolean('debug') else 'false'}});
$('#settings-analytics-enabled').prop('checked', {{'true' if settings.analytics.getboolean('enabled') else 'false'}});
$('#settings-general-auto_update').prop('checked', {{'true' if settings.general.getboolean('auto_update') else 'false'}});
- $('#settings-general-update_restart').prop('checked', {{'true' if settings.general.getboolean('update_restart') else 'false'}});
$('#save_button').on('click', function() {
var formdata = new FormData(document.getElementById("settings_form"));