From 64c35d7bdba011b7ae462ae0ef19b6dbcf50bbbf Mon Sep 17 00:00:00 2001 From: Dario Date: Thu, 24 Jan 2019 14:51:57 +0100 Subject: [PATCH] Initial version_checker refactor (#6018) * Initial version_checker refactor * Final changes * Implement __str__ * Improve code * Fix git detection * Improve docs * Update Docker update msg * Update exceptions * Fix test * Use self.is_latest_version() only if branch is master * Revert unnecessary change * Review * Update CHANGELOG.md --- CHANGELOG.md | 3 +- medusa/__main__.py | 9 +- medusa/config.py | 2 +- medusa/github_client.py | 18 + medusa/server/api/v1/core.py | 4 +- medusa/server/web/core/error_logs.py | 2 +- medusa/server/web/home/handler.py | 2 +- medusa/updater/__init__.py | 1 + medusa/updater/docker_updater.py | 54 ++ medusa/updater/github_updater.py | 381 ++++++++++ medusa/updater/source_updater.py | 264 +++++++ medusa/updater/update_manager.py | 44 ++ medusa/updater/version_checker.py | 357 +++++++++ medusa/version_checker.py | 1048 -------------------------- setup.cfg | 7 +- tests/conftest.py | 3 +- 16 files changed, 1138 insertions(+), 1061 deletions(-) create mode 100644 medusa/updater/__init__.py create mode 100644 medusa/updater/docker_updater.py create mode 100644 medusa/updater/github_updater.py create mode 100644 medusa/updater/source_updater.py create mode 100644 medusa/updater/update_manager.py create mode 100644 medusa/updater/version_checker.py delete mode 100644 medusa/version_checker.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a9735ea4bf..241ee237ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ #### Improvements #### Fixes -- Fixed saving newznab provider api key ([#5918](https://github.com/pymedusa/Medusa/pull/5918)) +- Fixed saving newznab provider API key ([#5918](https://github.com/pymedusa/Medusa/pull/5918)) +- Fixed permanent Docker update message ([#6018](https://github.com/pymedusa/Medusa/pull/6018)) ----- diff --git a/medusa/__main__.py b/medusa/__main__.py index ecc268cba9..6a103b2b1e 100755 --- a/medusa/__main__.py +++ b/medusa/__main__.py @@ -67,7 +67,7 @@ from medusa import ( app, auto_post_processor, cache, db, event_queue, exception_handler, helpers, logger as app_logger, metadata, name_cache, naming, network_timezones, providers, - scheduler, show_queue, show_updater, subtitles, torrent_checker, trakt_checker, version_checker + scheduler, show_queue, show_updater, subtitles, torrent_checker, trakt_checker ) from medusa.common import SD, SKIPPED, WANTED from medusa.config import ( @@ -90,6 +90,7 @@ from medusa.system.shutdown import Shutdown from medusa.themes import read_themes from medusa.tv import Series +from medusa.updater.version_checker import CheckVersion logger = logging.getLogger(__name__) @@ -987,9 +988,9 @@ def initialize(self, console_logging=True): pass if app.VERSION_NOTIFY: - updater = version_checker.CheckVersion().updater + updater = CheckVersion().updater if updater: - app.APP_VERSION = updater.get_cur_version() + app.APP_VERSION = updater.current_version app.MAJOR_DB_VERSION, app.MINOR_DB_VERSION = db.DBConnection().checkDBVersion() @@ -1128,7 +1129,7 @@ def initialize(self, console_logging=True): # initialize schedulers # updaters - app.version_check_scheduler = scheduler.Scheduler(version_checker.CheckVersion(), + app.version_check_scheduler = scheduler.Scheduler(CheckVersion(), cycleTime=datetime.timedelta(hours=app.UPDATE_FREQUENCY), threadName='CHECKVERSION', silent=False) diff --git a/medusa/config.py b/medusa/config.py index df815c978d..089e7fea51 100644 --- a/medusa/config.py +++ b/medusa/config.py @@ -32,7 +32,7 @@ from medusa.helper.common import try_int from medusa.helpers.utils import split_and_strip from medusa.logger.adapters.style import BraceAdapter -from medusa.version_checker import CheckVersion +from medusa.updater.version_checker import CheckVersion from requests.compat import urlsplit diff --git a/medusa/github_client.py b/medusa/github_client.py index 106aa8d126..0ff05f961a 100644 --- a/medusa/github_client.py +++ b/medusa/github_client.py @@ -102,3 +102,21 @@ def get_github_repo(organization, repo, gh=None): except github.GithubException as e: logger.debug('Unable to contact Github: {ex!r}', ex=e) raise + + +def get_latest_release(organization, repo, gh=None): + """Return the latest release of a repository. + + :param repo: + :type repo: string + :param gh: + :type gh: Github + :return: + :rtype github.GitRelease.GitRelease + """ + try: + gh = gh or github.MainClass.Github(**OPTIONS) + return gh.get_organization(organization).get_repo(repo).get_latest_release() + except github.GithubException as e: + logger.debug('Unable to contact Github: {ex!r}', ex=e) + raise diff --git a/medusa/server/api/v1/core.py b/medusa/server/api/v1/core.py index 1474e44c9e..1f6cc31a47 100644 --- a/medusa/server/api/v1/core.py +++ b/medusa/server/api/v1/core.py @@ -59,7 +59,7 @@ from medusa.show.show import Show from medusa.system.restart import Restart from medusa.system.shutdown import Shutdown -from medusa.version_checker import CheckVersion +from medusa.updater.version_checker import CheckVersion from requests.compat import unquote_plus @@ -1421,7 +1421,7 @@ def run(self): 'commit': check_version.updater.get_newest_commit_hash(), 'version': check_version.updater.get_newest_version(), }, - 'commits_offset': check_version.updater.get_num_commits_behind(), + 'commits_offset': check_version.updater.commits_behind, 'needs_update': needs_update, } diff --git a/medusa/server/web/core/error_logs.py b/medusa/server/web/core/error_logs.py index 75b8b5d635..5fdba4c778 100644 --- a/medusa/server/web/core/error_logs.py +++ b/medusa/server/web/core/error_logs.py @@ -13,7 +13,7 @@ from medusa.issue_submitter import IssueSubmitter from medusa.logger import filter_logline, read_loglines from medusa.server.web.core.base import PageTemplate, WebRoot -from medusa.version_checker import CheckVersion +from medusa.updater.version_checker import CheckVersion from six import text_type diff --git a/medusa/server/web/home/handler.py b/medusa/server/web/home/handler.py index 6f15097943..0dbfeb044c 100644 --- a/medusa/server/web/home/handler.py +++ b/medusa/server/web/home/handler.py @@ -105,7 +105,7 @@ from medusa.system.restart import Restart from medusa.system.shutdown import Shutdown from medusa.tv.series import Series, SeriesIdentifier -from medusa.version_checker import CheckVersion +from medusa.updater.version_checker import CheckVersion from requests.compat import ( quote_plus, diff --git a/medusa/updater/__init__.py b/medusa/updater/__init__.py new file mode 100644 index 0000000000..9bad5790a5 --- /dev/null +++ b/medusa/updater/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/medusa/updater/docker_updater.py b/medusa/updater/docker_updater.py new file mode 100644 index 0000000000..5aa9f19a0f --- /dev/null +++ b/medusa/updater/docker_updater.py @@ -0,0 +1,54 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import logging + +from medusa import app +from medusa.logger.adapters.style import BraceAdapter +from medusa.updater.source_updater import SourceUpdateManager + + +log = BraceAdapter(logging.getLogger(__name__)) +log.logger.addHandler(logging.NullHandler()) + + +class DockerUpdateManager(SourceUpdateManager): + def __init__(self): + super(DockerUpdateManager, self).__init__() + + def __str__(self): + return 'Docker Updater' + + def need_update(self): + if self.branch != self._find_installed_branch(): + log.debug(u'Branch checkout: {0}->{1}', self._find_installed_branch(), self.branch) + return True + + if self.branch == 'master' and not self.is_latest_version(): + self._set_update_text() + return True + + return False + + def can_update(self): + """Whether or not the update can be performed. + + :return: + :rtype: bool + """ + return False + + def _set_update_text(self): + """Set an update text, when running in a Docker container.""" + log.debug('There is an update available, Medusa is running in a Docker container,' + ' so auto updating is disabled.') + + url = 'http://github.com/' + self.github_org + '/' + self.github_repo + '/releases' + newest_text = 'There is a newer version available' + newest_text += ' (' + self.newest_version + ') — Pull the latest Docker image and rebuild your container to update' + app.NEWEST_VERSION_STRING = newest_text + + def update(self): + """Download the latest version.""" + return False diff --git a/medusa/updater/github_updater.py b/medusa/updater/github_updater.py new file mode 100644 index 0000000000..eafcb03b04 --- /dev/null +++ b/medusa/updater/github_updater.py @@ -0,0 +1,381 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import logging +import os +import platform +import re +import subprocess + +from medusa import app, notifiers +from medusa.logger.adapters.style import BraceAdapter +from medusa.updater.update_manager import UpdateManager + +from six import text_type + + +ERROR_MESSAGE = ('Unable to find your git executable. Set git executable path in Advanced Settings ' + 'OR shutdown application and delete your .git folder and run from source to enable updates.') + +log = BraceAdapter(logging.getLogger(__name__)) +log.logger.addHandler(logging.NullHandler()) + + +class GitUpdateManager(UpdateManager): + def __init__(self): + super(GitUpdateManager, self).__init__() + self._git_path = self._find_working_git() + self.github_org = self.get_github_org() + self.github_repo = self.get_github_repo() + self.branch = self._find_installed_branch() + + self._cur_commit_hash = None + self._newest_commit_hash = None + self._num_commits_behind = 0 + self._num_commits_ahead = 0 + + def __str__(self): + return 'GitHub Updater' + + @property + def current_commit_hash(self): + return self._cur_commit_hash + + @property + def newest_commit_hash(self): + return self._newest_commit_hash + + @property + def current_version(self): + self.update_commit_hash() + cur_version = self._run_git(self._git_path, 'describe --tags --abbrev=0 {0}'.format( + self._cur_commit_hash))[0] + return cur_version.lstrip('v') + + @property + def newest_version(self): + self.update_newest_commit_hash() + new_version = self._run_git(self._git_path, 'describe --tags --abbrev=0 {0}'.format( + self._newest_commit_hash))[0] + return new_version.lstrip('v') + + @property + def commits_behind(self): + return self._num_commits_behind + + @property + def commits_ahead(self): + return self._num_commits_ahead + + def _find_working_git(self): + test_cmd = 'version' + + if app.GIT_PATH: + main_git = '"' + app.GIT_PATH + '"' + else: + main_git = 'git' + + log.debug(u'Checking if we can use git commands: {0} {1}', main_git, test_cmd) + _, _, exit_status = self._run_git(main_git, test_cmd) + + if exit_status == 0: + log.debug(u'Using: {0}', main_git) + return main_git + else: + log.debug(u'Not using: {0}', main_git) + + # trying alternatives + alternative_git = [] + + # osx people who start sr from launchd have a broken path, so try a hail-mary attempt for them + if platform.system().lower() == 'darwin': + alternative_git.append('/usr/local/git/bin/git') + + if platform.system().lower() == 'windows': + if main_git != main_git.lower(): + alternative_git.append(main_git.lower()) + + if alternative_git: + log.debug(u'Trying known alternative git locations') + + for cur_git in alternative_git: + log.debug(u'Checking if we can use git commands: {0} {1}', cur_git, test_cmd) + _, _, exit_status = self._run_git(cur_git, test_cmd) + + if exit_status == 0: + log.debug(u'Using: {0}', cur_git) + return cur_git + else: + log.debug(u'Not using: {0}', cur_git) + + def _run_git(self, git_path, args): + output = err = exit_status = None + + if not git_path: + git_path = self._find_working_git() + if git_path: + self._git_path = git_path + else: + # Warn user only if he has version check enabled + if app.VERSION_NOTIFY: + log.warning(u"No git specified, can't use git commands") + app.NEWEST_VERSION_STRING = ERROR_MESSAGE + exit_status = 1 + return output, err, exit_status + + # If we have a valid git remove the git warning + # String will be updated as soon we check github + app.NEWEST_VERSION_STRING = None + cmd = git_path + ' ' + args + + try: + log.debug(u'Executing {cmd} with your shell in {dir}', {'cmd': cmd, 'dir': app.PROG_DIR}) + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + shell=True, cwd=app.PROG_DIR) + output, err = p.communicate() + exit_status = p.returncode + + # Convert bytes to string in python3 + if isinstance(output, (bytes, bytearray)): + output = output.decode('utf-8') + + if output: + output = output.strip() + + except OSError: + log.info(u"Command {cmd} didn't work", {'cmd': cmd}) + exit_status = 1 + + if exit_status == 0: + log.debug(u'{cmd} : returned successful', {'cmd': cmd}) + exit_status = 0 + + elif exit_status == 1: + if output: + if 'stash' in output: + log.warning(u"Enable 'git reset' in settings or stash your changes in local files") + else: + log.warning(u'{cmd} returned : {output}', {'cmd': cmd, 'output': output}) + else: + log.warning(u'{cmd} returned no data', {'cmd': cmd}) + exit_status = 1 + + elif exit_status == 128 or 'fatal:' in output or err: + log.warning(u'{cmd} returned : {output}', {'cmd': cmd, 'output': output}) + exit_status = 128 + + else: + log.warning(u'{cmd} returned : {output}. Treat as error for now', {'cmd': cmd, 'output': output}) + exit_status = 1 + + return output, err, exit_status + + def update_commit_hash(self): + """Attempt to set the hash of the currently installed version of the application. + + Uses git to get commit version. + """ + output, _, exit_status = self._run_git(self._git_path, 'rev-parse HEAD') + + if exit_status == 0 and output: + cur_commit_hash = output.strip() + if not re.match('^[a-z0-9]+$', cur_commit_hash): + log.warning(u"Output doesn't look like a hash, not using it") + return False + + self._cur_commit_hash = cur_commit_hash + app.CUR_COMMIT_HASH = cur_commit_hash + return True + + return False + + def update_newest_commit_hash(self): + # update remote origin url + self.update_remote_origin() + + # get all new info from github + output, _, exit_status = self._run_git(self._git_path, 'fetch --prune %s' % app.GIT_REMOTE) + if not exit_status == 0: + log.warning(u"Unable to contact github, can't check for update") + return False + + # get latest commit_hash from remote + output, _, exit_status = self._run_git(self._git_path, 'rev-parse --verify --quiet "@{upstream}"') + + if exit_status == 0 and output: + cur_commit_hash = output.strip() + if not re.match('^[a-z0-9]+$', cur_commit_hash): + log.debug(u"Output doesn't look like a hash, not using it") + return False + else: + self._newest_commit_hash = cur_commit_hash + return True + else: + log.debug(u"git didn't return newest commit hash") + return False + + def _find_installed_branch(self): + branch_info, _, exit_status = self._run_git(self._git_path, 'symbolic-ref -q HEAD') + if exit_status == 0 and branch_info: + branch = branch_info.strip().replace('refs/heads/', '', 1) + if branch: + app.BRANCH = branch + return branch + return '' + + def check_for_update(self): + """Use git commands to check if there is a newer version that the provided commit hash.""" + self.update_commit_hash() + self.update_newest_commit_hash() + + # get number of commits behind and ahead (option --count not supported git < 1.7.2) + output, _, exit_status = self._run_git(self._git_path, 'rev-list --left-right "@{upstream}"...HEAD') + if exit_status == 0 and output: + try: + self._num_commits_behind = int(output.count('<')) + self._num_commits_ahead = int(output.count('>')) + except Exception: + log.debug(u"git didn't return numbers for behind and ahead, not using it") + return False + + log.debug(u'cur_commit = {0}, newest_commit = {1}, num_commits_behind = {2}, num_commits_ahead = {3}', + self._cur_commit_hash, self._newest_commit_hash, self._num_commits_behind, self._num_commits_ahead) + + def need_update(self): + if self.branch != self._find_installed_branch(): + log.debug(u'Branch checkout: {0}->{1}', self._find_installed_branch(), self.branch) + return True + + try: + self.check_for_update() + except Exception as e: + log.warning(u"Unable to contact github, can't check for update: {0!r}", e) + return False + + if self._num_commits_behind > 0 or self._num_commits_ahead > 0: + self._set_update_text() + return True + + return False + + def _set_update_text(self): + if self._num_commits_behind > 0 or self._is_hard_reset_allowed(): + base_url = 'http://github.com/' + self.github_org + '/' + self.github_repo + if self._newest_commit_hash: + url = base_url + '/compare/' + self._cur_commit_hash + '...' + self._newest_commit_hash + else: + url = base_url + '/commits/' + + newest_text = 'There is a newer version available ' + newest_text += " (you're " + text_type(self._num_commits_behind) + ' commit' + if self._num_commits_behind > 1: + newest_text += 's' + newest_text += ' behind' + if self._num_commits_ahead > 0: + newest_text += ' and {ahead} commit{s} ahead'.format(ahead=self._num_commits_ahead, + s='s' if self._num_commits_ahead > 1 else '') + newest_text += ') — Update Now' + + elif self._num_commits_ahead > 0: + newest_text = u'Local branch is ahead of {0}. Automatic update not possible'.format(self.branch) + log.warning(newest_text) + else: + return + + app.NEWEST_VERSION_STRING = newest_text + + def can_update(self): + """Return whether update can be executed. + + :return: + :rtype: bool + """ + return self._num_commits_ahead <= 0 or self._is_hard_reset_allowed() + + def update(self): + """Call git pull origin in order to update the application. + + Returns a bool depending on the call's success. + """ + # update remote origin url + self.update_remote_origin() + + # remove untracked files and performs a hard reset on git branch to avoid update issues + if self._is_hard_reset_allowed(): + self.reset() + + # Executing git clean before updating + self.clean() + + if self.branch == self._find_installed_branch(): + _, _, exit_status = self._run_git(self._git_path, 'pull -f %s %s' % (app.GIT_REMOTE, self.branch)) + else: + _, _, exit_status = self._run_git(self._git_path, 'checkout -f ' + self.branch) + + # Executing git clean after updating + self.clean() + + if exit_status == 0: + self.update_commit_hash() + # Notify update successful + if app.NOTIFY_ON_UPDATE: + try: + notifiers.notify_git_update(app.CUR_COMMIT_HASH or '') + except Exception: + log.debug(u'Unable to send update notification. Continuing the update process') + return True + + return False + + @staticmethod + def _is_hard_reset_allowed(): + """Return whether git hard reset is allowed or not. + + :return: + :rtype: bool + """ + return app.GIT_RESET and (not app.GIT_RESET_BRANCHES or + app.BRANCH in app.GIT_RESET_BRANCHES) + + def clean(self): + """Call git clean to remove all untracked files. + + It only affects source folders and libX and extX folders, + to prevent deleting untracked user data not known by .gitignore + + :return: + :rtype: int + """ + # Fixes: goo.gl/tr8Awf - to be removed in the next release + root_dir = os.path.basename(app.PROG_DIR) + helper_folder = os.path.join(root_dir, 'helper') + helpers_folder = os.path.join(root_dir, 'helpers') + + folders = (app.LIB_FOLDER, app.LIB2_FOLDER, app.LIB3_FOLDER, app.EXT_FOLDER, + app.EXT2_FOLDER, app.EXT3_FOLDER, app.SRC_FOLDER, app.STATIC_FOLDER, + helper_folder, helpers_folder) + app.LEGACY_SRC_FOLDERS + _, _, exit_status = self._run_git(self._git_path, 'clean -d -f -x {0}'.format(' '.join(folders))) + + return exit_status + + def reset(self): + """Call git reset --hard to perform a hard reset.""" + _, _, exit_status = self._run_git(self._git_path, 'reset --hard {0}/{1}'.format(app.GIT_REMOTE, app.BRANCH)) + if exit_status == 0: + return True + + def list_remote_branches(self): + # update remote origin url + self.update_remote_origin() + app.BRANCH = self._find_installed_branch() + + branches, _, exit_status = self._run_git(self._git_path, 'ls-remote --heads %s' % app.GIT_REMOTE) + if exit_status == 0 and branches: + if branches: + return re.findall(r'refs/heads/(.*)', branches) + return [] + + def update_remote_origin(self): + self._run_git(self._git_path, 'config remote.%s.url %s' % (app.GIT_REMOTE, app.GIT_REMOTE_URL)) + self._run_git(self._git_path, 'config remote.%s.pushurl %s' % (app.GIT_REMOTE, app.GIT_REMOTE_URL)) diff --git a/medusa/updater/source_updater.py b/medusa/updater/source_updater.py new file mode 100644 index 0000000000..4fd8f39642 --- /dev/null +++ b/medusa/updater/source_updater.py @@ -0,0 +1,264 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import logging +import os +import shutil +import stat +import tarfile + +from github import GithubException + +from medusa import app, helpers, notifiers +from medusa.common import VERSION +from medusa.github_client import get_github_repo, get_latest_release +from medusa.logger.adapters.style import BraceAdapter +from medusa.session.core import MedusaSafeSession +from medusa.updater.update_manager import UpdateManager + +from six import text_type + + +log = BraceAdapter(logging.getLogger(__name__)) +log.logger.addHandler(logging.NullHandler()) + + +class SourceUpdateManager(UpdateManager): + def __init__(self): + super(SourceUpdateManager, self).__init__() + self.github_org = self.get_github_org() + self.github_repo = self.get_github_repo() + + self.branch = app.BRANCH + if app.BRANCH == '': + self.branch = self._find_installed_branch() + + self._cur_commit_hash = app.CUR_COMMIT_HASH + self._newest_commit_hash = None + self._num_commits_behind = 0 + self._num_commits_ahead = 0 + + self.session = MedusaSafeSession() + + def __str__(self): + return 'Source Updater' + + @staticmethod + def _find_installed_branch(): + return app.CUR_COMMIT_BRANCH if app.CUR_COMMIT_BRANCH else 'master' + + @property + def current_commit_hash(self): + return self._cur_commit_hash + + @property + def newest_commit_hash(self): + return self._newest_commit_hash + + @property + def current_version(self): + return VERSION + + @property + def newest_version(self): + latest_release = get_latest_release(self.github_org, self.github_repo) + return latest_release.tag_name.lstrip('v') + + @property + def commits_behind(self): + return self._num_commits_behind + + @property + def commits_ahead(self): + return self._num_commits_ahead + + def need_update(self): + if self.branch != self._find_installed_branch(): + log.debug(u'Branch checkout: {0}->{1}', self._find_installed_branch(), self.branch) + return True + + # need this to run first to set self._newest_commit_hash + try: + self.check_for_update() + except Exception as error: + log.warning(u"Unable to contact github, can't check for update: {0!r}", error) + return False + + # This will be used until the first update + if self.branch == 'master' and not self._cur_commit_hash: + if self.is_latest_version(): + app.CUR_COMMIT_HASH = self._newest_commit_hash + app.CUR_COMMIT_BRANCH = self.branch + return False + else: + self._set_update_text() + return True + + elif self._num_commits_behind > 0 or self._num_commits_ahead > 0: + self._set_update_text() + return True + + return False + + def can_update(self): + """Whether or not the update can be performed. + + :return: + :rtype: bool + """ + return True + + def check_for_update(self): + """Use pygithub to ask github if there is a newer version.. + + If there is a newer version it sets application's version text. + + commit_hash: hash that we're checking against + """ + gh = get_github_repo(app.GIT_ORG, app.GIT_REPO) + + # try to get the newest commit hash and commits by comparing branch and current commit + if self._cur_commit_hash: + try: + branch_compared = gh.compare(base=self.branch, head=self._cur_commit_hash) + self._newest_commit_hash = branch_compared.base_commit.sha + self._num_commits_behind = branch_compared.behind_by + self._num_commits_ahead = branch_compared.ahead_by + except Exception: + self._newest_commit_hash = None + self._num_commits_behind = 0 + self._num_commits_ahead = 0 + + # fall back and iterate over last 100 (items per page in gh_api) commits + if not self._newest_commit_hash: + for curCommit in gh.get_commits(): + if not self._newest_commit_hash: + self._newest_commit_hash = curCommit.sha + if not self._cur_commit_hash: + break + + if curCommit.sha == self._cur_commit_hash: + break + + # when _cur_commit_hash doesn't match anything _num_commits_behind == 100 + self._num_commits_behind += 1 + + log.debug(u'cur_commit = {0}, newest_commit = {1}, num_commits_behind = {2}', + self._cur_commit_hash, self._newest_commit_hash, self._num_commits_behind) + + def _set_update_text(self): + if self._num_commits_behind > 0: + base_url = 'http://github.com/' + self.github_org + '/' + self.github_repo + if self._newest_commit_hash: + url = base_url + '/compare/' + self._cur_commit_hash + '...' + self._newest_commit_hash + else: + url = base_url + '/commits/' + + newest_text = 'There is a newer version available' + newest_text += " (you're " + text_type(self._num_commits_behind) + ' commit' + if self._num_commits_behind > 1: + newest_text += 's' + newest_text += ' behind) — Update Now' + else: + url = 'http://github.com/' + self.github_org + '/' + self.github_repo + '/releases' + newest_text = 'There is a newer version available' + newest_text += ' (' + self.newest_version + ') — Update Now' + + app.NEWEST_VERSION_STRING = newest_text + + def update(self): + """Download the latest source tarball from github and installs it over the existing version.""" + tar_download_url = 'http://github.com/' + self.github_org + '/' + self.github_repo + '/tarball/' + self.branch + + try: + # prepare the update dir + app_update_dir = os.path.join(app.PROG_DIR, u'medusa-update') + + if os.path.isdir(app_update_dir): + log.info(u'Clearing out update folder {0!r} before extracting', app_update_dir) + shutil.rmtree(app_update_dir) + + log.info(u'Clearing update folder {0!r} before extracting', app_update_dir) + os.makedirs(app_update_dir) + + # retrieve file + log.info(u'Downloading update from {0!r}', tar_download_url) + tar_download_path = os.path.join(app_update_dir, u'medusa-update.tar') + helpers.download_file(tar_download_url, tar_download_path, session=self.session) + + if not os.path.isfile(tar_download_path): + log.warning(u"Unable to retrieve new version from {0!r}, can't update", tar_download_url) + return False + + if not tarfile.is_tarfile(tar_download_path): + log.warning(u"Retrieved version from {0!r} is corrupt, can't update", tar_download_url) + return False + + # extract to medusa-update dir + log.info(u'Extracting file {0}', tar_download_path) + tar = tarfile.open(tar_download_path) + tar.extractall(app_update_dir) + tar.close() + + # delete .tar.gz + log.info(u'Deleting file {0}', tar_download_path) + os.remove(tar_download_path) + + # find update dir name + update_dir_contents = [x for x in os.listdir(app_update_dir) if + os.path.isdir(os.path.join(app_update_dir, x))] + if len(update_dir_contents) != 1: + log.warning(u'Invalid update data, update failed: {0}', update_dir_contents) + return False + content_dir = os.path.join(app_update_dir, update_dir_contents[0]) + + # walk temp folder and move files to main folder + log.info(u'Moving files from {0} to {1}', content_dir, app.PROG_DIR) + for dirname, _, filenames in os.walk(content_dir): # @UnusedVariable + dirname = dirname[len(content_dir) + 1:] + for curfile in filenames: + old_path = os.path.join(content_dir, dirname, curfile) + new_path = os.path.join(app.PROG_DIR, dirname, curfile) + + # Avoid DLL access problem on WIN32/64 + # These files needing to be updated manually + # or find a way to kill the access from memory + extension = os.path.splitext(curfile)[1] + if extension == '.dll': + try: + log.debug(u'Special handling for {0}', curfile) + os.chmod(new_path, stat.S_IWRITE) + os.remove(new_path) + os.renames(old_path, new_path) + except Exception as e: + log.debug(u'Unable to update {0}: {1!r}', new_path, e) + os.remove(old_path) # Trash the updated file without moving in new path + continue + + if os.path.isfile(new_path): + os.remove(new_path) + os.renames(old_path, new_path) + + app.CUR_COMMIT_HASH = self._newest_commit_hash + app.CUR_COMMIT_BRANCH = self.branch + + except Exception as e: + log.exception(u'Error while trying to update: {0}', e) + return False + + # Notify update successful + try: + notifiers.notify_git_update(app.CUR_COMMIT_HASH or '') + except Exception: + log.debug(u'Unable to send update notification. Continuing the update process') + return True + + @staticmethod + def list_remote_branches(): + try: + gh = get_github_repo(app.GIT_ORG, app.GIT_REPO) + return [x.name for x in gh.get_branches() if x] + except GithubException as error: + log.warning(u"Unable to contact github, can't check for update: {0!r}", error) + return [] diff --git a/medusa/updater/update_manager.py b/medusa/updater/update_manager.py new file mode 100644 index 0000000000..787ce2376c --- /dev/null +++ b/medusa/updater/update_manager.py @@ -0,0 +1,44 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import logging +from distutils.version import LooseVersion + +from medusa import app +from medusa.logger.adapters.style import BraceAdapter + +from six import text_type + + +log = BraceAdapter(logging.getLogger(__name__)) +log.logger.addHandler(logging.NullHandler()) + + +class UpdateManager(object): + + @staticmethod + def get_github_org(): + return app.GIT_ORG + + @staticmethod + def get_github_repo(): + return app.GIT_REPO + + @staticmethod + def get_update_url(): + return app.WEB_ROOT + '/home/update/?pid=' + text_type(app.PID) + + def current_version(self): + """Get the current verion of the app.""" + raise NotImplementedError + + def newest_version(self): + """Get the newest verion of the app.""" + raise NotImplementedError + + def is_latest_version(self): + """Compare the current installed version with the remote version.""" + if LooseVersion(self.newest_version) > LooseVersion(self.current_version): + return False + return True diff --git a/medusa/updater/version_checker.py b/medusa/updater/version_checker.py new file mode 100644 index 0000000000..686ab95c39 --- /dev/null +++ b/medusa/updater/version_checker.py @@ -0,0 +1,357 @@ +# coding=utf-8 + +from __future__ import unicode_literals + +import datetime +import logging +import os +import re +import time +from builtins import object +from builtins import str +from logging import DEBUG, WARNING + +from medusa import app, db, helpers, ui +from medusa.logger.adapters.style import BraceAdapter +from medusa.session.core import MedusaSafeSession +from medusa.updater.docker_updater import DockerUpdateManager +from medusa.updater.github_updater import GitUpdateManager +from medusa.updater.source_updater import SourceUpdateManager + + +log = BraceAdapter(logging.getLogger(__name__)) +log.logger.addHandler(logging.NullHandler()) + + +class CheckVersion(object): + """Version check class meant to run as a thread object with the sr scheduler.""" + + def __init__(self): + self.amActive = False + self.updater = self.find_install_type() + self.session = MedusaSafeSession() + + def run(self, force=False): + + self.amActive = True + + # Update remote branches and store in app.GIT_REMOTE_BRANCHES + self.list_remote_branches() + + if self.updater: + # set current branch version + app.BRANCH = self.get_branch() + + if self.check_for_new_version(force): + if app.AUTO_UPDATE: + log.info(u'New update found, starting auto-updater ...') + ui.notifications.message('New update found, starting auto-updater') + if self.run_backup_if_safe(): + if app.version_check_scheduler.action.update(): + log.info(u'Update was successful!') + ui.notifications.message('Update was successful') + app.events.put(app.events.SystemEvent.RESTART) + else: + log.info(u'Update failed!') + ui.notifications.message('Update failed!') + + self.check_for_new_news(force) + + self.amActive = False + + def run_backup_if_safe(self): + return self.safe_to_update() is True and self._runbackup() is True + + def _runbackup(self): + # Do a system backup before update + log.info(u'Config backup in progress...') + ui.notifications.message('Backup', 'Config backup in progress...') + try: + backupDir = os.path.join(app.DATA_DIR, app.BACKUP_DIR) + if not os.path.isdir(backupDir): + os.mkdir(backupDir) + + if self._keeplatestbackup(backupDir) and self._backup(backupDir): + log.info(u'Config backup successful, updating...') + ui.notifications.message('Backup', 'Config backup successful, updating...') + return True + else: + log.warning(u'Config backup failed, aborting update') + ui.notifications.message('Backup', 'Config backup failed, aborting update') + return False + except Exception as e: + log.error(u'Update: Config backup failed. Error: {0!r}', e) + ui.notifications.message('Backup', 'Config backup failed, aborting update') + return False + + @staticmethod + def _keeplatestbackup(backupDir=None): + if not backupDir: + return False + + import glob + files = glob.glob(os.path.join(backupDir, '*.zip')) + if not files: + return True + + now = time.time() + newest = files[0], now - os.path.getctime(files[0]) + for f in files[1:]: + age = now - os.path.getctime(f) + if age < newest[1]: + newest = f, age + files.remove(newest[0]) + + for f in files: + os.remove(f) + + return True + + # TODO: Merge with backup in helpers + @staticmethod + def _backup(backupDir=None): + if not backupDir: + return False + source = [ + os.path.join(app.DATA_DIR, app.APPLICATION_DB), + app.CONFIG_FILE, + os.path.join(app.DATA_DIR, app.FAILED_DB), + os.path.join(app.DATA_DIR, app.CACHE_DB) + ] + target = os.path.join(backupDir, app.BACKUP_FILENAME.format(timestamp=time.strftime('%Y%m%d%H%M%S'))) + + for (path, dirs, files) in os.walk(app.CACHE_DIR, topdown=True): + for dirname in dirs: + if path == app.CACHE_DIR and dirname not in ['images']: + dirs.remove(dirname) + for filename in files: + source.append(os.path.join(path, filename)) + + return helpers.backup_config_zip(source, target, app.DATA_DIR) + + def safe_to_update(self): + + def db_safe(self): + message = { + 'equal': { + 'type': DEBUG, + 'text': u'We can proceed with the update. New update has same DB version'}, + 'upgrade': { + 'type': WARNING, + 'text': u"We can't proceed with the update. New update has a new DB version. Please manually update"}, + 'downgrade': { + 'type': WARNING, + 'text': u"We can't proceed with the update. New update has a old DB version. It's not possible to downgrade"}, + } + try: + result = self.getDBcompare() + if result in message: + log.log(message[result]['type'], message[result]['text']) # unpack the result message into a log entry + else: + log.warning(u"We can't proceed with the update. Unable to check remote DB version. Error: {0}", result) + return result in ['equal'] # add future True results to the list + except Exception as error: + log.error(u"We can't proceed with the update. Unable to compare DB version. Error: {0!r}", error) + return False + + def postprocessor_safe(): + if not app.auto_post_processor_scheduler.action.amActive: + log.debug(u'We can proceed with the update. Post-Processor is not running') + return True + else: + log.debug(u"We can't proceed with the update. Post-Processor is running") + return False + + def showupdate_safe(): + if not app.show_update_scheduler.action.amActive: + log.debug(u'We can proceed with the update. Shows are not being updated') + return True + else: + log.debug(u"We can't proceed with the update. Shows are being updated") + return False + + db_safe = db_safe(self) + postprocessor_safe = postprocessor_safe() + showupdate_safe = showupdate_safe() + + if db_safe and postprocessor_safe and showupdate_safe: + log.debug(u'Proceeding with auto update') + return True + else: + log.debug(u'Auto update aborted') + return False + + def getDBcompare(self): + """ + Compare the current DB version with the new branch version. + + :return: 'upgrade', 'equal', or 'downgrade' + """ + try: + self.updater.need_update() + cur_hash = str(self.updater.get_newest_commit_hash()) + assert len(cur_hash) == 40, 'Commit hash wrong length: {length} hash: {hash}'.format( + length=len(cur_hash), hash=cur_hash) + + check_url = 'http://rawcdn.githack.com/{org}/{repo}/{commit}/medusa/databases/main_db.py'.format( + org=app.GIT_ORG, repo=app.GIT_REPO, commit=cur_hash) + response = self.session.get(check_url) + + # Get remote DB version + match_max_db = re.search(r'MAX_DB_VERSION\s*=\s*(?P\d{2,3})', response.text) + new_branch_major_db_version = int(match_max_db.group('version')) if match_max_db else None + match_minor_db = re.search(r'CURRENT_MINOR_DB_VERSION\s*=\s*(?P\d{1,2})', response.text) + new_branch_min_db_version = int(match_minor_db.group('version')) if match_minor_db else None + + # Check local DB version + main_db_con = db.DBConnection() + cur_branch_major_db_version, cur_branch_minor_db_version = main_db_con.checkDBVersion() + + if any([cur_branch_major_db_version is None, cur_branch_minor_db_version is None, + new_branch_major_db_version is None, new_branch_min_db_version is None]): + return 'Could not compare database versions, aborting' + + if new_branch_major_db_version > cur_branch_major_db_version: + return 'upgrade' + elif new_branch_major_db_version == cur_branch_major_db_version: + if new_branch_min_db_version < cur_branch_minor_db_version: + return 'downgrade' + elif new_branch_min_db_version > cur_branch_minor_db_version: + return 'upgrade' + return 'equal' + else: + return 'downgrade' + except Exception as e: + return repr(e) + + def find_install_type(self): + """ + Determine how this copy of Medusa was installed. + + :return: type of installation. Possible values are: + 'docker': any docker build + 'git': running from source using git + 'source': running from source without git + """ + if self.runs_in_docker(): + return DockerUpdateManager() + elif os.path.isdir(os.path.join(app.PROG_DIR, u'.git')): + return GitUpdateManager() + + return SourceUpdateManager() + + def check_for_new_version(self, force=False): + """ + Check the internet for a newer version. + + :force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced + :return: bool, True for new version or False for no new version. + """ + if not self.updater or (not app.VERSION_NOTIFY and not app.AUTO_UPDATE and not force): + log.info(u'Version checking is disabled, not checking for the newest version') + app.NEWEST_VERSION_STRING = None + return False + + # checking for updates + if not app.AUTO_UPDATE: + log.info(u'Checking for updates using {0}', self.updater) + + if not self.updater.need_update(): + app.NEWEST_VERSION_STRING = None + + if force: + ui.notifications.message('No update needed') + log.info(u'No update needed') + + # no updates needed + return False + + # found updates + return self.updater.can_update() + + def check_for_new_news(self, force=False): + """ + Check GitHub for the latest news. + + :return: unicode, a copy of the news + :force: ignored + """ + # Grab a copy of the news + log.debug(u'Checking GitHub for latest news.') + response = self.session.get(app.NEWS_URL) + if not response or not response.text: + log.debug(u'Could not load news from URL: {0}', app.NEWS_URL) + return + + try: + last_read = datetime.datetime.strptime(app.NEWS_LAST_READ, '%Y-%m-%d') + except ValueError: + log.warning(u'Invalid news last read date: {0}', app.NEWS_LAST_READ) + last_read = 0 + + news = response.text + app.NEWS_UNREAD = 0 + got_latest = False + for match in re.finditer(r'^####\s*(\d{4}-\d{2}-\d{2})\s*####', news, re.M): + if not got_latest: + got_latest = True + app.NEWS_LATEST = match.group(1) + + try: + if datetime.datetime.strptime(match.group(1), '%Y-%m-%d') > last_read: + app.NEWS_UNREAD += 1 + except ValueError: + log.warning(u'Unable to match latest news date. Repository news date: {0}', match.group(1)) + pass + + return news + + def need_update(self): + if self.updater: + return self.updater.need_update() + + def update(self): + if self.updater: + # update branch with current config branch value + self.updater.branch = app.BRANCH + + # check for updates + if self.updater.need_update(): + return self.updater.update() + + def list_remote_branches(self): + if self.updater: + app.GIT_REMOTE_BRANCHES = self.updater.list_remote_branches() + return app.GIT_REMOTE_BRANCHES + + def get_branch(self): + if self.updater: + return self.updater.branch + + @staticmethod + def runs_in_docker(): + """ + Check if Medusa is run in a docker container. + + If run in a container, we don't want to use the auto update feature, but just want to inform the user + there is an update available. The user can update through getting the latest docker tag. + """ + if app.RUNS_IN_DOCKER is not None: + return app.RUNS_IN_DOCKER + + path = '/proc/{pid}/cgroup'.format(pid=os.getpid()) + try: + if not os.path.isfile(path): + return False + + with open(path) as f: + for line in f: + if re.match(r'\d+:[\w=]+:/docker(-[ce]e)?/\w+', line): + log.debug(u'Running in a docker container') + app.RUNS_IN_DOCKER = True + return True + return False + except (EnvironmentError, OSError) as error: + log.info(u'Tried to check the path {path} if we are running in a docker container, ' + u'but an error occurred: {error}', {'path': path, 'error': error}) + return False diff --git a/medusa/version_checker.py b/medusa/version_checker.py deleted file mode 100644 index a53cf8d3d8..0000000000 --- a/medusa/version_checker.py +++ /dev/null @@ -1,1048 +0,0 @@ -# coding=utf-8 -# Author: Nic Wolfe -# -# This file is part of Medusa. -# -# Medusa 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. -# -# Medusa 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 Medusa. If not, see . -from __future__ import unicode_literals - -import datetime -import logging -import os -import platform -import re -import shutil -import stat -import subprocess -import tarfile -import time -from builtins import object -from builtins import str -from logging import DEBUG, WARNING - -from github import GithubException - -from medusa import app, db, helpers, notifiers, ui -from medusa.github_client import get_github_repo -from medusa.logger.adapters.style import BraceAdapter -from medusa.session.core import MedusaSafeSession - - -ERROR_MESSAGE = ('Unable to find your git executable. Set git executable path in Advanced Settings ' - 'OR shutdown application and delete your .git folder and run from source to enable updates.') - -log = BraceAdapter(logging.getLogger(__name__)) -log.logger.addHandler(logging.NullHandler()) - - -class CheckVersion(object): - """Version check class meant to run as a thread object with the sr scheduler.""" - - def __init__(self): - self.updater = None - self.install_type = None - self.amActive = False - self.install_type = self.find_install_type() - if self.install_type == 'git': - self.updater = GitUpdateManager() - elif self.install_type == 'source': - self.updater = SourceUpdateManager() - - self.session = MedusaSafeSession() - - def run(self, force=False): - - self.amActive = True - - # Update remote branches and store in app.GIT_REMOTE_BRANCHES - self.list_remote_branches() - - if self.updater: - # set current branch version - app.BRANCH = self.get_branch() - - if self.check_for_new_version(force): - if app.AUTO_UPDATE: - log.info(u'New update found, starting auto-updater ...') - ui.notifications.message('New update found, starting auto-updater') - if self.run_backup_if_safe(): - if app.version_check_scheduler.action.update(): - log.info(u'Update was successful!') - ui.notifications.message('Update was successful') - app.events.put(app.events.SystemEvent.RESTART) - else: - log.info(u'Update failed!') - ui.notifications.message('Update failed!') - - self.check_for_new_news(force) - - self.amActive = False - - def run_backup_if_safe(self): - return self.safe_to_update() is True and self._runbackup() is True - - def _runbackup(self): - # Do a system backup before update - log.info(u'Config backup in progress...') - ui.notifications.message('Backup', 'Config backup in progress...') - try: - backupDir = os.path.join(app.DATA_DIR, app.BACKUP_DIR) - if not os.path.isdir(backupDir): - os.mkdir(backupDir) - - if self._keeplatestbackup(backupDir) and self._backup(backupDir): - log.info(u'Config backup successful, updating...') - ui.notifications.message('Backup', 'Config backup successful, updating...') - return True - else: - log.warning(u'Config backup failed, aborting update') - ui.notifications.message('Backup', 'Config backup failed, aborting update') - return False - except Exception as e: - log.error(u'Update: Config backup failed. Error: {0!r}', e) - ui.notifications.message('Backup', 'Config backup failed, aborting update') - return False - - @staticmethod - def _keeplatestbackup(backupDir=None): - if not backupDir: - return False - - import glob - files = glob.glob(os.path.join(backupDir, '*.zip')) - if not files: - return True - - now = time.time() - newest = files[0], now - os.path.getctime(files[0]) - for f in files[1:]: - age = now - os.path.getctime(f) - if age < newest[1]: - newest = f, age - files.remove(newest[0]) - - for f in files: - os.remove(f) - - return True - - # TODO: Merge with backup in helpers - @staticmethod - def _backup(backupDir=None): - if not backupDir: - return False - source = [ - os.path.join(app.DATA_DIR, app.APPLICATION_DB), - app.CONFIG_FILE, - os.path.join(app.DATA_DIR, app.FAILED_DB), - os.path.join(app.DATA_DIR, app.CACHE_DB) - ] - target = os.path.join(backupDir, app.BACKUP_FILENAME.format(timestamp=time.strftime('%Y%m%d%H%M%S'))) - - for (path, dirs, files) in os.walk(app.CACHE_DIR, topdown=True): - for dirname in dirs: - if path == app.CACHE_DIR and dirname not in ['images']: - dirs.remove(dirname) - for filename in files: - source.append(os.path.join(path, filename)) - - return helpers.backup_config_zip(source, target, app.DATA_DIR) - - def safe_to_update(self): - - def db_safe(self): - message = { - 'equal': { - 'type': DEBUG, - 'text': u'We can proceed with the update. New update has same DB version'}, - 'upgrade': { - 'type': WARNING, - 'text': u"We can't proceed with the update. New update has a new DB version. Please manually update"}, - 'downgrade': { - 'type': WARNING, - 'text': u"We can't proceed with the update. New update has a old DB version. It's not possible to downgrade"}, - } - try: - result = self.getDBcompare() - if result in message: - log.log(message[result]['type'], message[result]['text']) # unpack the result message into a log entry - else: - log.warning(u"We can't proceed with the update. Unable to check remote DB version. Error: {0}", result) - return result in ['equal'] # add future True results to the list - except Exception as error: - log.error(u"We can't proceed with the update. Unable to compare DB version. Error: {0!r}", error) - return False - - def postprocessor_safe(): - if not app.auto_post_processor_scheduler.action.amActive: - log.debug(u'We can proceed with the update. Post-Processor is not running') - return True - else: - log.debug(u"We can't proceed with the update. Post-Processor is running") - return False - - def showupdate_safe(): - if not app.show_update_scheduler.action.amActive: - log.debug(u'We can proceed with the update. Shows are not being updated') - return True - else: - log.debug(u"We can't proceed with the update. Shows are being updated") - return False - - db_safe = db_safe(self) - postprocessor_safe = postprocessor_safe() - showupdate_safe = showupdate_safe() - - if db_safe and postprocessor_safe and showupdate_safe: - log.debug(u'Proceeding with auto update') - return True - else: - log.debug(u'Auto update aborted') - return False - - def getDBcompare(self): - """ - Compare the current DB version with the new branch version. - - :return: 'upgrade', 'equal', or 'downgrade' - """ - try: - self.updater.need_update() - cur_hash = str(self.updater.get_newest_commit_hash()) - assert len(cur_hash) == 40, 'Commit hash wrong length: {length} hash: {hash}'.format( - length=len(cur_hash), hash=cur_hash) - - check_url = 'http://rawcdn.githack.com/{org}/{repo}/{commit}/medusa/databases/main_db.py'.format( - org=app.GIT_ORG, repo=app.GIT_REPO, commit=cur_hash) - response = self.session.get(check_url) - - # Get remote DB version - match_max_db = re.search(r'MAX_DB_VERSION\s*=\s*(?P\d{2,3})', response.text) - new_branch_major_db_version = int(match_max_db.group('version')) if match_max_db else None - match_minor_db = re.search(r'CURRENT_MINOR_DB_VERSION\s*=\s*(?P\d{1,2})', response.text) - new_branch_min_db_version = int(match_minor_db.group('version')) if match_minor_db else None - - # Check local DB version - main_db_con = db.DBConnection() - cur_branch_major_db_version, cur_branch_minor_db_version = main_db_con.checkDBVersion() - - if any([cur_branch_major_db_version is None, cur_branch_minor_db_version is None, - new_branch_major_db_version is None, new_branch_min_db_version is None]): - return 'Could not compare database versions, aborting' - - if new_branch_major_db_version > cur_branch_major_db_version: - return 'upgrade' - elif new_branch_major_db_version == cur_branch_major_db_version: - if new_branch_min_db_version < cur_branch_minor_db_version: - return 'downgrade' - elif new_branch_min_db_version > cur_branch_minor_db_version: - return 'upgrade' - return 'equal' - else: - return 'downgrade' - except Exception as e: - return repr(e) - - @staticmethod - def find_install_type(): - """ - Determines how this copy of sr was installed. - - :return: type of installation. Possible values are: - 'win': any compiled windows build - 'git': running from source using git - 'source': running from source without git - """ - # check if we're a windows build - if app.BRANCH.startswith('build '): - install_type = 'win' - elif os.path.isdir(os.path.join(app.PROG_DIR, u'.git')): - install_type = 'git' - else: - install_type = 'source' - - return install_type - - def check_for_new_version(self, force=False): - """ - Check the internet for a newer version. - - :force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced - :return: bool, True for new version or False for no new version. - """ - if not self.updater or (not app.VERSION_NOTIFY and not app.AUTO_UPDATE and not force): - log.info(u'Version checking is disabled, not checking for the newest version') - app.NEWEST_VERSION_STRING = None - return False - - # checking for updates - if not app.AUTO_UPDATE: - log.info(u'Checking for updates using {0}', self.install_type.upper()) - - if not self.updater.need_update(): - app.NEWEST_VERSION_STRING = None - - if force: - ui.notifications.message('No update needed') - log.info(u'No update needed') - - # no updates needed - return False - - # found updates - self.updater.set_newest_text() - return self.updater.can_update() - - def check_for_new_news(self, force=False): - """ - Check GitHub for the latest news. - - :return: unicode, a copy of the news - :force: ignored - """ - # Grab a copy of the news - log.debug(u'Checking GitHub for latest news.') - response = self.session.get(app.NEWS_URL) - if not response or not response.text: - log.debug(u'Could not load news from URL: {0}', app.NEWS_URL) - return - - try: - last_read = datetime.datetime.strptime(app.NEWS_LAST_READ, '%Y-%m-%d') - except ValueError: - log.warning(u'Invalid news last read date: {0}', app.NEWS_LAST_READ) - last_read = 0 - - news = response.text - app.NEWS_UNREAD = 0 - got_latest = False - for match in re.finditer(r'^####\s*(\d{4}-\d{2}-\d{2})\s*####', news, re.M): - if not got_latest: - got_latest = True - app.NEWS_LATEST = match.group(1) - - try: - if datetime.datetime.strptime(match.group(1), '%Y-%m-%d') > last_read: - app.NEWS_UNREAD += 1 - except ValueError: - log.warning(u'Unable to match latest news date. Repository news date: {0}', match.group(1)) - pass - - return news - - def need_update(self): - if self.updater: - return self.updater.need_update() - - def update(self): - if self.updater: - # update branch with current config branch value - self.updater.branch = app.BRANCH - - # check for updates - if self.updater.need_update(): - return self.updater.update() - - def list_remote_branches(self): - if self.updater: - app.GIT_REMOTE_BRANCHES = self.updater.list_remote_branches() - return app.GIT_REMOTE_BRANCHES - - def get_branch(self): - if self.updater: - return self.updater.branch - - -class UpdateManager(object): - def __init__(self): - """Update manager initialization.""" - # Initialize the app.RUNS_IN_DOCKER variable - self.runs_in_docker() - - @staticmethod - def get_github_org(): - return app.GIT_ORG - - @staticmethod - def get_github_repo(): - return app.GIT_REPO - - @staticmethod - def get_update_url(): - return app.WEB_ROOT + '/home/update/?pid=' + str(app.PID) - - @staticmethod - def runs_in_docker(): - """ - Check if Medusa is run in a docker container. - - If run in a container, we don't want to use the auto update feature, but just want to inform the user - there is an update available. The user can update through getting the latest docker tag. - """ - if app.RUNS_IN_DOCKER is not None: - return app.RUNS_IN_DOCKER - - path = '/proc/{pid}/cgroup'.format(pid=os.getpid()) - try: - if not os.path.isfile(path): - return False - - with open(path) as f: - for line in f: - if re.match(r'\d+:[\w=]+:/docker(-[ce]e)?/\w+', line): - log.debug(u'Running in a docker container') - app.RUNS_IN_DOCKER = True - return True - return False - except (EnvironmentError, OSError) as error: - log.info(u'Tried to check the path {path} if we are running in a docker container, ' - u'but an error occurred: {error}', {'path': path, 'error': error}) - return False - - def set_newest_text_docker(self): - """ - Set an alternative update text, when running in a docker container. - - This method is used by the GitUpdateMananager and the SourceUpdateManager. Both should not auto update from - within the container. - """ - if app.RUNS_IN_DOCKER and (not self._cur_commit_hash or self._num_commits_behind > 0): - log.debug(u'There is an update available, Medusa is running in a docker container, so auto updating is disabled.') - app.NEWEST_VERSION_STRING = 'There is an update available: please pull the latest docker image, ' \ - 'and rebuild your container to update' - return True - return False - - -class GitUpdateManager(UpdateManager): - def __init__(self): - super(GitUpdateManager, self).__init__() - self._git_path = self._find_working_git() - self.github_org = self.get_github_org() - self.github_repo = self.get_github_repo() - - self.branch = app.BRANCH = self._find_installed_branch() - - self._cur_commit_hash = None - self._newest_commit_hash = None - self._num_commits_behind = 0 - self._num_commits_ahead = 0 - self._cur_version = '' - - def get_cur_commit_hash(self): - return self._cur_commit_hash - - def get_newest_commit_hash(self): - return self._newest_commit_hash - - def get_cur_version(self): - if self._cur_commit_hash or self._find_installed_version(): - self._cur_version = self._run_git(self._git_path, 'describe --tags --abbrev=0 {0}'.format(self._cur_commit_hash))[0] - return self._cur_version - - def get_newest_version(self): - if self._newest_commit_hash: - self._cur_version = self._run_git(self._git_path, 'describe --tags --abbrev=0 ' + self._newest_commit_hash)[0] - else: - self._cur_version = self._run_git(self._git_path, 'describe --tags --abbrev=0 ' + self._cur_commit_hash)[0] - return self._cur_version - - def get_num_commits_behind(self): - return self._num_commits_behind - - def get_num_commits_ahead(self): - return self._num_commits_ahead - - def _find_working_git(self): - test_cmd = 'version' - - if app.GIT_PATH: - main_git = '"' + app.GIT_PATH + '"' - else: - main_git = 'git' - - log.debug(u'Checking if we can use git commands: {0} {1}', main_git, test_cmd) - _, _, exit_status = self._run_git(main_git, test_cmd) - - if exit_status == 0: - log.debug(u'Using: {0}', main_git) - return main_git - else: - log.debug(u'Not using: {0}', main_git) - - # trying alternatives - - alternative_git = [] - - # osx people who start sr from launchd have a broken path, so try a hail-mary attempt for them - if platform.system().lower() == 'darwin': - alternative_git.append('/usr/local/git/bin/git') - - if platform.system().lower() == 'windows': - if main_git != main_git.lower(): - alternative_git.append(main_git.lower()) - - if alternative_git: - log.debug(u'Trying known alternative git locations') - - for cur_git in alternative_git: - log.debug(u'Checking if we can use git commands: {0} {1}', cur_git, test_cmd) - _, _, exit_status = self._run_git(cur_git, test_cmd) - - if exit_status == 0: - log.debug(u'Using: {0}', cur_git) - return cur_git - else: - log.debug(u'Not using: {0}', cur_git) - - # Still haven't found a working git - # Warn user only if he has version check enabled - if app.VERSION_NOTIFY: - app.NEWEST_VERSION_STRING = ERROR_MESSAGE - - return None - - @staticmethod - def _run_git(git_path, args): - - output = err = exit_status = None - - if not git_path: - log.warning(u"No git specified, can't use git commands") - app.NEWEST_VERSION_STRING = ERROR_MESSAGE - exit_status = 1 - return output, err, exit_status - - # If we have a valid git remove the git warning - # String will be updated as soon we check github - app.NEWEST_VERSION_STRING = None - - cmd = git_path + ' ' + args - - try: - log.debug(u'Executing {cmd} with your shell in {dir}', {'cmd': cmd, 'dir': app.PROG_DIR}) - p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - shell=True, cwd=app.PROG_DIR) - output, err = p.communicate() - exit_status = p.returncode - - # Convert bytes to string in python3 - if isinstance(output, (bytes, bytearray)): - output = output.decode('utf-8') - - if output: - output = output.strip() - - except OSError: - log.info(u"Command {cmd} didn't work", {'cmd': cmd}) - exit_status = 1 - - if exit_status == 0: - log.debug(u'{cmd} : returned successful', {'cmd': cmd}) - exit_status = 0 - - elif exit_status == 1: - if output: - if 'stash' in output: - log.warning(u"Enable 'git reset' in settings or stash your changes in local files") - else: - log.warning(u'{cmd} returned : {output}', {'cmd': cmd, 'output': output}) - else: - log.warning(u'{cmd} returned no data', {'cmd': cmd}) - exit_status = 1 - - elif exit_status == 128 or 'fatal:' in output or err: - log.warning(u'{cmd} returned : {output}', {'cmd': cmd, 'output': output}) - exit_status = 128 - - else: - log.warning(u'{cmd} returned : {output}. Treat as error for now', {'cmd': cmd, 'output': output}) - exit_status = 1 - - return output, err, exit_status - - def _find_installed_version(self): - """Attempt to find the currently installed version of the application. - - Uses git show to get commit version. - - :return: True for success or False for failure - """ - output, _, exit_status = self._run_git(self._git_path, 'rev-parse HEAD') # @UnusedVariable - - if exit_status == 0 and output: - cur_commit_hash = output.strip() - if not re.match('^[a-z0-9]+$', cur_commit_hash): - log.warning(u"Output doesn't look like a hash, not using it") - return False - self._cur_commit_hash = cur_commit_hash - app.CUR_COMMIT_HASH = str(cur_commit_hash) - return True - else: - return False - - def _find_installed_branch(self): - branch_info, _, exit_status = self._run_git(self._git_path, 'symbolic-ref -q HEAD') # @UnusedVariable - if exit_status == 0 and branch_info: - branch = branch_info.strip().replace('refs/heads/', '', 1) - if branch: - app.BRANCH = branch - return branch - return '' - - def _check_github_for_update(self): - """ - Uses git commands to check if there is a newer version that the provided - commit hash. If there is a newer version it sets _num_commits_behind. - """ - - self._num_commits_behind = 0 - self._num_commits_ahead = 0 - - # update remote origin url - self.update_remote_origin() - - # get all new info from github - output, _, exit_status = self._run_git(self._git_path, 'fetch --prune %s' % app.GIT_REMOTE) - if not exit_status == 0: - log.warning(u"Unable to contact github, can't check for update") - return - - # get latest commit_hash from remote - output, _, exit_status = self._run_git(self._git_path, 'rev-parse --verify --quiet "@{upstream}"') - - if exit_status == 0 and output: - cur_commit_hash = output.strip() - - if not re.match('^[a-z0-9]+$', cur_commit_hash): - log.debug(u"Output doesn't look like a hash, not using it") - return - - else: - self._newest_commit_hash = cur_commit_hash - else: - log.debug(u"git didn't return newest commit hash") - return - - # get number of commits behind and ahead (option --count not supported git < 1.7.2) - output, _, exit_status = self._run_git(self._git_path, 'rev-list --left-right "@{upstream}"...HEAD') - if exit_status == 0 and output: - - try: - self._num_commits_behind = int(output.count('<')) - self._num_commits_ahead = int(output.count('>')) - - except Exception: - log.debug(u"git didn't return numbers for behind and ahead, not using it") - return - - log.debug(u'cur_commit = {0}, newest_commit = {1}, num_commits_behind = {2}, num_commits_ahead = {3}', - self._cur_commit_hash, self._newest_commit_hash, self._num_commits_behind, self._num_commits_ahead) - - def set_newest_text(self): - - # if we're up to date then don't set this - app.NEWEST_VERSION_STRING = None - - if self.set_newest_text_docker(): - return - - if self._num_commits_behind > 0 or self._is_hard_reset_allowed(): - - base_url = 'http://github.com/' + self.github_org + '/' + self.github_repo - if self._newest_commit_hash: - url = base_url + '/compare/' + self._cur_commit_hash + '...' + self._newest_commit_hash - else: - url = base_url + '/commits/' - - newest_text = 'There is a newer version available ' - newest_text += " (you're " + str(self._num_commits_behind) + ' commit' - if self._num_commits_behind > 1: - newest_text += 's' - newest_text += ' behind' - if self._num_commits_ahead > 0: - newest_text += ' and {ahead} commit{s} ahead'.format(ahead=self._num_commits_ahead, - s='s' if self._num_commits_ahead > 1 else '') - newest_text += ') — Update Now' - - elif self._num_commits_ahead > 0: - newest_text = u'Local branch is ahead of {0}. Automatic update not possible'.format(self.branch) - log.warning(newest_text) - - else: - return - - app.NEWEST_VERSION_STRING = newest_text - - def need_update(self): - - if self.branch != self._find_installed_branch(): - log.debug(u'Branch checkout: {0}->{1}', self._find_installed_branch(), self.branch) - return True - - self._find_installed_version() - if not self._cur_commit_hash: - return True - else: - try: - self._check_github_for_update() - except Exception as e: - log.warning(u"Unable to contact github, can't check for update: {0!r}", e) - return False - - if self._num_commits_behind > 0 or self._num_commits_ahead > 0: - return True - - return False - - def can_update(self): - """Return whether update can be executed. - - :return: - :rtype: bool - """ - return self._num_commits_ahead <= 0 or self._is_hard_reset_allowed() - - def update(self): - """Call git pull origin in order to update the application. - - Returns a bool depending on the call's success. - """ - # update remote origin url - self.update_remote_origin() - - # remove untracked files and performs a hard reset on git branch to avoid update issues - if self._is_hard_reset_allowed(): - self.reset() - - # Executing git clean before updating - self.clean() - - if self.branch == self._find_installed_branch(): - _, _, exit_status = self._run_git(self._git_path, 'pull -f %s %s' % (app.GIT_REMOTE, self.branch)) # @UnusedVariable - else: - _, _, exit_status = self._run_git(self._git_path, 'checkout -f ' + self.branch) # @UnusedVariable - - # Executing git clean after updating - self.clean() - - if exit_status == 0: - self._find_installed_version() - # Notify update successful - if app.NOTIFY_ON_UPDATE: - try: - notifiers.notify_git_update(app.CUR_COMMIT_HASH or '') - except Exception: - log.debug(u'Unable to send update notification. Continuing the update process') - return True - - else: - return False - - @staticmethod - def _is_hard_reset_allowed(): - """Return whether git hard reset is allowed or not. - - :return: - :rtype: bool - """ - return app.GIT_RESET and (not app.GIT_RESET_BRANCHES or - app.BRANCH in app.GIT_RESET_BRANCHES) - - def clean(self): - """Call git clean to remove all untracked files. - - It only affects source folders and libX and extX folders, - to prevent deleting untracked user data not known by .gitignore - - :return: - :rtype: int - """ - # Fixes: goo.gl/tr8Awf - to be removed in the next release - root_dir = os.path.basename(app.PROG_DIR) - helper_folder = os.path.join(root_dir, 'helper') - helpers_folder = os.path.join(root_dir, 'helpers') - - folders = (app.LIB_FOLDER, app.LIB2_FOLDER, app.LIB3_FOLDER, app.EXT_FOLDER, - app.EXT2_FOLDER, app.EXT3_FOLDER, app.SRC_FOLDER, app.STATIC_FOLDER, - helper_folder, helpers_folder) + app.LEGACY_SRC_FOLDERS - _, _, exit_status = self._run_git(self._git_path, 'clean -d -f -x {0}'.format(' '.join(folders))) - - return exit_status - - def reset(self): - """ - Calls git reset --hard to perform a hard reset. Returns a bool depending - on the call's success. - """ - _, _, exit_status = self._run_git(self._git_path, 'reset --hard {0}/{1}'.format - (app.GIT_REMOTE, app.BRANCH)) # @UnusedVariable - if exit_status == 0: - return True - - def list_remote_branches(self): - # update remote origin url - self.update_remote_origin() - app.BRANCH = self._find_installed_branch() - - branches, _, exit_status = self._run_git(self._git_path, 'ls-remote --heads %s' % app.GIT_REMOTE) # @UnusedVariable - if exit_status == 0 and branches: - if branches: - return re.findall(r'refs/heads/(.*)', branches) - return [] - - def update_remote_origin(self): - self._run_git(self._git_path, 'config remote.%s.url %s' % (app.GIT_REMOTE, app.GIT_REMOTE_URL)) - self._run_git(self._git_path, 'config remote.%s.pushurl %s' % (app.GIT_REMOTE, app.GIT_REMOTE_URL)) - - -class SourceUpdateManager(UpdateManager): - def __init__(self): - super(SourceUpdateManager, self).__init__() - self.github_org = self.get_github_org() - self.github_repo = self.get_github_repo() - - self.branch = app.BRANCH - if app.BRANCH == '': - self.branch = self._find_installed_branch() - - self._cur_commit_hash = app.CUR_COMMIT_HASH - self._newest_commit_hash = None - self._num_commits_behind = 0 - self._num_commits_ahead = 0 - - self.session = MedusaSafeSession() - - @staticmethod - def _find_installed_branch(): - return app.CUR_COMMIT_BRANCH if app.CUR_COMMIT_BRANCH else 'master' - - def get_cur_commit_hash(self): - return self._cur_commit_hash - - def get_newest_commit_hash(self): - return self._newest_commit_hash - - @staticmethod - def get_cur_version(): - return '' - - @staticmethod - def get_newest_version(): - return '' - - def get_num_commits_behind(self): - return self._num_commits_behind - - def get_num_commits_ahead(self): - return self._num_commits_ahead - - def need_update(self): - # need this to run first to set self._newest_commit_hash - try: - self._check_github_for_update() - except Exception as error: - log.warning(u"Unable to contact github, can't check for update: {0!r}", error) - return False - - if self.branch != self._find_installed_branch(): - log.debug(u'Branch checkout: {0}->{1}', self._find_installed_branch(), self.branch) - return True - - if not self._cur_commit_hash or self._num_commits_behind > 0 or self._num_commits_ahead > 0: - return True - - return False - - def can_update(self): - """Whether or not the update can be performed. - - :return: - :rtype: bool - """ - return True - - def _check_github_for_update(self): - """Use pygithub to ask github if there is a newer version.. - - If there is a newer version it sets application's version text. - - commit_hash: hash that we're checking against - """ - - self._num_commits_behind = 0 - self._newest_commit_hash = None - - gh = get_github_repo(app.GIT_ORG, app.GIT_REPO) - # try to get newest commit hash and commits behind directly by comparing branch and current commit - if self._cur_commit_hash: - try: - branch_compared = gh.compare(base=self.branch, head=self._cur_commit_hash) - self._newest_commit_hash = branch_compared.base_commit.sha - self._num_commits_behind = branch_compared.behind_by - self._num_commits_ahead = branch_compared.ahead_by - except Exception: # UnknownObjectException - self._newest_commit_hash = '' - self._num_commits_behind = 0 - self._num_commits_ahead = 0 - self._cur_commit_hash = '' - - # fall back and iterate over last 100 (items per page in gh_api) commits - if not self._newest_commit_hash: - - for curCommit in gh.get_commits(): - if not self._newest_commit_hash: - self._newest_commit_hash = curCommit.sha - if not self._cur_commit_hash: - break - - if curCommit.sha == self._cur_commit_hash: - break - - # when _cur_commit_hash doesn't match anything _num_commits_behind == 100 - self._num_commits_behind += 1 - - log.debug(u'cur_commit = {0}, newest_commit = {1}, num_commits_behind = {2}', - self._cur_commit_hash, self._newest_commit_hash, self._num_commits_behind) - - def set_newest_text(self): - - # if we're up to date then don't set this - app.NEWEST_VERSION_STRING = None - - if self.set_newest_text_docker(): - return - - if not self._cur_commit_hash: - log.debug(u"Unknown current version number, don't know if we should update or not") - - newest_text = "Unknown current version number: If you've never used the application " \ - 'upgrade system before then current version is not set. ' \ - '— Update Now' - - elif self._num_commits_behind > 0: - base_url = 'http://github.com/' + self.github_org + '/' + self.github_repo - if self._newest_commit_hash: - url = base_url + '/compare/' + self._cur_commit_hash + '...' + self._newest_commit_hash - else: - url = base_url + '/commits/' - - newest_text = 'There is a newer version available' - newest_text += " (you're " + str(self._num_commits_behind) + ' commit' - if self._num_commits_behind > 1: - newest_text += 's' - newest_text += ' behind) — Update Now' - else: - return - - app.NEWEST_VERSION_STRING = newest_text - - def update(self): - """ - Downloads the latest source tarball from github and installs it over the existing version. - """ - - tar_download_url = 'http://github.com/' + self.github_org + '/' + self.github_repo + '/tarball/' + self.branch - - try: - # prepare the update dir - app_update_dir = os.path.join(app.PROG_DIR, u'sr-update') - - if os.path.isdir(app_update_dir): - log.info(u'Clearing out update folder {0!r} before extracting', app_update_dir) - shutil.rmtree(app_update_dir) - - log.info(u'Clearing update folder {0!r} before extracting', app_update_dir) - os.makedirs(app_update_dir) - - # retrieve file - log.info(u'Downloading update from {0!r}', tar_download_url) - tar_download_path = os.path.join(app_update_dir, u'sr-update.tar') - helpers.download_file(tar_download_url, tar_download_path, session=self.session) - - if not os.path.isfile(tar_download_path): - log.warning(u"Unable to retrieve new version from {0!r}, can't update", tar_download_url) - return False - - if not tarfile.is_tarfile(tar_download_path): - log.warning(u"Retrieved version from {0!r} is corrupt, can't update", tar_download_url) - return False - - # extract to sr-update dir - log.info(u'Extracting file {0}', tar_download_path) - tar = tarfile.open(tar_download_path) - tar.extractall(app_update_dir) - tar.close() - - # delete .tar.gz - log.info(u'Deleting file {0}', tar_download_path) - os.remove(tar_download_path) - - # find update dir name - update_dir_contents = [x for x in os.listdir(app_update_dir) if - os.path.isdir(os.path.join(app_update_dir, x))] - if len(update_dir_contents) != 1: - log.warning(u'Invalid update data, update failed: {0}', update_dir_contents) - return False - content_dir = os.path.join(app_update_dir, update_dir_contents[0]) - - # walk temp folder and move files to main folder - log.info(u'Moving files from {0} to {1}', content_dir, app.PROG_DIR) - for dirname, _, filenames in os.walk(content_dir): # @UnusedVariable - dirname = dirname[len(content_dir) + 1:] - for curfile in filenames: - old_path = os.path.join(content_dir, dirname, curfile) - new_path = os.path.join(app.PROG_DIR, dirname, curfile) - - # Avoid DLL access problem on WIN32/64 - # These files needing to be updated manually - # or find a way to kill the access from memory - extension = os.path.splitext(curfile)[1] - if extension == '.dll': - try: - log.debug(u'Special handling for {0}', curfile) - os.chmod(new_path, stat.S_IWRITE) - os.remove(new_path) - os.renames(old_path, new_path) - except Exception as e: - log.debug(u'Unable to update {0}: {1!r}', new_path, e) - os.remove(old_path) # Trash the updated file without moving in new path - continue - - if os.path.isfile(new_path): - os.remove(new_path) - os.renames(old_path, new_path) - - app.CUR_COMMIT_HASH = self._newest_commit_hash - app.CUR_COMMIT_BRANCH = self.branch - - except Exception as e: - log.exception(u'Error while trying to update: {0}', e) - return False - - # Notify update successful - try: - notifiers.notify_git_update(app.CUR_COMMIT_HASH or '') - except Exception: - log.debug(u'Unable to send update notification. Continuing the update process') - return True - - @staticmethod - def list_remote_branches(): - try: - gh = get_github_repo(app.GIT_ORG, app.GIT_REPO) - return [x.name for x in gh.get_branches() if x] - except GithubException as error: - log.warning(u"Unable to contact github, can't check for update: {0!r}", error) - return [] diff --git a/setup.cfg b/setup.cfg index 9cbeccdc5e..076ba10789 100644 --- a/setup.cfg +++ b/setup.cfg @@ -173,7 +173,12 @@ flake8-ignore = medusa/themes/__init__.py F401 medusa/tv/__init__.py D104 F401 medusa/ui.py D100 D101 D102 D200 D202 D204 D205 D400 D401 E305 N802 N803 N806 - medusa/version_checker.py D100 D101 D102 D200 D202 D205 D400 D401 N802 N803 N806 + medusa/updater/__init__.py D104 + medusa/updater/docker_updater.py D100 D101 D102 D105 + medusa/updater/github_updater.py D100 D101 D102 D105 + medusa/updater/source_updater.py D100 D101 D102 D105 + medusa/updater/update_manager.py D100 D101 D102 + medusa/updater/version_checker.py D100 D101 D102 N802 N803 N806 setup.py D200 D400 tests/__init__.py D104 tests/*.py D101 D102 D103 diff --git a/tests/conftest.py b/tests/conftest.py index 4866f7e9dc..d6e41f2fb1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,11 +20,10 @@ from medusa.logger import read_loglines as logger_read_loglines from medusa.providers.generic_provider import GenericProvider from medusa.tv import Episode, Series -from medusa.version_checker import CheckVersion +from medusa.updater.version_checker import CheckVersion from mock.mock import Mock import pytest -import six from six import iteritems, text_type from subliminal.subtitle import Subtitle from subliminal.video import Video