Compare commits
4 Commits
fbafe75fae
...
304e973db1
| Author | SHA1 | Date | |
|---|---|---|---|
| 304e973db1 | |||
| 6c84112d29 | |||
| 1131b81454 | |||
| 6a1780bcee |
+18
-2
@@ -2,11 +2,27 @@
|
|||||||
"theme": "auto",
|
"theme": "auto",
|
||||||
"token": "",
|
"token": "",
|
||||||
"server_url": "",
|
"server_url": "",
|
||||||
"server_port": "32400",
|
|
||||||
"server_scheme": "https",
|
"server_scheme": "https",
|
||||||
|
"server_port": "32400",
|
||||||
"timeout": 9,
|
"timeout": 9,
|
||||||
"library_name": "",
|
"library_name": "",
|
||||||
"sync_mode": "merge_local_primary",
|
"sync_mode": "merge_local_primary",
|
||||||
"local_path": "playlist",
|
"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):
|
class ReplacementRule(BaseModel):
|
||||||
|
id: str = ""
|
||||||
search: str
|
search: str
|
||||||
replace: str = ""
|
replace: str = ""
|
||||||
|
|
||||||
@@ -404,12 +405,12 @@ async def update_regex_rules(payload: RegexRulePayload):
|
|||||||
async def update_path_mapping(payload: PathMappingPayload):
|
async def update_path_mapping(payload: PathMappingPayload):
|
||||||
path_mapping_dict = {
|
path_mapping_dict = {
|
||||||
"mode": payload.mode,
|
"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": {
|
"regex": {
|
||||||
"local_pre": [{"search": rule.search, "replace": rule.replace} for rule in payload.regex.local_pre],
|
"local_pre": [{"id": rule.id, "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],
|
"local_post": [{"id": rule.id, "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_pre": [{"id": rule.id, "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],
|
"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)
|
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}")
|
logger.warning(f"Skipping mapping with missing cloud path: {mapping}")
|
||||||
continue
|
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
|
# Create a unique placeholder using the validated UUID
|
||||||
# Using special markers to prevent conflicts with actual paths
|
# Using special markers to prevent conflicts with actual paths
|
||||||
placeholder = f"__MAPPING__{mapping_id}__"
|
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)
|
# Pre-processing rules (use re.escape to treat paths as literal strings)
|
||||||
# local_pre: Replace local path with placeholder
|
# 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({
|
local_pre_rules.append({
|
||||||
"pattern": re.escape(local_path),
|
"pattern": local_pattern,
|
||||||
"replacement": placeholder
|
"replacement": placeholder
|
||||||
})
|
})
|
||||||
|
|
||||||
# remote_pre: Replace cloud path with 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({
|
remote_pre_rules.append({
|
||||||
"pattern": re.escape(cloud_path),
|
"pattern": remote_pattern,
|
||||||
"replacement": placeholder
|
"replacement": placeholder
|
||||||
})
|
})
|
||||||
|
|
||||||
# Post-processing rules
|
# Post-processing rules
|
||||||
# local_post: Replace placeholder with local path
|
# local_post: Replace placeholder with local path
|
||||||
|
# Note: In regex replacement, backslashes need to be escaped
|
||||||
local_post_rules.append({
|
local_post_rules.append({
|
||||||
"pattern": re.escape(placeholder),
|
"pattern": re.escape(placeholder),
|
||||||
"replacement": local_path
|
"replacement": local_path.replace("\\", "\\\\")
|
||||||
})
|
})
|
||||||
|
|
||||||
# remote_post: Replace placeholder with cloud path
|
# remote_post: Replace placeholder with cloud path
|
||||||
remote_post_rules.append({
|
remote_post_rules.append({
|
||||||
"pattern": re.escape(placeholder),
|
"pattern": re.escape(placeholder),
|
||||||
"replacement": cloud_path
|
"replacement": cloud_path.replace("\\", "\\\\")
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info(f"Compiled {len(local_pre_rules)} simple mapping pairs into rules")
|
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
|
# Apply pre-processing rules for REGEX or SIMPLE mode
|
||||||
# base_text doesn't need pre-processing as it's the normalized state
|
# base_text doesn't need pre-processing as it's the normalized state
|
||||||
if local_text is not None and compiled_rules.local_pre:
|
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 = preprocess_playlist_text(
|
||||||
local_text, [], compiled_rules.local_pre
|
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:
|
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 = preprocess_playlist_text(
|
||||||
remote_text, [], compiled_rules.remote_pre
|
remote_text, [], compiled_rules.remote_pre
|
||||||
)
|
)
|
||||||
|
logger.debug(f" After preprocessing (first 200 chars): {repr(remote_text[:200])}")
|
||||||
elif legacy_compiled_rules:
|
elif legacy_compiled_rules:
|
||||||
# Use legacy preprocessing for all texts
|
# Use legacy preprocessing for all texts
|
||||||
base_text = preprocess_playlist_text(
|
base_text = preprocess_playlist_text(
|
||||||
|
|||||||
@@ -22,6 +22,20 @@ import {
|
|||||||
Code2
|
Code2
|
||||||
} from 'lucide-react';
|
} 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 {
|
interface StrategyOption {
|
||||||
value: SyncStrategy;
|
value: SyncStrategy;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -137,7 +151,7 @@ const MappingGroupEditor: React.FC<MappingGroupEditorProps> = ({
|
|||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
if (isLocked) return;
|
if (isLocked) return;
|
||||||
const newId = Date.now().toString() + Math.random().toString();
|
const newId = generateUUID();
|
||||||
onChange([...rules, { id: newId, search: '', replace: '' }]);
|
onChange([...rules, { id: newId, search: '', replace: '' }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const mapPathMappingConfig = (data: any): PathMappingConfig => {
|
|||||||
// Helper function to convert PathMappingConfig to API format
|
// Helper function to convert PathMappingConfig to API format
|
||||||
const pathMappingToApi = (config: PathMappingConfig) => {
|
const pathMappingToApi = (config: PathMappingConfig) => {
|
||||||
const rulesToApi = (rules: ReplacementRule[]) =>
|
const rulesToApi = (rules: ReplacementRule[]) =>
|
||||||
rules.map(({ search, replace }) => ({ search, replace }));
|
rules.map(({ id, search, replace }) => ({ id, search, replace }));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode: config.mode,
|
mode: config.mode,
|
||||||
|
|||||||
Reference in New Issue
Block a user