Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 304e973db1 | |||
| 6c84112d29 | |||
| 1131b81454 | |||
| 6a1780bcee |
+19
-3
@@ -2,11 +2,27 @@
|
||||
"theme": "auto",
|
||||
"token": "",
|
||||
"server_url": "",
|
||||
"server_port": "32400",
|
||||
"server_scheme": "https",
|
||||
"server_port": "32400",
|
||||
"timeout": 9,
|
||||
"library_name": "",
|
||||
"sync_mode": "merge_local_primary",
|
||||
"local_path": "playlist",
|
||||
"path_rules": []
|
||||
}
|
||||
"path_rules": [],
|
||||
"path_mapping": {
|
||||
"mode": "SIMPLE",
|
||||
"simple": [],
|
||||
"regex": {
|
||||
"local_pre": [],
|
||||
"local_post": [],
|
||||
"remote_pre": [],
|
||||
"remote_post": []
|
||||
}
|
||||
},
|
||||
"schedule_mode": "DISABLED",
|
||||
"schedule_cron": "",
|
||||
"schedule_daily_time": "02:00",
|
||||
"schedule_weekly_days": [0],
|
||||
"schedule_weekly_time": "03:00",
|
||||
"schedule_auto_watch": false
|
||||
}
|
||||
|
||||
+6
-5
@@ -93,6 +93,7 @@ class RegexRule(BaseModel):
|
||||
|
||||
|
||||
class ReplacementRule(BaseModel):
|
||||
id: str = ""
|
||||
search: str
|
||||
replace: str = ""
|
||||
|
||||
@@ -404,12 +405,12 @@ async def update_regex_rules(payload: RegexRulePayload):
|
||||
async def update_path_mapping(payload: PathMappingPayload):
|
||||
path_mapping_dict = {
|
||||
"mode": payload.mode,
|
||||
"simple": [{"search": rule.search, "replace": rule.replace} for rule in payload.simple],
|
||||
"simple": [{"id": rule.id, "search": rule.search, "replace": rule.replace} for rule in payload.simple],
|
||||
"regex": {
|
||||
"local_pre": [{"search": rule.search, "replace": rule.replace} for rule in payload.regex.local_pre],
|
||||
"local_post": [{"search": rule.search, "replace": rule.replace} for rule in payload.regex.local_post],
|
||||
"remote_pre": [{"search": rule.search, "replace": rule.replace} for rule in payload.regex.remote_pre],
|
||||
"remote_post": [{"search": rule.search, "replace": rule.replace} for rule in payload.regex.remote_post],
|
||||
"local_pre": [{"id": rule.id, "search": rule.search, "replace": rule.replace} for rule in payload.regex.local_pre],
|
||||
"local_post": [{"id": rule.id, "search": rule.search, "replace": rule.replace} for rule in payload.regex.local_post],
|
||||
"remote_pre": [{"id": rule.id, "search": rule.search, "replace": rule.replace} for rule in payload.regex.remote_pre],
|
||||
"remote_post": [{"id": rule.id, "search": rule.search, "replace": rule.replace} for rule in payload.regex.remote_post],
|
||||
}
|
||||
}
|
||||
server_config.set_and_save_config(path_mapping=path_mapping_dict)
|
||||
|
||||
@@ -683,34 +683,58 @@ def _compile_simple_mapping_rules(simple_mappings: list[dict]) -> CompiledRegexR
|
||||
logger.warning(f"Skipping mapping with missing cloud path: {mapping}")
|
||||
continue
|
||||
|
||||
# Normalize Windows paths: Replace double backslashes with single backslashes
|
||||
# This handles cases where users enter escaped paths like \\Koha9-Main\\Music
|
||||
# when the actual playlist content uses \Koha9-Main\Music
|
||||
original_local = local_path
|
||||
original_cloud = cloud_path
|
||||
local_path = local_path.replace("\\\\", "\\")
|
||||
cloud_path = cloud_path.replace("\\\\", "\\")
|
||||
|
||||
if local_path != original_local or cloud_path != original_cloud:
|
||||
logger.info(f"Normalized Windows paths:")
|
||||
logger.info(f" Local: {repr(original_local)} -> {repr(local_path)}")
|
||||
logger.info(f" Cloud: {repr(original_cloud)} -> {repr(cloud_path)}")
|
||||
|
||||
# Create a unique placeholder using the validated UUID
|
||||
# Using special markers to prevent conflicts with actual paths
|
||||
placeholder = f"__MAPPING__{mapping_id}__"
|
||||
|
||||
# Debug logging for path mapping
|
||||
logger.debug(f"Simple mapping pair:")
|
||||
logger.debug(f" Local path (search): {repr(local_path)}")
|
||||
logger.debug(f" Cloud path (replace): {repr(cloud_path)}")
|
||||
logger.debug(f" Placeholder: {placeholder}")
|
||||
|
||||
# Pre-processing rules (use re.escape to treat paths as literal strings)
|
||||
# local_pre: Replace local path with placeholder
|
||||
local_pattern = re.escape(local_path)
|
||||
logger.debug(f" Local pre pattern: {repr(local_pattern)}")
|
||||
local_pre_rules.append({
|
||||
"pattern": re.escape(local_path),
|
||||
"pattern": local_pattern,
|
||||
"replacement": placeholder
|
||||
})
|
||||
|
||||
# remote_pre: Replace cloud path with placeholder
|
||||
remote_pattern = re.escape(cloud_path)
|
||||
logger.debug(f" Remote pre pattern: {repr(remote_pattern)}")
|
||||
remote_pre_rules.append({
|
||||
"pattern": re.escape(cloud_path),
|
||||
"pattern": remote_pattern,
|
||||
"replacement": placeholder
|
||||
})
|
||||
|
||||
# Post-processing rules
|
||||
# local_post: Replace placeholder with local path
|
||||
# Note: In regex replacement, backslashes need to be escaped
|
||||
local_post_rules.append({
|
||||
"pattern": re.escape(placeholder),
|
||||
"replacement": local_path
|
||||
"replacement": local_path.replace("\\", "\\\\")
|
||||
})
|
||||
|
||||
# remote_post: Replace placeholder with cloud path
|
||||
remote_post_rules.append({
|
||||
"pattern": re.escape(placeholder),
|
||||
"replacement": cloud_path
|
||||
"replacement": cloud_path.replace("\\", "\\\\")
|
||||
})
|
||||
|
||||
logger.info(f"Compiled {len(local_pre_rules)} simple mapping pairs into rules")
|
||||
@@ -803,13 +827,19 @@ def sync_all_playlists(
|
||||
# Apply pre-processing rules for REGEX or SIMPLE mode
|
||||
# base_text doesn't need pre-processing as it's the normalized state
|
||||
if local_text is not None and compiled_rules.local_pre:
|
||||
logger.debug(f"Applying local_pre rules to playlist: {playlist}")
|
||||
logger.debug(f" Before preprocessing (first 200 chars): {repr(local_text[:200])}")
|
||||
local_text = preprocess_playlist_text(
|
||||
local_text, [], compiled_rules.local_pre
|
||||
)
|
||||
logger.debug(f" After preprocessing (first 200 chars): {repr(local_text[:200])}")
|
||||
if remote_text and compiled_rules.remote_pre:
|
||||
logger.debug(f"Applying remote_pre rules to playlist: {playlist}")
|
||||
logger.debug(f" Before preprocessing (first 200 chars): {repr(remote_text[:200])}")
|
||||
remote_text = preprocess_playlist_text(
|
||||
remote_text, [], compiled_rules.remote_pre
|
||||
)
|
||||
logger.debug(f" After preprocessing (first 200 chars): {repr(remote_text[:200])}")
|
||||
elif legacy_compiled_rules:
|
||||
# Use legacy preprocessing for all texts
|
||||
base_text = preprocess_playlist_text(
|
||||
|
||||
@@ -22,6 +22,20 @@ import {
|
||||
Code2
|
||||
} from 'lucide-react';
|
||||
|
||||
// Generate a UUID for mapping rules
|
||||
const generateUUID = (): string => {
|
||||
// Use crypto.randomUUID() if available (modern browsers)
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
// Fallback to manual UUID v4 generation
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
interface StrategyOption {
|
||||
value: SyncStrategy;
|
||||
label: string;
|
||||
@@ -137,7 +151,7 @@ const MappingGroupEditor: React.FC<MappingGroupEditorProps> = ({
|
||||
|
||||
const handleAdd = () => {
|
||||
if (isLocked) return;
|
||||
const newId = Date.now().toString() + Math.random().toString();
|
||||
const newId = generateUUID();
|
||||
onChange([...rules, { id: newId, search: '', replace: '' }]);
|
||||
};
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ const mapPathMappingConfig = (data: any): PathMappingConfig => {
|
||||
// Helper function to convert PathMappingConfig to API format
|
||||
const pathMappingToApi = (config: PathMappingConfig) => {
|
||||
const rulesToApi = (rules: ReplacementRule[]) =>
|
||||
rules.map(({ search, replace }) => ({ search, replace }));
|
||||
rules.map(({ id, search, replace }) => ({ id, search, replace }));
|
||||
|
||||
return {
|
||||
mode: config.mode,
|
||||
|
||||
Reference in New Issue
Block a user