adapted to v10.11.x
This commit is contained in:
17
README.md
17
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:
|
||||
@@ -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.
|
||||
21
config.yaml
21
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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user