1131b81454
Root cause: The UUID (id) field was being stripped when saving path mapping: - Backend ReplacementRule model was missing id field - Frontend pathMappingToApi() didn't include id in conversion - Backend update_path_mapping endpoint didn't save id Changes: - Add id field to ReplacementRule model in main.py - Include id when saving path mapping rules in update_path_mapping - Include id in frontend pathMappingToApi conversion Co-authored-by: Koha9 <36852125+Koha9@users.noreply.github.com>
238 lines
9.3 KiB
TypeScript
238 lines
9.3 KiB
TypeScript
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 || '';
|
|
|
|
const MODE_TO_STRATEGY: Record<string, SyncStrategy> = {
|
|
local_force: SyncStrategy.LOCAL_OVERWRITE,
|
|
remote_force: SyncStrategy.CLOUD_OVERWRITE,
|
|
merge_local_primary: SyncStrategy.MERGE_LOCAL,
|
|
merge_remote_primary: SyncStrategy.MERGE_CLOUD,
|
|
};
|
|
|
|
const STRATEGY_TO_MODE: Record<SyncStrategy, string> = {
|
|
[SyncStrategy.LOCAL_OVERWRITE]: 'local_force',
|
|
[SyncStrategy.CLOUD_OVERWRITE]: 'remote_force',
|
|
[SyncStrategy.MERGE_LOCAL]: 'merge_local_primary',
|
|
[SyncStrategy.MERGE_CLOUD]: 'merge_remote_primary',
|
|
};
|
|
|
|
const handleResponse = async <T>(response: Response): Promise<ApiResponse<T>> => {
|
|
try {
|
|
const data = await response.json();
|
|
if (!response.ok) {
|
|
return { data: data as T, status: 'error', message: (data as any)?.detail || response.statusText };
|
|
}
|
|
return { data, status: 'success' };
|
|
} catch (error: any) {
|
|
return { data: {} as T, status: 'error', message: error?.message || 'Unexpected error' };
|
|
}
|
|
};
|
|
|
|
const mapPlaylist = (item: any): Playlist => ({
|
|
id: item.id || `${item.title}-${item.trackCount}`,
|
|
title: item.title ?? item.name ?? 'Unknown',
|
|
trackCount: item.trackCount ?? item.track_count ?? 0,
|
|
lastUpdated: item.lastUpdated || item.last_updated || new Date().toISOString(),
|
|
});
|
|
|
|
const mapLibrary = (item: any): PlexLibrary => ({
|
|
id: item.id ?? item.title,
|
|
title: item.title ?? item.id,
|
|
type: item.type ?? 'artist',
|
|
});
|
|
|
|
// Helper function to map raw rules array to ReplacementRule[]
|
|
const mapReplacementRules = (rules: any[]): ReplacementRule[] =>
|
|
(rules || []).map((rule, index) => ({
|
|
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(({ id, search, replace }) => ({ id, 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; 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 pathMapping = mapPathMappingConfig(result.data);
|
|
const connection: PlexConnectionSettings = {
|
|
protocol: (result.data.scheme as 'http' | 'https') || 'https',
|
|
address: result.data.server_url || '',
|
|
port: result.data.port || '32400',
|
|
token: result.data.token || '',
|
|
libraryName: result.data.library_name || '',
|
|
};
|
|
return { status: 'success', data: { strategy, pathMapping, connection, localPath: result.data.local_path || '' } };
|
|
}
|
|
return result as ApiResponse<any> as ApiResponse<{ strategy: SyncStrategy; pathMapping: PathMappingConfig; connection: PlexConnectionSettings; localPath: string }>;
|
|
},
|
|
|
|
async updateSyncStrategy(strategy: SyncStrategy): Promise<ApiResponse<{ sync_mode: string }>> {
|
|
const payload = { mode: STRATEGY_TO_MODE[strategy] };
|
|
const response = await fetch(`${API_BASE}/api/settings/sync-mode`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
return handleResponse(response);
|
|
},
|
|
|
|
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),
|
|
});
|
|
return handleResponse(response);
|
|
},
|
|
|
|
async updateLibrary(libraryName: string): Promise<ApiResponse<{ library_name: string }>> {
|
|
const response = await fetch(`${API_BASE}/api/settings/library`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ library_name: libraryName }),
|
|
});
|
|
return handleResponse(response);
|
|
},
|
|
|
|
async getScheduleSettings(): Promise<ApiResponse<ScheduleSettings & { nextRun?: string }>> {
|
|
const response = await fetch(`${API_BASE}/api/schedule`);
|
|
return handleResponse(response);
|
|
},
|
|
|
|
async saveScheduleSettings(settings: ScheduleSettings): Promise<ApiResponse<null>> {
|
|
const response = await fetch(`${API_BASE}/api/schedule`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(settings),
|
|
});
|
|
return handleResponse(response);
|
|
},
|
|
|
|
async getPlaylists(serverType: ServerType, signal?: AbortSignal, localPath?: string): Promise<ApiResponse<Playlist[]>> {
|
|
const params = new URLSearchParams({ server: serverType.toLowerCase() });
|
|
if (serverType === ServerType.LOCAL && localPath) {
|
|
params.append('local_path', localPath);
|
|
}
|
|
const response = await fetch(`${API_BASE}/api/playlists?${params.toString()}`, { signal });
|
|
const result = await handleResponse<any>(response);
|
|
if (result.status === 'success' && (result.data as any)?.playlists) {
|
|
return { data: (result.data.playlists as any[]).map(mapPlaylist), status: 'success' };
|
|
}
|
|
return result as ApiResponse<Playlist[]>;
|
|
},
|
|
|
|
async getServerStatus(signal?: AbortSignal): Promise<ApiResponse<PlexServerConnection>> {
|
|
const response = await fetch(`${API_BASE}/api/server`, { signal });
|
|
const result = await handleResponse<any>(response);
|
|
if (result.status === 'success') {
|
|
const info = result.data.serverInfo || {};
|
|
const libraries: PlexLibrary[] = (result.data.libraries || []).map(mapLibrary);
|
|
return {
|
|
status: 'success',
|
|
data: {
|
|
isConnected: !!info.isConnected,
|
|
name: info.name,
|
|
ip: info.ip,
|
|
port: info.port ? Number(info.port) : undefined,
|
|
libraryName: info.libraryName,
|
|
libraries,
|
|
},
|
|
};
|
|
}
|
|
return result as ApiResponse<PlexServerConnection>;
|
|
},
|
|
|
|
async connectToPlex(settings: PlexConnectionSettings, signal?: AbortSignal): Promise<ApiResponse<{ token: string; serverInfo: PlexServerConnection }>> {
|
|
const response = await fetch(`${API_BASE}/api/connect`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
protocol: settings.protocol,
|
|
address: settings.address,
|
|
port: settings.port,
|
|
token: settings.token,
|
|
username: settings.username,
|
|
password: settings.password,
|
|
library_name: settings.libraryName,
|
|
timeout: settings.timeout,
|
|
}),
|
|
signal,
|
|
});
|
|
const result = await handleResponse<any>(response);
|
|
if (result.status === 'success') {
|
|
const info = result.data.serverInfo;
|
|
info.libraries = (info.libraries || []).map(mapLibrary);
|
|
return { status: 'success', data: { token: result.data.token, serverInfo: info } };
|
|
}
|
|
return result as ApiResponse<{ token: string; serverInfo: PlexServerConnection }>;
|
|
},
|
|
|
|
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' },
|
|
body: JSON.stringify({
|
|
mode: STRATEGY_TO_MODE[strategy],
|
|
local_path: localPath,
|
|
}),
|
|
});
|
|
return handleResponse(response);
|
|
},
|
|
|
|
async getSyncStatus(): Promise<ApiResponse<{ is_syncing: boolean; last_sync_time: string | null; status: string; error: string | null }>> {
|
|
const response = await fetch(`${API_BASE}/api/sync/status`);
|
|
return handleResponse(response);
|
|
},
|
|
};
|