Add Path Mapping UI with Simple Mapping and Regex Rules modes
- Updated frontend/types.ts with new types: ReplacementRule, PathMappingRules, PathMappingMode, PathMappingConfig - Replaced StrategySelector.tsx with new UI featuring: - Simple Mapping tab for local/cloud path pairs - Regex Rules tab with 4 rule groups (localPre, localPost, remotePre, remotePost) - MappingGroupEditor sub-component for editing rule lists - Updated App.tsx to use PathMappingConfig state instead of RegexReplacement[] - Updated api.ts to handle new PathMappingConfig structure - Updated backend config.py with path_mapping field support - Added /api/settings/path-mapping endpoint in main.py Co-authored-by: Koha9 <36852125+Koha9@users.noreply.github.com>
This commit is contained in:
+61
-13
@@ -1,4 +1,4 @@
|
||||
import { Playlist, ServerType, ApiResponse, PlexServerConnection, PlexConnectionSettings, PlexLibrary, RegexReplacement, SyncStrategy, ScheduleSettings } from '../types';
|
||||
import { Playlist, ServerType, ApiResponse, PlexServerConnection, PlexConnectionSettings, PlexLibrary, ReplacementRule, PathMappingConfig, PathMappingMode, PathMappingRules, SyncStrategy, ScheduleSettings } from '../types';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
@@ -41,21 +41,69 @@ const mapLibrary = (item: any): PlexLibrary => ({
|
||||
type: item.type ?? 'artist',
|
||||
});
|
||||
|
||||
const mapRegexRules = (rules: any[]): RegexReplacement[] =>
|
||||
// Helper function to map raw rules array to ReplacementRule[]
|
||||
const mapReplacementRules = (rules: any[]): ReplacementRule[] =>
|
||||
(rules || []).map((rule, index) => ({
|
||||
id: rule.id || `${rule.pattern || 'rule'}-${index}`,
|
||||
pattern: rule.pattern || '',
|
||||
replacement: rule.replacement || '',
|
||||
id: rule.id || `${rule.search || 'rule'}-${index}-${Date.now()}`,
|
||||
search: rule.search || rule.pattern || '',
|
||||
replace: rule.replace || rule.replacement || '',
|
||||
}));
|
||||
|
||||
// Helper function to map API path_mapping response to PathMappingConfig
|
||||
const mapPathMappingConfig = (data: any): PathMappingConfig => {
|
||||
const defaultConfig: PathMappingConfig = {
|
||||
mode: PathMappingMode.SIMPLE,
|
||||
simple: [],
|
||||
regex: {
|
||||
localPre: [],
|
||||
localPost: [],
|
||||
remotePre: [],
|
||||
remotePost: []
|
||||
}
|
||||
};
|
||||
|
||||
if (!data || !data.path_mapping) {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
const pm = data.path_mapping;
|
||||
return {
|
||||
mode: pm.mode === 'REGEX' ? PathMappingMode.REGEX : PathMappingMode.SIMPLE,
|
||||
simple: mapReplacementRules(pm.simple || []),
|
||||
regex: {
|
||||
localPre: mapReplacementRules(pm.regex?.localPre || pm.regex?.local_pre || []),
|
||||
localPost: mapReplacementRules(pm.regex?.localPost || pm.regex?.local_post || []),
|
||||
remotePre: mapReplacementRules(pm.regex?.remotePre || pm.regex?.remote_pre || []),
|
||||
remotePost: mapReplacementRules(pm.regex?.remotePost || pm.regex?.remote_post || [])
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to convert PathMappingConfig to API format
|
||||
const pathMappingToApi = (config: PathMappingConfig) => {
|
||||
const rulesToApi = (rules: ReplacementRule[]) =>
|
||||
rules.map(({ search, replace }) => ({ search, replace }));
|
||||
|
||||
return {
|
||||
mode: config.mode,
|
||||
simple: rulesToApi(config.simple),
|
||||
regex: {
|
||||
local_pre: rulesToApi(config.regex.localPre),
|
||||
local_post: rulesToApi(config.regex.localPost),
|
||||
remote_pre: rulesToApi(config.regex.remotePre),
|
||||
remote_post: rulesToApi(config.regex.remotePost)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const apiService = {
|
||||
async getSettings(): Promise<ApiResponse<{ strategy: SyncStrategy; regex: RegexReplacement[]; connection: PlexConnectionSettings; localPath: string }>> {
|
||||
async getSettings(): Promise<ApiResponse<{ strategy: SyncStrategy; pathMapping: PathMappingConfig; connection: PlexConnectionSettings; localPath: string }>> {
|
||||
const response = await fetch(`${API_BASE}/api/settings`);
|
||||
const result = await handleResponse<any>(response);
|
||||
if (result.status === 'success') {
|
||||
const mode = result.data.sync_mode as string;
|
||||
const strategy = MODE_TO_STRATEGY[mode] || SyncStrategy.LOCAL_OVERWRITE;
|
||||
const regex = mapRegexRules(result.data.path_rules || []);
|
||||
const pathMapping = mapPathMappingConfig(result.data);
|
||||
const connection: PlexConnectionSettings = {
|
||||
protocol: (result.data.scheme as 'http' | 'https') || 'https',
|
||||
address: result.data.server_url || '',
|
||||
@@ -63,9 +111,9 @@ export const apiService = {
|
||||
token: result.data.token || '',
|
||||
libraryName: result.data.library_name || '',
|
||||
};
|
||||
return { status: 'success', data: { strategy, regex, connection, localPath: result.data.local_path || '' } };
|
||||
return { status: 'success', data: { strategy, pathMapping, connection, localPath: result.data.local_path || '' } };
|
||||
}
|
||||
return result as ApiResponse<any> as ApiResponse<{ strategy: SyncStrategy; regex: RegexReplacement[]; connection: PlexConnectionSettings; localPath: string }>;
|
||||
return result as ApiResponse<any> as ApiResponse<{ strategy: SyncStrategy; pathMapping: PathMappingConfig; connection: PlexConnectionSettings; localPath: string }>;
|
||||
},
|
||||
|
||||
async updateSyncStrategy(strategy: SyncStrategy): Promise<ApiResponse<{ sync_mode: string }>> {
|
||||
@@ -78,9 +126,9 @@ export const apiService = {
|
||||
return handleResponse(response);
|
||||
},
|
||||
|
||||
async saveRegexRules(replacements: RegexReplacement[]): Promise<ApiResponse<{ rules: RegexReplacement[] }>> {
|
||||
const payload = { rules: replacements.map(({ pattern, replacement }) => ({ pattern, replacement })) };
|
||||
const response = await fetch(`${API_BASE}/api/settings/regex-rules`, {
|
||||
async savePathMapping(config: PathMappingConfig): Promise<ApiResponse<null>> {
|
||||
const payload = pathMappingToApi(config);
|
||||
const response = await fetch(`${API_BASE}/api/settings/path-mapping`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -170,7 +218,7 @@ export const apiService = {
|
||||
return result as ApiResponse<{ token: string; serverInfo: PlexServerConnection }>;
|
||||
},
|
||||
|
||||
async syncPlaylists(strategy: SyncStrategy, _regexRules: RegexReplacement[], localPath?: string): Promise<ApiResponse<null>> {
|
||||
async syncPlaylists(strategy: SyncStrategy, _pathMapping: PathMappingConfig, localPath?: string): Promise<ApiResponse<null>> {
|
||||
const response = await fetch(`${API_BASE}/api/sync`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
Reference in New Issue
Block a user