adapted to v10.11.x

This commit is contained in:
CodeDevMLH
2025-11-02 01:36:32 +01:00
parent 1c466adde9
commit d9609f34ac
3 changed files with 91 additions and 19 deletions

View File

@@ -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:
@@ -124,3 +126,16 @@ 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
### 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.

View File

@@ -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

View File

@@ -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):