From caaef3a3048c5bb77fb13c9cea6e8831ff6189ec Mon Sep 17 00:00:00 2001 From: I-am-PUID-0 <36779668+I-am-PUID-0@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:39:37 -0400 Subject: [PATCH] Version [3.2.0] --- CHANGELOG.md | 17 +++++++++++++++++ Dockerfile | 1 + README.md | 2 +- base/__init__.py | 1 + main.py | 2 +- utils/auto_update.py | 8 +++++--- utils/download.py | 41 +++++++++++++++++++++++++++++++++-------- utils/logger.py | 8 +++++--- utils/processes.py | 16 ++++++++++------ zurg/download.py | 35 ++++++++++++++++++++++++----------- zurg/setup.py | 12 ++++++++---- zurg/update.py | 37 +++++++++++++++++++++++++------------ 12 files changed, 131 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f77b39..33a9647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## Version [3.2.0] - 2024-30-07 🚀 + +### Changed 🔄 + +- Update process: Refactored update process to apply updates to Zurg and Riven before starting the processes 🔄 +- Zurg: Disabling plex_update.sh in config file has been disbaled, for now. Comment out the line in the config file to disable the Zurg based plex update process if desired 🔄 + +### Added ✨ + +- Zurg: Allow nightly release custom versions for ZURG_VERSION +- Zurg: Add plex_update.sh from Zurg to working directory for Zurg use 📦 + +### Fixed 🛠️ + +- Logging: Fixed logging for Zurg to ensure log levels are properly set 📝 + + ## Version [3.1.0] - 2024-26-07 🚀 ### Added ✨ diff --git a/Dockerfile b/Dockerfile index 28d6d8c..010d8e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ WORKDIR / ADD . / ./ ADD https://raw.githubusercontent.com/debridmediamanager/zurg-testing/main/config.yml /zurg/ +ADD https://raw.githubusercontent.com/debridmediamanager/zurg-testing/main/plex_update.sh /zurg/ ENV \ XDG_CONFIG_HOME=/config \ diff --git a/README.md b/README.md index deb0a49..0392dd6 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ of this parameter has the format `=`. |`COLOR_LOG_ENABLED`| Enable color logging for DMB. | `false` | | | | |`ZURG_ENABLED`| Set the value "true" to enable the Zurg process | `false ` | | | :heavy_check_mark:| |`GITHUB_TOKEN`| GitHub Personal Token for use with Zurg private repo. Requires Zurg [sponsorship](https://github.com/sponsors/debridmediamanager) | `false ` | | | :heavy_check_mark:| -|`ZURG_VERSION`| The version of Zurg to use. If enabled, the value should contain v0.9.x or v0.9.x-hotfix.x format | `latest` | | | :heavy_check_mark: | +|`ZURG_VERSION`| The version of Zurg to use. If enabled, the value should contain v0.9.x or v0.9.x-hotfix.x format or "nightly" if wanting the nightly builds from Zurg private repo (requires GITHUB_TOKEN) | `latest` | | | :heavy_check_mark: | |`ZURG_UPDATE`| Enable automatic updates of Zurg. Adding this variable will enable automatic updates to the latest version of Zurg locally within the container. | `false` | | | :heavy_check_mark:| |`ZURG_LOG_LEVEL`| Set the log level for Zurg | `INFO` | | | :heavy_check_mark:| |`SEERR_API_KEY`| The Overseerr API Key |`none`|| :heavy_check_mark:|| diff --git a/base/__init__.py b/base/__init__.py index fd050dc..ffdb4e2 100644 --- a/base/__init__.py +++ b/base/__init__.py @@ -7,6 +7,7 @@ from packaging.version import Version, parse as parse_version import time import os +import ast import requests import zipfile import io diff --git a/main.py b/main.py index 2eb39b0..7552172 100644 --- a/main.py +++ b/main.py @@ -25,7 +25,7 @@ def shutdown(signum, frame): def main(): logger = get_logger() - version = '3.1.0' + version = '3.2.0' ascii_art = f''' diff --git a/utils/auto_update.py b/utils/auto_update.py index bed84eb..8a02177 100644 --- a/utils/auto_update.py +++ b/utils/auto_update.py @@ -6,9 +6,9 @@ class Update(ProcessHandler): def __init__(self): logger = get_logger() super().__init__(logger) - + def update_schedule(self, process_name): - self.update_check(process_name) + #self.update_check(process_name) interval_minutes = int(self.auto_update_interval() * 60) schedule.every(interval_minutes).minutes.do(self.update_check, process_name) @@ -26,9 +26,11 @@ def auto_update_interval(self): def auto_update(self, process_name, enable_update): if enable_update: self.logger.info(f"Automatic updates set to {format_time(self.auto_update_interval())} for {process_name}") + initial_update = self.update_check(process_name) self.schedule_thread = threading.Thread(target=self.update_schedule, args=(process_name,)) self.schedule_thread.start() - self.start_process(process_name) + if not initial_update: + self.start_process(process_name) else: self.logger.info(f"Automatic update disabled for {process_name}") self.start_process(process_name) diff --git a/utils/download.py b/utils/download.py index c7f73cd..8ae6cfc 100644 --- a/utils/download.py +++ b/utils/download.py @@ -46,16 +46,27 @@ def fetch_with_retries(self, url, headers, max_retries=5): self.logger.error(f"Failed to fetch {url} after {max_retries} attempts.") return None - def get_latest_release(self, repo_owner, repo_name): + def get_latest_release(self, repo_owner, repo_name, nightly=False): self.logger.info(f"Fetching latest {repo_name} release.") headers = self.get_headers() - api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" + if nightly: + api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases" + else: + api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" response = self.fetch_with_retries(api_url, headers) if response and response.status_code == 200: - latest_release = response.json() - version_tag = latest_release['tag_name'] - self.logger.info(f"{repo_name} latest release: {version_tag}") - return version_tag, None + releases = response.json() + if nightly: + nightly_releases = [release for release in releases if 'nightly' in release['tag_name']] + if nightly_releases: + latest_nightly = max(nightly_releases, key=lambda x: x['tag_name']) + return latest_nightly['tag_name'], None + return None, "No nightly releases found." + else: + latest_release = response.json() + version_tag = latest_release['tag_name'] + self.logger.info(f"{repo_name} latest release: {version_tag}") + return version_tag, None else: return None, "Error: Unable to access the repository API." @@ -136,7 +147,7 @@ def download_and_extract(self, url, target_dir, zip_folder_name=None, headers=No shutil.copyfileobj(src, dst) except Exception as e: self.logger.error(f"Error while extracting {file_info.filename}: {e}") - self.logger.info(f"Successfully downloaded {zip_folder_name} and extracted to {target_dir}") + self.logger.debug(f"Successfully downloaded {zip_folder_name} and extracted to {target_dir}") return True, None except zipfile.BadZipFile as e: self.logger.error(f"Failed to create ZipFile object: {e}") @@ -150,7 +161,7 @@ def download_and_extract(self, url, target_dir, zip_folder_name=None, headers=No def set_permissions(self, file_path, mode): try: os.chmod(file_path, mode) - self.logger.info(f"Set permissions for {file_path} to {oct(mode)}") + self.logger.debug(f"Set permissions for {file_path} to {oct(mode)}") except Exception as e: self.logger.error(f"Failed to set permissions for {file_path}: {e}") @@ -178,4 +189,18 @@ def get_architecture(self): except Exception as e: self.logger.error(f"Error determining system architecture: {e}") return 'unknown' + + def parse_repo_info(self, repo_info): + if not repo_info: + raise ValueError(f"{repo_info} environment variable is not set.") + + parts = repo_info.split(',') + if len(parts) < 2: + raise ValueError(f"{repo_info} environment variable must contain at least username and repository name.") + + username = parts[0].strip() + repository = parts[1].strip() + branch = parts[2].strip() if len(parts) > 2 else 'main' + self.logger.debug(f"Repository: {username}/{repository} branch: {branch}, being used for plex_debrid") + return username, repository, branch diff --git a/utils/logger.py b/utils/logger.py index fd63419..2f9e036 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -32,9 +32,9 @@ def parse_log_level_and_message(line, process_name): else: log_level = 'UNKNOWN' message = line - + date_time_prefix_pattern = re.compile(r'^\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} ') - message = date_time_prefix_pattern.sub('', message).strip() + message = date_time_prefix_pattern.sub('', message).strip() return log_level, message @@ -301,6 +301,8 @@ def get_logger(log_name='DMB', log_dir='./log'): log_level_env = os.getenv('DMB_LOG_LEVEL') if log_level_env: log_level = log_level_env.upper() + os.environ['LOG_LEVEL'] = log_level + os.environ['RCLONE_LOG_LEVEL'] = log_level else: log_level = 'INFO' numeric_level = getattr(logging, log_level, logging.INFO) @@ -360,4 +362,4 @@ def get_logger(log_name='DMB', log_dir='./log'): logger.removeHandler(hdlr) logger.addHandler(handler) logger.addHandler(stdout_handler) - return logger + return logger \ No newline at end of file diff --git a/utils/processes.py b/utils/processes.py index 1cff2e9..96e0740 100644 --- a/utils/processes.py +++ b/utils/processes.py @@ -7,12 +7,14 @@ def __init__(self, logger): self.process = None self.subprocess_logger = None - def start_process(self, process_name, config_dir, command, key_type): + def start_process(self, process_name, config_dir, command, key_type=None): try: - if key_type: + if key_type is not None: self.logger.info(f"Starting {process_name} w/ {key_type}") + process_description = f"{process_name} w/ {key_type}" else: self.logger.info(f"Starting {process_name}") + process_description = f"{process_name}" self.process = subprocess.Popen( command, stdout=subprocess.PIPE, @@ -22,24 +24,26 @@ def start_process(self, process_name, config_dir, command, key_type): universal_newlines=True, bufsize=1 ) - self.subprocess_logger = SubprocessLogger(self.logger, f"{process_name} w/ {key_type}") + self.subprocess_logger = SubprocessLogger(self.logger, f"{process_description}") self.subprocess_logger.start_logging_stdout(self.process) self.subprocess_logger.start_monitoring_stderr(self.process, key_type, process_name) return self.process except Exception as e: - self.logger.error(f"Error running subprocess for {process_name} w/ {key_type}: {e}") + self.logger.error(f"Error running subprocess for {process_description}: {e}") return None - def stop_process(self, process_name, key_type): + def stop_process(self, process_name, key_type=None): try: if key_type: self.logger.info(f"Stopping {process_name} w/ {key_type}") + process_description = f"{process_name} w/ {key_type}" else: self.logger.info(f"Stopping {process_name}") + process_description = f"{process_name}" if self.process: self.process.kill() if self.subprocess_logger: self.subprocess_logger.stop_logging_stdout() self.subprocess_logger.stop_monitoring_stderr() except Exception as e: - self.logger.error(f"Error stopping subprocess for {process_name} w/ {key_type}: {e}") + self.logger.error(f"Error stopping subprocess for {process_description}: {e}") \ No newline at end of file diff --git a/zurg/download.py b/zurg/download.py index 024e13e..48e1229 100644 --- a/zurg/download.py +++ b/zurg/download.py @@ -23,7 +23,7 @@ def __lt__(self, other): return self.subversion < other.subversion def __eq__(self, other): - return (self.main_version, self.hotfix, self.subversion) == (other.main_version, other.hotfix, other.subversion) + return (self.main_version, self.hotfix, self.subversion) == (other.main_version, other.hotfix, self.subversion) def __str__(self): version_str = f'v{self.main_version}' @@ -49,12 +49,12 @@ def parse_custom_version(version_str): logger.error(f"Error parsing version string '{version_str}': {e}") return None -def get_latest_release(repo_owner, repo_name): - return downloader.get_latest_release(repo_owner, repo_name) +def get_latest_release(repo_owner, repo_name, nightly=False): + return downloader.get_latest_release(repo_owner, repo_name, nightly=nightly) def get_architecture(): return downloader.get_architecture() - + def download_and_unzip_release(repo_owner, repo_name, release_version, architecture): try: headers = {} @@ -83,26 +83,39 @@ def download_and_unzip_release(repo_owner, repo_name, release_version, architect logger.error(f"Error in download and extraction: {e}") return False - def version_check(): try: architecture = get_architecture() os.environ['CURRENT_ARCHITECTURE'] = architecture if GHTOKEN: repo_owner = 'debridmediamanager' - repo_name = 'zurg' + repo_name = 'zurg' else: repo_owner = 'debridmediamanager' - repo_name = 'zurg-testing' - + repo_name = 'zurg-testing' + + nightly = False + if ZURGVERSION: - release_version = ZURGVERSION if ZURGVERSION.startswith('v') else 'v' + ZURGVERSION - logger.info("Using release version from environment variable: %s", release_version) - else: + if "nightly" in ZURGVERSION.lower(): + release_version = ZURGVERSION + logger.info("Using nightly release version from environment variable") + nightly = True + else: + release_version = ZURGVERSION if ZURGVERSION.startswith('v') else 'v' + ZURGVERSION + logger.info("Using release version from environment variable: %s", release_version) + else: release_version, error = get_latest_release(repo_owner, repo_name) if error: logger.error(error) raise Exception("Failed to get the latest release version.") + + if nightly: + release_version, error = get_latest_release(repo_owner, repo_name, nightly=True) + if error: + logger.error(error) + raise Exception("Failed to get the latest nightly release version.") + if not download_and_unzip_release(repo_owner, repo_name, release_version, architecture): raise Exception("Failed to download and extract the release.") diff --git a/zurg/setup.py b/zurg/setup.py index c17ecc5..e8f130f 100644 --- a/zurg/setup.py +++ b/zurg/setup.py @@ -9,6 +9,7 @@ def zurg_setup(): zurg_app_base = '/zurg/zurg' zurg_config_override = '/config/config.yml' zurg_config_base = '/zurg/config.yml' + zurg_plex_update_base = '/zurg/plex_update.sh' try: if ZURGLOGLEVEL is not None: # Needs additional testing @@ -73,7 +74,7 @@ def update_creds(file_path, zurguser, zurgpass): file.write("# password:\n") else: file.write(line) - + def check_and_set_zurg_version(dir_path): zurg_binary_path = os.path.join(dir_path, 'zurg') if os.path.exists(zurg_binary_path) and not ZURGVERSION: @@ -96,6 +97,8 @@ def setup_zurg_instance(config_dir, token, key_type): try: zurg_executable_path = os.path.join(config_dir, 'zurg') config_file_path = os.path.join(config_dir, 'config.yml') + plex_update_file_path = os.path.join(config_dir, 'plex_update.sh') + refresh_file_path = os.path.join(config_dir, 'plex_refresh.py') logger.info(f"Preparing Zurg instance for {key_type}") if os.path.exists(zurg_app_override): @@ -119,7 +122,10 @@ def setup_zurg_instance(config_dir, token, key_type): shutil.copy(zurg_config_base, config_file_path) else: logger.info(f"Using Zurg config found for {key_type} in {config_dir}") - + + if not os.path.exists(plex_update_file_path): + shutil.copy(zurg_plex_update_base,plex_update_file_path) + if ZURGPORT: port = ZURGPORT logger.debug(f"Setting port {port} for Zurg w/ {key_type} instance") @@ -135,8 +141,6 @@ def setup_zurg_instance(config_dir, token, key_type): logger.debug(f"Zurg w/ {key_type} instance configured to port: {port}") update_token(config_file_path, token) - update_plex(config_file_path) - except Exception as e: raise Exception(f"Error setting up Zurg instance for {key_type}: {e}") diff --git a/zurg/update.py b/zurg/update.py index 7c8d59f..39d0cbc 100644 --- a/zurg/update.py +++ b/zurg/update.py @@ -40,8 +40,8 @@ def start_process(self, process_name, config_dir=None): elif dir_to_check == "/zurg/AD": key_type = "AllDebrid" command = [zurg_executable] - super().start_process(process_name, dir_to_check, command, key_type) - + super().start_process(process_name, dir_to_check, command, key_type) + def update_check(self, process_name): try: if GHTOKEN: @@ -51,29 +51,40 @@ def update_check(self, process_name): repo_owner = 'debridmediamanager' repo_name = 'zurg-testing' - if ZURGVERSION: - self.logger.info(f"ZURG_VERSION is set to: {ZURGVERSION}. Automatic updates will not be applied!") - return - current_version = os.getenv('ZURG_CURRENT_VERSION') - latest_release, error = get_latest_release(repo_owner, repo_name) - if error: - self.logger.error(f"Failed to fetch the latest {process_name} release: {error}") - return + nightly = False + + if ZURGVERSION: + if "nightly" in ZURGVERSION.lower(): + self.logger.info(f"ZURG_VERSION is set to nightly build. Checking for updates.") + latest_release, error = get_latest_release(repo_owner, repo_name, nightly=True) + if error: + self.logger.error(f"Failed to fetch the latest nightly {process_name} release: {error}") + return False + else: + self.logger.info(f"ZURG_VERSION is set to: {ZURGVERSION}. Automatic updates will not be applied!") + return False + else: + latest_release, error = get_latest_release(repo_owner, repo_name) + if error: + self.logger.error(f"Failed to fetch the latest {process_name} release: {error}") + return False + self.logger.info(f"{process_name} current version: {current_version}") self.logger.debug(f"{process_name} latest available version: {latest_release}") if current_version == latest_release: self.logger.info(f"{process_name} is already up to date.") + return False else: self.logger.info(f"A new version of {process_name} is available. Applying updates.") architecture = get_architecture() success = download_and_unzip_release(repo_owner, repo_name, latest_release, architecture) if not success: raise Exception(f"Failed to download and extract the release for {process_name}.") - + directories_to_check = ["/zurg/RD", "/zurg/AD"] zurg_presence = {dir_to_check: os.path.exists(os.path.join(dir_to_check, 'zurg')) for dir_to_check in directories_to_check} @@ -82,9 +93,11 @@ def update_check(self, process_name): key_type = "RealDebrid" if dir_to_check == "/zurg/RD" else "AllDebrid" zurg_app_base = '/zurg/zurg' zurg_executable_path = os.path.join(dir_to_check, 'zurg') - self.terminate_zurg_instance('Zurg', dir_to_check, key_type) + self.stop_process(process_name, key_type) shutil.copy(zurg_app_base, zurg_executable_path) self.start_process('Zurg', dir_to_check) + return True except Exception as e: self.logger.error(f"An error occurred in update_check for {process_name}: {e}") + return False \ No newline at end of file