adapted to v10.11.x
This commit is contained in:
19
README.md
19
README.md
@@ -16,6 +16,7 @@ Below is an overview of its main functionality and usage:
|
|||||||
- [Simple example yaml](#simple-example-yaml)
|
- [Simple example yaml](#simple-example-yaml)
|
||||||
- [Running the Script](#running-the-script)
|
- [Running the Script](#running-the-script)
|
||||||
- [Regex Marker](#regex-marker)
|
- [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.
|
- Inserting text before or after specific content.
|
||||||
- Replacing old text with new text.
|
- 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: 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.
|
- Recursively applies changes to files matching specified patterns in the destination directory.
|
||||||
|
|
||||||
5. **Error and Warning Tracking**:
|
5. **Error and Warning Tracking**:
|
||||||
@@ -75,7 +77,7 @@ copy_rules:
|
|||||||
- sources: # Files/folders to copy
|
- sources: # Files/folders to copy
|
||||||
- './source_folder' # Entire folder located from where you called the script
|
- './source_folder' # Entire folder located from where you called the script
|
||||||
- source: "./src/file.js"
|
- 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
|
mode: "replace" # Options: replace, update
|
||||||
|
|
||||||
- sources:
|
- sources:
|
||||||
@@ -123,4 +125,17 @@ old_text: 're:this\\.get\("libraryPageSize",!1\),10\);return 0===t\?0:t\|\|10
|
|||||||
Notes:
|
Notes:
|
||||||
1. Properly escape backslashes (YAML + Regex!)
|
1. Properly escape backslashes (YAML + Regex!)
|
||||||
2. The first match will be used (count=1 for replacement/insertion)
|
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
|
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:
|
copy_rules:
|
||||||
- sources:
|
- sources:
|
||||||
- source: './img/icon-transparent.png'
|
- source: './img/icon-transparent.png'
|
||||||
target: './assets/img/icon-transparent.png'
|
target: './icon-transparent*.png'
|
||||||
- source: './img/banner-light.png'
|
- source: './img/banner-light.png'
|
||||||
target: './assets/img/banner-light.png'
|
target: './banner-light*.png'
|
||||||
- source: './img/banner-dark.png'
|
- source: './img/banner-dark.png'
|
||||||
target: './assets/img/banner-dark.png'
|
target: './banner-dark*.png'
|
||||||
|
|
||||||
- source: './img/bc8d51405ec040305a87.ico'
|
- source: './img/favicon_32x32.ico'
|
||||||
target: './bc8d51405ec040305a87.ico'
|
target: './favicon*.ico'
|
||||||
- source: './img/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
|
mode: 'replace' # Überschreibt vorhandene Dateien/Ordner
|
||||||
|
|
||||||
@@ -29,8 +36,6 @@ copy_rules:
|
|||||||
target: './assets/img/background.png'
|
target: './assets/img/background.png'
|
||||||
- source: './img/logo.png'
|
- source: './img/logo.png'
|
||||||
target: './assets/img/logo.png'
|
target: './assets/img/logo.png'
|
||||||
- source: './img/favicon.png'
|
|
||||||
target: './assets/img/favicon.png'
|
|
||||||
mode: 'copy' # Kopiert Dateien/Ordner
|
mode: 'copy' # Kopiert Dateien/Ordner
|
||||||
|
|
||||||
# Modifikationsregeln
|
# Modifikationsregeln
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import shutil
|
|||||||
import yaml
|
import yaml
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from glob import glob
|
||||||
|
|
||||||
# ANSI escape sequences for colors
|
# ANSI escape sequences for colors
|
||||||
class Colors:
|
class Colors:
|
||||||
@@ -49,6 +50,56 @@ def ensureDirectory(path):
|
|||||||
os.makedirs(path, exist_ok=True)
|
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
|
# MARK: Copy sources
|
||||||
def copySources(config, destinationDirectory):
|
def copySources(config, destinationDirectory):
|
||||||
"""Copy files and folders according to copy rules."""
|
"""Copy files and folders according to copy rules."""
|
||||||
@@ -58,23 +109,24 @@ def copySources(config, destinationDirectory):
|
|||||||
try:
|
try:
|
||||||
# Distinguish between sources with explicit target and without
|
# Distinguish between sources with explicit target and without
|
||||||
if isinstance(source, dict):
|
if isinstance(source, dict):
|
||||||
sourcePath = source['source']
|
sourcePath = resolveSourcePath(source['source'])
|
||||||
|
|
||||||
checkTargetPath = source['target']
|
checkTargetPath = source['target']
|
||||||
|
|
||||||
# Check for absolute paths in target and correct them if necessary
|
# Check for absolute paths in target and correct them if necessary
|
||||||
if os.path.isabs(checkTargetPath):
|
if os.path.isabs(checkTargetPath):
|
||||||
targetPath = os.path.normpath(os.path.join(destinationDirectory, checkTargetPath.lstrip('./')))
|
corrected = 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}")
|
raise ValueError(f"{Colors.RED}Absolute path incorrect: {Colors.YELLOW}Corrected {checkTargetPath} to {corrected}.{Colors.RESET}")
|
||||||
else:
|
|
||||||
#targetPath = os.path.join(destinationDirectory, source['target']) # old code
|
targetPath = resolveTargetPath(checkTargetPath, destinationDirectory)
|
||||||
targetPath = os.path.normpath(os.path.join(destinationDirectory, checkTargetPath))
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
sourcePath = source
|
sourcePath = resolveSourcePath(source)
|
||||||
#targetPath = os.path.join(destinationDirectory, os.path.basename(sourcePath)) # old code
|
if os.path.isdir(sourcePath):
|
||||||
targetPath = os.path.normpath(os.path.join(destinationDirectory, os.path.basename(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
|
# Check if source exists
|
||||||
if not os.path.exists(sourcePath):
|
if not os.path.exists(sourcePath):
|
||||||
|
|||||||
Reference in New Issue
Block a user