06e49be1f9
99ea3a6 feat: Display next sync schedule information fb8d17a feat: Implement schedule settings and basic UI git-subtree-dir: sample-front-end git-subtree-split: 99ea3a68de98503b706d3ee5782baf4a66dc7134
221 lines
7.4 KiB
TypeScript
221 lines
7.4 KiB
TypeScript
|
|
|
|
import { Playlist, ServerType, ApiResponse, PlexServerConnection, PlexConnectionSettings, PlexLibrary, SyncStrategy, RegexReplacement, ScheduleSettings, ScheduleMode } from '../types';
|
|
import { MOCK_LOCAL_PLAYLISTS, MOCK_CLOUD_PLAYLISTS } from './mockData';
|
|
|
|
const SIMULATE_DELAY_MS = 800;
|
|
|
|
// Mock available libraries on a server
|
|
const MOCK_LIBRARIES: PlexLibrary[] = [
|
|
{ id: 'lib1', title: 'Music (Flac)', type: 'artist' },
|
|
{ id: 'lib2', title: 'MP3 Collection', type: 'artist' },
|
|
{ id: 'lib3', title: 'Soundtracks', type: 'artist' },
|
|
{ id: 'lib4', title: 'Audiobooks', type: 'artist' }
|
|
];
|
|
|
|
// Helper to simulate network request or call actual API
|
|
const fetchPlaylists = async (type: ServerType, signal?: AbortSignal): Promise<Playlist[]> => {
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
if (type === ServerType.LOCAL) {
|
|
resolve([...MOCK_LOCAL_PLAYLISTS]);
|
|
} else {
|
|
resolve([...MOCK_CLOUD_PLAYLISTS]);
|
|
}
|
|
}, SIMULATE_DELAY_MS);
|
|
|
|
if (signal) {
|
|
signal.addEventListener('abort', () => {
|
|
clearTimeout(timer);
|
|
reject(new DOMException('Aborted', 'AbortError'));
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const fetchServerStatus = async (signal?: AbortSignal): Promise<PlexServerConnection> => {
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
// 90% chance of success for demo
|
|
const isSuccess = Math.random() > 0.1;
|
|
if (isSuccess) {
|
|
resolve({
|
|
isConnected: true,
|
|
name: 'Home Media Server',
|
|
ip: '192.168.1.105',
|
|
port: 32400,
|
|
libraryName: 'Music (Flac)'
|
|
});
|
|
} else {
|
|
resolve({
|
|
isConnected: false
|
|
});
|
|
}
|
|
}, SIMULATE_DELAY_MS);
|
|
|
|
if (signal) {
|
|
signal.addEventListener('abort', () => {
|
|
clearTimeout(timer);
|
|
reject(new DOMException('Aborted', 'AbortError'));
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const authenticatePlex = async (settings: PlexConnectionSettings, signal?: AbortSignal): Promise<{ token: string, serverInfo: PlexServerConnection }> => {
|
|
return new Promise((resolve, reject) => {
|
|
// Determine effective timeout
|
|
const timeoutSeconds = settings.timeout || 9;
|
|
const timeoutMs = timeoutSeconds * 1000;
|
|
|
|
// Simulate latency (random between 1s and 2s, or longer to test timeout)
|
|
const latency = 1500;
|
|
|
|
const timer = setTimeout(() => {
|
|
// Check if we timed out (simulated)
|
|
if (latency > timeoutMs) {
|
|
reject(new Error(`Connection timed out after ${settings.timeout}s`));
|
|
return;
|
|
}
|
|
|
|
// Simulate validation
|
|
if (!settings.address) {
|
|
reject(new Error("Server address is required"));
|
|
return;
|
|
}
|
|
|
|
// If user provided username/password, mock a token generation
|
|
let token = settings.token;
|
|
if (!token && settings.username && settings.password) {
|
|
token = "MOCK_TOKEN_XYZ_999";
|
|
} else if (!token) {
|
|
reject(new Error("Token or Username/Password required"));
|
|
return;
|
|
}
|
|
|
|
// Success response with libraries
|
|
resolve({
|
|
token: token,
|
|
serverInfo: {
|
|
isConnected: true,
|
|
name: 'My Plex Server',
|
|
ip: settings.address,
|
|
port: parseInt(settings.port) || 32400,
|
|
libraryName: MOCK_LIBRARIES[0].title, // Default to first library
|
|
libraries: MOCK_LIBRARIES
|
|
}
|
|
});
|
|
}, latency);
|
|
|
|
// Handle User Cancellation
|
|
if (signal) {
|
|
signal.addEventListener('abort', () => {
|
|
clearTimeout(timer);
|
|
reject(new DOMException('Aborted', 'AbortError'));
|
|
});
|
|
}
|
|
|
|
// Handle Actual Timeout Logic (if latency was indeterminate in real world)
|
|
// In this mock, the latency is fixed at 1500, but logic above simulates the check.
|
|
// However, if the user set timeout < 1.5s, we should reject sooner.
|
|
if (timeoutMs < latency) {
|
|
setTimeout(() => {
|
|
clearTimeout(timer);
|
|
reject(new Error(`Connection timed out after ${settings.timeout}s`));
|
|
}, timeoutMs);
|
|
}
|
|
});
|
|
}
|
|
|
|
const triggerSync = async (strategy: SyncStrategy, regexRules: RegexReplacement[]): Promise<void> => {
|
|
return new Promise((resolve) => {
|
|
// Simulate a sync process taking 3 seconds
|
|
setTimeout(() => {
|
|
resolve();
|
|
}, 3000);
|
|
});
|
|
};
|
|
|
|
// Basic Cron validation helper
|
|
const validateCron = (expression: string): boolean => {
|
|
const parts = expression.trim().split(/\s+/);
|
|
if (parts.length !== 5) return false;
|
|
// A very naive check, real validation is more complex but this fits the mock requirement
|
|
return true;
|
|
};
|
|
|
|
export const apiService = {
|
|
getPlaylists: async (serverType: ServerType, signal?: AbortSignal): Promise<ApiResponse<Playlist[]>> => {
|
|
try {
|
|
const data = await fetchPlaylists(serverType, signal);
|
|
return { data, status: 'success' };
|
|
} catch (error: any) {
|
|
if (error.name === 'AbortError') {
|
|
// Return a specific status or handle gracefully
|
|
return { data: [], status: 'error', message: 'Request cancelled' };
|
|
}
|
|
console.error(`Error fetching ${serverType} playlists:`, error);
|
|
return { data: [], status: 'error', message: 'Failed to fetch playlists' };
|
|
}
|
|
},
|
|
|
|
getServerStatus: async (signal?: AbortSignal): Promise<ApiResponse<PlexServerConnection>> => {
|
|
try {
|
|
const data = await fetchServerStatus(signal);
|
|
return { data, status: 'success' };
|
|
} catch (error: any) {
|
|
if (error.name === 'AbortError') return { data: { isConnected: false }, status: 'error', message: 'Cancelled' };
|
|
return {
|
|
data: { isConnected: false },
|
|
status: 'error',
|
|
message: 'Failed to connect to server'
|
|
};
|
|
}
|
|
},
|
|
|
|
connectToPlex: async (settings: PlexConnectionSettings, signal?: AbortSignal): Promise<ApiResponse<{ token: string, serverInfo: PlexServerConnection }>> => {
|
|
try {
|
|
const data = await authenticatePlex(settings, signal);
|
|
return { data, status: 'success', message: 'Connected successfully' };
|
|
} catch (error: any) {
|
|
if (error.name === 'AbortError') {
|
|
return {
|
|
data: { token: '', serverInfo: { isConnected: false } },
|
|
status: 'error',
|
|
message: 'Connection cancelled'
|
|
};
|
|
}
|
|
return {
|
|
data: { token: '', serverInfo: { isConnected: false } },
|
|
status: 'error',
|
|
message: error.message || 'Connection failed'
|
|
};
|
|
}
|
|
},
|
|
|
|
syncPlaylists: async (strategy: SyncStrategy, regexRules: RegexReplacement[]): Promise<ApiResponse<null>> => {
|
|
try {
|
|
await triggerSync(strategy, regexRules);
|
|
return { data: null, status: 'success', message: 'Sync complete' };
|
|
} catch (error) {
|
|
return { data: null, status: 'error', message: 'Sync failed' };
|
|
}
|
|
},
|
|
|
|
saveScheduleSettings: async (settings: ScheduleSettings): Promise<ApiResponse<null>> => {
|
|
// Simulate API call
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
// Validation only applies if the mode is CRON and user provided input
|
|
if (settings.mode === ScheduleMode.CRON && settings.cronExpression.trim() !== '') {
|
|
if (!validateCron(settings.cronExpression)) {
|
|
resolve({ data: null, status: 'error', message: 'Invalid Cron expression format' });
|
|
return;
|
|
}
|
|
}
|
|
|
|
resolve({ data: null, status: 'success', message: 'Schedule updated successfully' });
|
|
}, 500);
|
|
});
|
|
}
|
|
}; |