diff --git a/config_v10-10-7.yaml b/config_v10-10-7.yaml new file mode 100644 index 0000000..cf7c215 --- /dev/null +++ b/config_v10-10-7.yaml @@ -0,0 +1,84 @@ +# Zielverzeichnis für Operationen +destination_directory: './web' + +# Kopierregeln +copy_rules: + - sources: + - source: './img/icon-transparent.png' + target: './assets/img/icon-transparent.png' + - source: './img/banner-light.png' + target: './assets/img/banner-light.png' + - source: './img/banner-dark.png' + target: './assets/img/banner-dark.png' + + - source: './img/bc8d51405ec040305a87.ico' + target: './bc8d51405ec040305a87.ico' + - source: './img/favicon.ico' + target: './favicon.ico' + + mode: 'replace' # Überschreibt vorhandene Dateien/Ordner + + + + - sources: + - './seasonals' + - './featured' + - './pictures' + + - source: './img/background.png' + target: './assets/img/background.png' + - source: './img/logo.png' + target: './assets/img/logo.png' + - source: './img/favicon.png' + target: './assets/img/favicon.png' + mode: 'copy' # Kopiert Dateien/Ordner + +# Modifikationsregeln +modification_rules: + - file_pattern: 'session-login-index-html\.[0-9a-z]+\.chunk\.js$' + insert_rules: + - after_text: '
' + insert_text: '' + + # Instancename, Jellyseer I-Frame + - file_pattern: 'index.html' + insert_rules: + # Seasonals + - before_text: '' + insert_text: '
' + + # Page title and requests tab + - before_text: 're:' + insert_text: > + + + + replace_rules: + # Page title + - old_text: 'Jellyfin' + new_text: 'SpaceCloud - Cinema' + + # Instancename, Jellyseer I-Frame + - file_pattern: '^main\.jellyfin\.bundle\.js$' + replace_rules: + # Set limit on how many days items should be in the next up section (last number) + - old_text: 'this.set("maxDaysForNextUp",e.toString(),!1);var t=parseInt(this.get("maxDaysForNextUp",!1),10);return 0===t?0:t||365}}' + new_text: 'this.set("maxDaysForNextUp",e.toString(),!1);var t=parseInt(this.get("maxDaysForNextUp",!1),10);return 0===t?0:t||28}}' + # Default user page size (last number), 99 fits perfect on most desktops + - old_text: 'this.get("libraryPageSize",!1),10);return 0===t?0:t||100}' + new_text: 'this.get("libraryPageSize",!1),10);return 0===t?0:t||99}' + + + + + - file_pattern: 'home-html\.[0-9a-z]+\.chunk\.js$' + insert_rules: + # featured iframe and requests iframe style + - after_text: 'data-backdroptype="movie,series,book">' + insert_text: > + + + + # request tab on main page + - after_text: 'id="favoritesTab" data-index="1">
' + insert_text: '
' diff --git a/customize-WebUI_v10-10-7.py b/customize-WebUI_v10-10-7.py new file mode 100644 index 0000000..7cdd27c --- /dev/null +++ b/customize-WebUI_v10-10-7.py @@ -0,0 +1,371 @@ +import os +import shutil +import yaml +import re +import sys + +# ANSI escape sequences for colors +class Colors: + CYAN = '\033[96m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + RESET = '\033[0m' + +# Global variable to track errors +errorList = {"copies": 0, "modifications": 0, "copyWarnings": 0, "modWarnings": 0} + +# Initialize results +results = {"copies": 0, "modifications": 0} + +# MARK: Load configuration +def loadConfig(configPath): + """Load YAML configuration.""" + try: + print(f"Loading configuration from {configPath} ...") + with open(configPath, 'r', encoding='utf-8') as file: + config = yaml.safe_load(file) + + # validate configuration + if not config: + raise ValueError("Empty configuration file") + + print(f"{Colors.GREEN}Configuration loaded successfully.{Colors.RESET}") + return config + except FileNotFoundError: + print(f"{Colors.RED}Error: Configuration file not found at ./{configPath}{Colors.RESET}") + sys.exit(1) + except yaml.YAMLError as e: + print(f"{Colors.RED}Error parsing YAML configuration: {e}{Colors.RESET}") + sys.exit(1) + except ValueError as e: + print(f"{Colors.RED}Configuration error: {e}{Colors.RESET}") + sys.exit(1) + + +def ensureDirectory(path): + """Ensure that a directory exists.""" + print(f"\nChecking for or creating directory: {path}") + os.makedirs(path, exist_ok=True) + + +# MARK: Copy sources +def copySources(config, destinationDirectory): + """Copy files and folders according to copy rules.""" + print(f"{Colors.YELLOW}Starting source file & folder copy and replace process...{Colors.RESET}") + for rule in config.get('copy_rules', []): + for source in rule.get('sources', []): + try: + # Distinguish between sources with explicit target and without + if isinstance(source, dict): + sourcePath = source['source'] + + checkTargetPath = source['target'] + + # Check for absolute paths in target and correct them if necessary + if os.path.isabs(checkTargetPath): + targetPath = os.path.normpath(os.path.join(destinationDirectory, checkTargetPath.lstrip('./'))) + raise ValueError(f"{Colors.RED}Absolute path incorrect: {Colors.YELLOW}Corrected {checkTargetPath} to {targetPath}.{Colors.RESET}") + else: + #targetPath = os.path.join(destinationDirectory, source['target']) # old code + targetPath = os.path.normpath(os.path.join(destinationDirectory, checkTargetPath)) + + + else: + sourcePath = source + #targetPath = os.path.join(destinationDirectory, os.path.basename(sourcePath)) # old code + targetPath = os.path.normpath(os.path.join(destinationDirectory, os.path.basename(sourcePath))) + + # Check if source exists + if not os.path.exists(sourcePath): + raise FileNotFoundError(f"Source path does not exist: {sourcePath}") + + # Create target directory + ensureDirectory(os.path.dirname(targetPath)) + + # Copy or replace mode + if rule.get('mode') == 'replace' and os.path.exists(targetPath): + print(f"Replacing existing file/folder: {targetPath}") + + # Explicitly handle directory or file deletion + if os.path.isdir(targetPath): + shutil.rmtree(targetPath) + else: + os.remove(targetPath) + + # Update mode + if rule.get('mode') == 'update' and os.path.exists(targetPath): + print(f"Checking if {sourcePath} is newer than {targetPath}") + srcMtime = os.path.getmtime(sourcePath) + destMtime = os.path.getmtime(targetPath) + if srcMtime <= destMtime: + print(f"{Colors.YELLOW}Skipping {sourcePath}: Destination is up to date{Colors.RESET}") + break # Skip this source file/folder + elif rule.get('mode') != 'update' and not os.path.exists(sourcePath): + print(f"{Colors.YELLOW}Should update {sourcePath} to {targetPath}, but {sourcePath} is not (yet) existing.{Colors.RESET}") + + # Copy files or directories + if os.path.isdir(sourcePath): + if not os.path.exists(targetPath): + print(f"{Colors.GREEN}Copying directory: {sourcePath} -> {targetPath}{Colors.RESET}") + shutil.copytree(sourcePath, targetPath) + results['copies'] += 1 + else: + mode = rule.get('mode') + if mode == 'replace': + # handled earlier + print(f"{Colors.YELLOW}Unexpected existing directory in replace mode (already handled): {targetPath}{Colors.RESET}") + errorList['copyWarnings'] += 1 + elif mode in ('copy','merge'): + added = 0 + for rootDir, subdirs, files in os.walk(sourcePath): + relRoot = os.path.relpath(rootDir, sourcePath) + relRoot = '' if relRoot == '.' else relRoot + destRoot = os.path.join(targetPath, relRoot) if relRoot else targetPath + ensureDirectory(destRoot) + for fname in files: + srcFile = os.path.join(rootDir, fname) + destFile = os.path.join(destRoot, fname) + if not os.path.exists(destFile): + shutil.copy2(srcFile, destFile) + added += 1 + if added: + print(f"{Colors.GREEN}Added {added} new file(s) into existing directory (copy merge behavior): {targetPath}{Colors.RESET}") + results['copies'] += added + else: + print(f"{Colors.YELLOW}Directory already up to date (no new files): {targetPath}{Colors.RESET}") + errorList['copyWarnings'] += 1 + else: + print(f"{Colors.YELLOW}Skipping directory copy (mode {mode}): already exists {targetPath}{Colors.RESET}") + errorList['copyWarnings'] += 1 + else: + if not os.path.exists(targetPath): + print(f"{Colors.GREEN}Copying file: {sourcePath} -> {targetPath}{Colors.RESET}") + shutil.copy2(sourcePath, targetPath) + results['copies'] += 1 + else: + print(f"{Colors.YELLOW}Skipping file copy: {sourcePath} -> {targetPath} (already exists){Colors.RESET}") + errorList['copyWarnings'] += 1 + + except FileNotFoundError as e: + print(f"\n{Colors.RED}Error: {e}{Colors.RESET}") + errorList["copies"] += 1 + continue + + except ValueError as e: + print(f"\n{Colors.RED}Configuration error: {e}{Colors.RESET}") + errorList["copies"] += 1 + continue + + except PermissionError: + print(f"\n{Colors.RED}Error: Permission denied when copying {sourcePath}{Colors.RESET}") + errorList["copies"] += 1 + continue + + except OSError as e: + print(f"{Colors.RED}Error copying {sourcePath}: {e}{Colors.RESET}") + errorList["copies"] += 1 + continue + + print(f"\n{Colors.GREEN}Source file & folder copy/replace process completed{Colors.RESET} with {Colors.RED}{errorList['copies']} errors{Colors.RESET} and {Colors.YELLOW}{errorList['copyWarnings']} warnings{Colors.RESET}.\n") + + +# MARK: Modify files +def _is_regex(marker: str) -> bool: + """Return True if the marker is flagged as regex (prefix 're:').""" + return isinstance(marker, str) and marker.startswith('re:') + + +def _extract_pattern(marker: str) -> str: + """Strip the 're:' prefix and return the regex pattern.""" + return marker[3:] + + +def modifyFiles(config, destinationDirectory): + """Modify files according to modification rules.""" + print(f"{Colors.YELLOW}Starting file modification process...{Colors.RESET}") + for rule in config.get('modification_rules', []): + filePattern = rule.get('file_pattern', '*') + print(f"\nProcessing files matching pattern: {filePattern}") + + # Recursively search the destination directory + for root, _, files in os.walk(destinationDirectory): + for filename in files: + try: + if re.match(filePattern, filename): + filePath = os.path.join(root, filename) + print(f"Modifying file: {filePath}") + + # Read file content + with open(filePath, 'r', encoding='utf-8') as f: + content = f.read() + + # Perform text replacements / insertions + for insertRule in rule.get('insert_rules', []): + # AFTER TEXT INSERTION + if 'after_text' in insertRule: + raw_after = insertRule['after_text'] + insert_text = insertRule['insert_text'].replace('\n', '') + if _is_regex(raw_after): + pattern = _extract_pattern(raw_after) + # search first occurrence + match = re.search(pattern, content, re.DOTALL) + if not match: + raise ValueError(f"Regex after_text pattern '{pattern}' not found in file: {filePath}") + anchor = match.group(0) + # idempotency check + if anchor + insert_text in content[match.start():match.start()+len(anchor)+len(insert_text)+5]: + print(f" {Colors.YELLOW}Regex after_text already has insertion after anchor.{Colors.RESET}") + errorList['modWarnings'] += 1 + else: + print(f" {Colors.GREEN}Regex inserting after anchor: /{pattern}/ -> {insert_text[:60]}...{Colors.RESET}") + content = content[:match.end()] + insert_text + content[match.end():] + results['modifications'] += 1 + else: + # Plain (substring) variant – use first occurrence only (consistent with replace count=1) + anchor = raw_after + idx = content.find(anchor) + if idx == -1: + raise ValueError(f"Text '{anchor}' not found in file: {filePath}") + after_pos = idx + len(anchor) + # Precise idempotency: is insert_text already directly after anchor? + if content.startswith(insert_text, after_pos): + print(f" {Colors.YELLOW}Plain after_text already directly followed by insertion (idempotent).{Colors.RESET}") + errorList['modWarnings'] += 1 + else: + print(f" {Colors.GREEN}Inserting text after (plain): {anchor[:40]} -> {insert_text[:60]}...{Colors.RESET}") + content = content[:after_pos] + insert_text + content[after_pos:] + results['modifications'] += 1 + + # BEFORE TEXT INSERTION + if 'before_text' in insertRule: + raw_before = insertRule['before_text'] + insert_text = insertRule['insert_text'].replace('\n', '') + if _is_regex(raw_before): + pattern = _extract_pattern(raw_before) + match = re.search(pattern, content, re.DOTALL) + if not match: + raise ValueError(f"Regex before_text pattern '{pattern}' not found in file: {filePath}") + anchor = match.group(0) + segment_start = max(0, match.start() - len(insert_text) - 5) + if insert_text + anchor in content[segment_start:match.end()+len(insert_text)]: + print(f" {Colors.YELLOW}Regex before_text already has insertion before anchor.{Colors.RESET}") + errorList['modWarnings'] += 1 + else: + print(f" {Colors.GREEN}Regex inserting before anchor: /{pattern}/ <- {insert_text[:60]}...{Colors.RESET}") + content = content[:match.start()] + insert_text + content[match.start():] + results['modifications'] += 1 + else: + # Plain (substring) variant – first occurrence logic + anchor = raw_before + idx = content.find(anchor) + if idx == -1: + raise ValueError(f"Text '{anchor}' not found in file: {filePath}") + before_pos = idx + # Precise idempotency: does insert_text already sit immediately before anchor? + if before_pos >= len(insert_text) and content[before_pos - len(insert_text): before_pos] == insert_text: + print(f" {Colors.YELLOW}Plain before_text already directly preceded by insertion (idempotent).{Colors.RESET}") + errorList['modWarnings'] += 1 + else: + print(f" {Colors.GREEN}Inserting text before (plain): {insert_text[:60]}... <- {anchor[:40]}{Colors.RESET}") + content = content[:before_pos] + insert_text + content[before_pos:] + results['modifications'] += 1 + + # REPLACE RULES + for replaceRules in rule.get('replace_rules', []): + if 'old_text' in replaceRules: + raw_old = replaceRules['old_text'] + new_text = replaceRules['new_text'] + if _is_regex(raw_old): + pattern = _extract_pattern(raw_old) + if re.search(re.escape(new_text), content): + print(f" {Colors.YELLOW}Regex replacement already applied -> {new_text[:60]}...{Colors.RESET}") + errorList['modWarnings'] += 1 + continue + if not re.search(pattern, content, re.DOTALL): + raise ValueError(f"Regex old_text pattern '{pattern}' not found in file: {filePath}") + content_new, count = re.subn(pattern, new_text, content, count=1) + if count: + print(f" {Colors.GREEN}Regex replacing pattern /{pattern}/ -> {new_text[:60]}...{Colors.RESET}") + content = content_new + results['modifications'] += 1 + else: + print(f" {Colors.YELLOW}Regex replacement produced no change for /{pattern}/.{Colors.RESET}") + errorList['modWarnings'] += 1 + else: + old_text = raw_old + if old_text not in content and new_text not in content: + raise ValueError(f"Text '{old_text}' not found in file: {filePath}") + elif new_text not in content: + print(f" {Colors.GREEN}Replacing text: {old_text[:60]}... -> {new_text[:60]}...{Colors.RESET}") + content = content.replace(old_text, new_text, 1) + results['modifications'] += 1 + else: + print(f" {Colors.YELLOW}Text already replaced: {old_text[:40]} -> {new_text[:40]}{Colors.RESET}") + errorList['modWarnings'] += 1 + + + # Write modified contents + with open(filePath, 'w', encoding='utf-8') as f: + f.write(content) + + #else: + # print(f"Skipping file: {filename} (not found)") + + except ValueError as e: + print(f"\n{Colors.RED}Error: {e}{Colors.RESET}") + errorList["modifications"] += 1 + continue + + except PermissionError: + print(f"\n{Colors.RED}Error: Permission denied when modifying {filePath}{Colors.RESET}") + errorList["modifications"] += 1 + continue + + except IOError as e: + print(f"\n{Colors.RED}Error reading/writing file {filePath}: {e}{Colors.RESET}") + errorList["modifications"] += 1 + continue + + print(f"\n{Colors.GREEN}File modification process completed{Colors.RESET} with {Colors.RED}{errorList['modifications']} errors{Colors.RESET} and {Colors.YELLOW}{errorList['modWarnings']} warnings{Colors.RESET}.\n") + + +# MARK: Main function +def main(): + """Main function to execute all operations.""" + # Check command-line argument + if len(sys.argv) < 2: + print(f"{Colors.RED}Please provide the path to the configuration file.{Colors.RESET}") + sys.exit(1) + + configPath = sys.argv[1] + + # Load configuration + config = loadConfig(configPath) + + # Ensure destination directory + destinationDirectory = config.get('destination_directory', './web') + ensureDirectory(destinationDirectory) + + # Copy files and folders + copySources(config, destinationDirectory) + + # Modify files + modifyFiles(config, destinationDirectory) + + # Print results + print(f'\n{Colors.GREEN}Total successful copies: {results["copies"]}') + print(f'Total file modifications: {results["modifications"]}{Colors.RESET}') + + if errorList["copies"] > 0 or errorList["modifications"] > 0: + print(f"{Colors.RED}Errors occurred during the process. Check the output for details.") + print(f"Total copy errors: {errorList['copies']}") + print(f"Total modification errors: {errorList['modifications']}{Colors.RESET}") + elif errorList["copyWarnings"] > 0 or errorList["modWarnings"] > 0: + print(f"{Colors.GREEN}All operations in {destinationDirectory} from {configPath} completed successfully!{Colors.YELLOW} But there were {errorList['modWarnings'] + errorList['copyWarnings']} warnings. Maybe you should check them.{Colors.RESET}") + else: + print(f"{Colors.GREEN}All operations in {destinationDirectory} from {configPath} completed successfully!{Colors.RESET}") + + +if __name__ == '__main__': + main() \ No newline at end of file