Add regex-based path preprocessing for playlist sync
This commit is contained in:
@@ -15,6 +15,7 @@ class ServerConfig:
|
||||
self.url = ""
|
||||
self.scheme = "https"
|
||||
self.port = "32400"
|
||||
self.path_rules: list[dict[str, str]] = []
|
||||
self.load()
|
||||
|
||||
def load(self) -> None:
|
||||
@@ -37,6 +38,7 @@ class ServerConfig:
|
||||
self.url = config.get("server_url", "")
|
||||
self.scheme = config.get("server_scheme", "https")
|
||||
self.port = config.get("server_port", "32400")
|
||||
self.path_rules = config.get("path_rules", []) or []
|
||||
logger.info(f"Server config loaded: {self.__dict__}")
|
||||
|
||||
def save(self):
|
||||
@@ -46,6 +48,7 @@ class ServerConfig:
|
||||
"server_url": self.url,
|
||||
"server_scheme": self.scheme,
|
||||
"server_port": self.port,
|
||||
"path_rules": self.path_rules,
|
||||
}
|
||||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=4, ensure_ascii=False)
|
||||
@@ -70,6 +73,9 @@ class ServerConfig:
|
||||
raise ValueError("Invalid theme. Must be 'auto', 'dark', or 'light'.")
|
||||
self.theme = theme
|
||||
|
||||
def set_path_rules(self, path_rules: list[dict[str, str]]) -> None:
|
||||
self.path_rules = path_rules or []
|
||||
|
||||
def set_and_save_config(
|
||||
self,
|
||||
theme: str = None,
|
||||
@@ -77,6 +83,7 @@ class ServerConfig:
|
||||
url: str = None,
|
||||
scheme: str = None,
|
||||
port: str = None,
|
||||
path_rules: list[dict[str, str]] | None = None,
|
||||
) -> None:
|
||||
if theme is not None:
|
||||
self.set_theme(theme)
|
||||
@@ -88,6 +95,8 @@ class ServerConfig:
|
||||
self.set_scheme(scheme)
|
||||
if port is not None:
|
||||
self.set_port(port)
|
||||
if path_rules is not None:
|
||||
self.set_path_rules(path_rules)
|
||||
self.save()
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
@@ -70,6 +71,60 @@ def save_paths(paths: Sequence[str]) -> str:
|
||||
return "#EXTM3U\n" + "\n".join(paths) + "\n"
|
||||
|
||||
|
||||
def _compile_regex_rules(rules: Sequence[dict[str, str]]) -> list[tuple[re.Pattern[str], str]]:
|
||||
compiled: list[tuple[re.Pattern[str], str]] = []
|
||||
for rule in rules:
|
||||
pattern = rule.get("pattern")
|
||||
if not pattern:
|
||||
continue
|
||||
replacement = rule.get("replacement", "")
|
||||
try:
|
||||
compiled.append((re.compile(pattern), replacement))
|
||||
except re.error as exc:
|
||||
logger.warning(f"Skipping invalid regex '{pattern}': {exc}")
|
||||
return compiled
|
||||
|
||||
|
||||
def _apply_compiled_rules_to_paths(
|
||||
paths: Sequence[str], compiled_rules: Sequence[tuple[re.Pattern[str], str]]
|
||||
) -> list[str]:
|
||||
if not compiled_rules:
|
||||
return list(paths)
|
||||
|
||||
updated: list[str] = []
|
||||
for path in paths:
|
||||
new_path = path
|
||||
for pattern, replacement in compiled_rules:
|
||||
new_path = pattern.sub(replacement, new_path)
|
||||
updated.append(new_path)
|
||||
return updated
|
||||
|
||||
|
||||
def apply_regex_rules_to_paths(
|
||||
paths: Sequence[str], rules: Sequence[dict[str, str]]
|
||||
) -> list[str]:
|
||||
"""Apply regex replacement rules to each path in order."""
|
||||
|
||||
compiled_rules = _compile_regex_rules(rules)
|
||||
return _apply_compiled_rules_to_paths(paths, compiled_rules)
|
||||
|
||||
|
||||
def preprocess_playlist_text(
|
||||
text: str,
|
||||
rules: Sequence[dict[str, str]],
|
||||
compiled_rules: Sequence[tuple[re.Pattern[str], str]] | None = None,
|
||||
) -> str:
|
||||
"""Normalize playlist text and apply regex replacements."""
|
||||
|
||||
if not text:
|
||||
text = "#EXTM3U\n"
|
||||
|
||||
paths = load_paths(text)
|
||||
compiled = compiled_rules if compiled_rules is not None else _compile_regex_rules(rules)
|
||||
replaced_paths = _apply_compiled_rules_to_paths(paths, compiled)
|
||||
return save_paths(replaced_paths)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MergeChunk:
|
||||
type: Literal["normal", "conflict"]
|
||||
@@ -514,6 +569,8 @@ def sync_all_playlists(
|
||||
) -> list[PlaylistSyncResult]:
|
||||
"""Synchronize all playlists that can be matched by name."""
|
||||
|
||||
server_config.load()
|
||||
compiled_rules = _compile_regex_rules(server_config.path_rules)
|
||||
_ensure_test_dir(test_folder)
|
||||
local_playlists = _load_local_playlists(local_dir)
|
||||
remote_playlists = _fetch_remote_playlists()
|
||||
@@ -541,6 +598,17 @@ def sync_all_playlists(
|
||||
remote_text = snapshot_remote_text
|
||||
remote_present = bool(remote_text.strip()) or remote_exists
|
||||
|
||||
base_text = preprocess_playlist_text(
|
||||
base_text, server_config.path_rules, compiled_rules
|
||||
)
|
||||
remote_text = preprocess_playlist_text(
|
||||
remote_text, server_config.path_rules, compiled_rules
|
||||
)
|
||||
if local_text is not None:
|
||||
local_text = preprocess_playlist_text(
|
||||
local_text, server_config.path_rules, compiled_rules
|
||||
)
|
||||
|
||||
# Treat missing remote text as absent playlist.
|
||||
result = _sync_single_playlist(
|
||||
playlist=playlist,
|
||||
|
||||
Reference in New Issue
Block a user