From d9609f34ac9f6d8f88ee548632a0ff57ec9469b3 Mon Sep 17 00:00:00 2001 From: CodeDevMLH <145071728+CodeDevMLH@users.noreply.github.com> Date: Sun, 2 Nov 2025 01:36:32 +0100 Subject: [PATCH] adapted to v10.11.x --- README.md | 19 +++++++++++-- config.yaml | 21 ++++++++------ customize-WebUI.py | 70 ++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 91 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2a065db..a6cdf3a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Below is an overview of its main functionality and usage: - [Simple example yaml](#simple-example-yaml) - [Running the Script](#running-the-script) - [Regex Marker](#regex-marker) + - [Hashed assets](#hashed-assets) --- @@ -42,6 +43,7 @@ Below is an overview of its main functionality and usage: - Inserting text before or after specific content. - Replacing old text with new text. - NEW: Regex support (prefix markers with `re:`) for `before_text`, `after_text`, and `old_text` to handle dynamical hashes (e.g. Jellyfin assets) + - NEW: Wildcard-aware source/target resolution keeps hashed filenames in sync (use `icon*.png` or `icon?.png` patterns to overwrite existing hashed assets without renaming them) - Recursively applies changes to files matching specified patterns in the destination directory. 5. **Error and Warning Tracking**: @@ -75,7 +77,7 @@ copy_rules: - sources: # Files/folders to copy - './source_folder' # Entire folder located from where you called the script - source: "./src/file.js" - target: "./dist/file.js" + target: "./dist/file*.js" # search and replace file.randomHash.js with file.js as file.sameRandomHash.js mode: "replace" # Options: replace, update - sources: @@ -123,4 +125,17 @@ old_text: 're:this\\.get\("libraryPageSize",!1\),10\);return 0===t\?0:t\|\|10 Notes: 1. Properly escape backslashes (YAML + Regex!) 2. The first match will be used (count=1 for replacement/insertion) -3. Idempotency: The script checks whether the insertion or replacement text already exists to avoid duplicates \ No newline at end of file +3. Idempotency: The script checks whether the insertion or replacement text already exists to avoid duplicates + +### Hashed assets +When Jellyfin (or any build pipeline) appends random hashes to filenames, point both `source` and `target` to wildcard patterns. Example: + +```yaml +copy_rules: + - sources: + - source: './img/banner-light.png' + target: './assets/img/banner-light*.png' + mode: 'replace' +``` + +The script resolves the source file as usual, finds the latest matching target file like `banner-light.b113d4d1c6c07fcb73f0.png`, and overwrites it in-place. If no match exists yet, it falls back to a non-hashed filename so assets are still created on first run. \ No newline at end of file diff --git a/config.yaml b/config.yaml index cf7c215..92608b2 100644 --- a/config.yaml +++ b/config.yaml @@ -5,16 +5,23 @@ destination_directory: './web' copy_rules: - sources: - source: './img/icon-transparent.png' - target: './assets/img/icon-transparent.png' + target: './icon-transparent*.png' - source: './img/banner-light.png' - target: './assets/img/banner-light.png' + target: './banner-light*.png' - source: './img/banner-dark.png' - target: './assets/img/banner-dark.png' + target: './banner-dark*.png' - - source: './img/bc8d51405ec040305a87.ico' - target: './bc8d51405ec040305a87.ico' + - source: './img/favicon_32x32.ico' + target: './favicon*.ico' - source: './img/favicon.ico' - target: './favicon.ico' + target: './favicon*.ico' + - source: './img/touchicon_180x180.png' + target: './touchicon*.png' + - source: './img/notificationicon.png' + target: './notificationicon*.png' + + - source: './img/favicons_dir' + target: './favicons' mode: 'replace' # Überschreibt vorhandene Dateien/Ordner @@ -29,8 +36,6 @@ copy_rules: 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 diff --git a/customize-WebUI.py b/customize-WebUI.py index 7cdd27c..6bebc47 100644 --- a/customize-WebUI.py +++ b/customize-WebUI.py @@ -3,6 +3,7 @@ import shutil import yaml import re import sys +from glob import glob # ANSI escape sequences for colors class Colors: @@ -49,6 +50,56 @@ def ensureDirectory(path): os.makedirs(path, exist_ok=True) +def resolveSourcePath(rawPath): + """Resolve a source path and support glob patterns for hashed filenames.""" + normalized = os.path.normpath(rawPath) + + # Direct hit fast path + if os.path.exists(normalized): + return normalized + + # Allow globbing when hashed suffixes change per build (e.g. banner-light.*.png) + if any(token in rawPath for token in ('*', '?', '[')): + matches = glob(rawPath) + if matches: + # Prefer the most recently modified match to follow the latest build output + matches.sort(key=os.path.getmtime, reverse=True) + chosen = os.path.normpath(matches[0]) + if len(matches) > 1: + print(f"Multiple matches for pattern {rawPath}, selecting newest: {chosen}") + else: + print(f"Resolved pattern {rawPath} -> {chosen}") + return chosen + raise FileNotFoundError(f"Pattern '{rawPath}' did not match any files") + + # Nothing found -> propagate missing file + raise FileNotFoundError(f"Source path does not exist: {rawPath}") + + +def resolveTargetPath(rawTarget, destinationDirectory): + """Resolve destination path, allowing wildcard patterns to reuse hashed filenames.""" + relativeTarget = rawTarget.lstrip('./') + targetPattern = os.path.normpath(os.path.join(destinationDirectory, relativeTarget)) + + if any(token in relativeTarget for token in ('*', '?', '[')): + matches = glob(targetPattern) + if matches: + matches.sort(key=os.path.getmtime, reverse=True) + chosen = os.path.normpath(matches[0]) + if len(matches) > 1: + print(f"Multiple destination matches for {rawTarget}, selecting newest: {chosen}") + else: + print(f"Resolved destination pattern {rawTarget} -> {chosen}") + return chosen + + sanitizedName = re.sub(r'[\*\?\[\]]', '', os.path.basename(relativeTarget)) + fallback = os.path.normpath(os.path.join(os.path.dirname(targetPattern), sanitizedName)) + print(f"No existing destination matched {rawTarget}, using fallback: {fallback}") + return fallback + + return targetPattern + + # MARK: Copy sources def copySources(config, destinationDirectory): """Copy files and folders according to copy rules.""" @@ -58,23 +109,24 @@ def copySources(config, destinationDirectory): try: # Distinguish between sources with explicit target and without if isinstance(source, dict): - sourcePath = source['source'] + sourcePath = resolveSourcePath(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)) + corrected = os.path.normpath(os.path.join(destinationDirectory, checkTargetPath.lstrip('./'))) + raise ValueError(f"{Colors.RED}Absolute path incorrect: {Colors.YELLOW}Corrected {checkTargetPath} to {corrected}.{Colors.RESET}") + + targetPath = resolveTargetPath(checkTargetPath, destinationDirectory) 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))) + sourcePath = resolveSourcePath(source) + if os.path.isdir(sourcePath): + targetPath = os.path.normpath(os.path.join(destinationDirectory, os.path.basename(sourcePath))) + else: + targetPath = resolveTargetPath(os.path.basename(sourcePath), destinationDirectory) # Check if source exists if not os.path.exists(sourcePath):