mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-07 12:53:14 +02:00
improve app logging
- consolidate electron-log and expand support-oriented logging - consolidate log levels to info / trace only - add system/server diagnostics exporter
This commit is contained in:
@@ -1006,11 +1006,12 @@
|
||||
"lyricOffset_description": "Offset the lyric by the specified amount of milliseconds",
|
||||
"lyricOffset": "Lyric offset (ms)",
|
||||
"logLevel": "Log level",
|
||||
"logLevel_description": "Sets the minimum log level to display. Debug shows all logs, error only shows errors",
|
||||
"logLevel_description": "Sets the minimum log level to display",
|
||||
"logLevel_optionDebug": "Debug",
|
||||
"logLevel_optionError": "Error",
|
||||
"logLevel_optionDebugDescription": "Includes info, plus verbose debug information and redacted API responses",
|
||||
"logLevel_optionInfo": "Info",
|
||||
"logLevel_optionWarn": "Warn",
|
||||
"logLevel_optionInfoDescription": "Includes info, warnings, and errors",
|
||||
"exportDiagnostics": "Export diagnostics",
|
||||
"minimizeToTray_description": "Minimize the application to the system tray",
|
||||
"minimizeToTray": "Minimize to tray",
|
||||
"minimumScrobblePercentage_description": "The minimum percentage of the song that must be played before it is scrobbled",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createSocket } from 'dgram';
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
import { DiscoveredServerItem, ServerType } from '/@/shared/types/types';
|
||||
|
||||
type JellyfinResponse = {
|
||||
@@ -26,7 +27,7 @@ function discoverJellyfin(reply: (server: DiscoveredServerItem) => void) {
|
||||
});
|
||||
} catch (e) {
|
||||
// Got a spurious response, ignore?
|
||||
console.error(e);
|
||||
log.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -51,5 +52,5 @@ ipcMain.on('autodiscover-ping', (ev) => {
|
||||
|
||||
discoverAll((result) => port.postMessage(result))
|
||||
.then(() => port.close())
|
||||
.catch((err) => console.error(err));
|
||||
.catch((err) => log.error(err));
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import { promises as fs, watch as fsWatch } from 'fs';
|
||||
import path from 'path';
|
||||
import { validateHTMLColor } from 'validate-color';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
const defaultUserDataPath = app.getPath('userData');
|
||||
@@ -48,7 +50,7 @@ const sanitizeColors = (
|
||||
sanitized[key] = value;
|
||||
} else {
|
||||
invalidKeys.push(key);
|
||||
console.warn(`Custom theme "${themeId}" has an invalid color for "${key}": ${value}`);
|
||||
log.warn(`Custom theme "${themeId}" has an invalid color for "${key}": ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +130,7 @@ const resolveStylesheetPaths = (themeDir: string, stylesheets?: string[]): strin
|
||||
relativeToThemes.startsWith('..') || path.isAbsolute(relativeToThemes);
|
||||
|
||||
if (escapesThemesDir) {
|
||||
console.warn(
|
||||
log.warn(
|
||||
`Skipping linked file outside themes folder: ${relativePath} (from ${themeDir})`,
|
||||
);
|
||||
continue;
|
||||
@@ -146,7 +148,7 @@ const readStylesheetContents = async (stylesheetPaths: string[]): Promise<string
|
||||
try {
|
||||
return await fs.readFile(stylesheetPath, 'utf8');
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read linked stylesheet ${stylesheetPath}`, error);
|
||||
log.warn(`Failed to read linked stylesheet ${stylesheetPath}`, error);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
@@ -193,9 +195,7 @@ const resolveExtends = (
|
||||
}
|
||||
|
||||
if (visited.has(id) || depth > MAX_EXTENDS_DEPTH) {
|
||||
console.warn(
|
||||
`Custom theme "${id}" has a circular or too-deep "extends" chain, ignoring it`,
|
||||
);
|
||||
log.warn(`Custom theme "${id}" has a circular or too-deep "extends" chain, ignoring it`);
|
||||
return { fields: { mode: 'dark' }, invalidColorKeys: [] };
|
||||
}
|
||||
|
||||
@@ -322,14 +322,14 @@ const reloadThemes = async () => {
|
||||
cache = await loadThemesFromDisk();
|
||||
broadcastThemes(cache);
|
||||
} catch (error) {
|
||||
console.error('Failed to load custom themes', error);
|
||||
log.error('Failed to load custom themes', error);
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleReload = () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
reloadThemes().catch((error) => console.error('Failed to reload custom themes', error));
|
||||
reloadThemes().catch((error) => log.error('Failed to reload custom themes', error));
|
||||
}, RELOAD_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
@@ -357,7 +357,7 @@ const startWatcher = async () => {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to watch themes folder', error);
|
||||
log.error('Failed to watch themes folder', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -384,7 +384,7 @@ app.whenReady()
|
||||
await startWatcher();
|
||||
await reloadThemes();
|
||||
})
|
||||
.catch((error) => console.error('Failed to initialize custom themes', error));
|
||||
.catch((error) => log.error('Failed to initialize custom themes', error));
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (watcher) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Client, SetActivity } from '@xhayper/discord-rpc';
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const FEISHIN_DISCORD_APPLICATION_ID = '1165957668758900787';
|
||||
|
||||
let client: Client | null = null;
|
||||
@@ -21,26 +23,36 @@ const isConnected = () => {
|
||||
|
||||
const setActivity = (activity: SetActivity) => {
|
||||
if (client) {
|
||||
client.user?.setActivity({
|
||||
...activity,
|
||||
void client.user?.setActivity({ ...activity }).catch((error) => {
|
||||
log.warn('Discord RPC set activity failed', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const clearActivity = () => {
|
||||
if (client) {
|
||||
client.user?.clearActivity();
|
||||
void client.user?.clearActivity().catch((error) => {
|
||||
log.warn('Discord RPC clear activity failed', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const quit = () => {
|
||||
if (client) {
|
||||
client?.destroy();
|
||||
void client.destroy().catch((error) => {
|
||||
log.error('Discord RPC destroy failed', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ipcMain.handle('discord-rpc-initialize', async (_event, clientId?: string) => {
|
||||
await createClient(clientId);
|
||||
try {
|
||||
await createClient(clientId);
|
||||
log.info('Discord RPC initialized');
|
||||
} catch (error) {
|
||||
log.error('Discord RPC initialize failed', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('discord-rpc-is-connected', () => {
|
||||
@@ -48,19 +60,17 @@ ipcMain.handle('discord-rpc-is-connected', () => {
|
||||
});
|
||||
|
||||
ipcMain.handle('discord-rpc-set-activity', (_event, activity: SetActivity) => {
|
||||
if (client) {
|
||||
setActivity(activity);
|
||||
}
|
||||
setActivity(activity);
|
||||
});
|
||||
|
||||
ipcMain.handle('discord-rpc-clear-activity', () => {
|
||||
if (client) {
|
||||
clearActivity();
|
||||
}
|
||||
clearActivity();
|
||||
});
|
||||
|
||||
ipcMain.handle('discord-rpc-quit', () => {
|
||||
quit();
|
||||
client = null;
|
||||
log.info('Discord RPC quit');
|
||||
});
|
||||
|
||||
export const discordRpc = {
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from '.';
|
||||
import { orderSearchResults } from './shared';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const SEARCH_URL = 'https://genius.com/api/search/song';
|
||||
|
||||
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/genius.ts
|
||||
@@ -100,7 +102,7 @@ export async function getLyricsBySongId(url: string): Promise<null | string> {
|
||||
try {
|
||||
result = await axios.get<string>(url, { responseType: 'text' });
|
||||
} catch (e) {
|
||||
console.error('Genius lyrics request got an error!', (e as Error)?.message);
|
||||
log.error('Genius lyrics request got an error!', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -138,7 +140,7 @@ export async function getSearchResults(
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Genius search request got an error!', (e as Error)?.message);
|
||||
log.error('Genius search request got an error!', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -193,7 +195,7 @@ async function getSongId(
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Genius search request got an error!', (e as Error)?.message);
|
||||
log.error('Genius search request got an error!', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getSearchResults as searchSimpMusic,
|
||||
} from './simpmusic';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
|
||||
export enum LyricSource {
|
||||
@@ -110,7 +111,7 @@ const searchAllSources = async (
|
||||
allSearchResults.push(...result.value.searchResults);
|
||||
} else if (result.status === 'rejected') {
|
||||
const index = settled.indexOf(result);
|
||||
console.error(`Error searching ${sources[index]} for lyrics:`, result.reason);
|
||||
log.error(`Error searching ${sources[index]} for lyrics:`, result.reason);
|
||||
}
|
||||
}
|
||||
return allSearchResults;
|
||||
@@ -174,7 +175,7 @@ const getRemoteLyrics = async (song: Song) => {
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching lyrics from ${bestMatch.source}:`, error);
|
||||
log.error(`Error fetching lyrics from ${bestMatch.source}:`, error);
|
||||
}
|
||||
|
||||
if (lyricsFromSource) {
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from '.';
|
||||
import { orderSearchResults } from './shared';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const FETCH_URL = 'https://lrclib.net/api/get';
|
||||
const SEEARCH_URL = 'https://lrclib.net/api/search';
|
||||
|
||||
@@ -46,7 +48,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
|
||||
try {
|
||||
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
|
||||
} catch (e) {
|
||||
console.error('LrcLib lyrics request got an error!', (e as Error)?.message);
|
||||
log.error('LrcLib lyrics request got an error!', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -71,7 +73,7 @@ export async function getSearchResults(
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('LrcLib search request got an error!', (e as Error)?.message);
|
||||
log.error('LrcLib search request got an error!', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ export async function query(
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('LrcLib search request got an error!', (e as Error).message);
|
||||
log.error('LrcLib search request got an error!', (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
import { store } from '../settings';
|
||||
import { orderSearchResults } from './shared';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const SEARCH_URL = 'https://music.163.com/api/search/get';
|
||||
const LYRICS_URL = 'https://music.163.com/api/song/lyric';
|
||||
|
||||
@@ -81,7 +83,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('NetEase lyrics request got an error!', e);
|
||||
log.error('NetEase lyrics request got an error!', e);
|
||||
return null;
|
||||
}
|
||||
const enableTranslation = store.get('enableNeteaseTranslation', false) as boolean;
|
||||
@@ -114,7 +116,7 @@ export async function getSearchResults(
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('NetEase search request got an error!', e);
|
||||
log.error('NetEase search request got an error!', e);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '.';
|
||||
import { orderSearchResults } from './shared';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const API_URL = 'https://api-lyrics.simpmusic.org/v1';
|
||||
|
||||
const TIMEOUT_MS = 5000;
|
||||
@@ -38,7 +40,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic lyrics request errored:', (e as Error)?.message);
|
||||
log.error('SimpMusic lyrics request errored:', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -63,7 +65,7 @@ export async function getSearchResults(
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic search errored:', (e as Error)?.message);
|
||||
log.error('SimpMusic search errored:', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -95,7 +97,7 @@ export async function query(
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic search errored:', (e as Error).message);
|
||||
log.error('SimpMusic search errored:', (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ export async function query(
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic lyrics fetch errored:', (e as Error).message);
|
||||
log.error('SimpMusic lyrics fetch errored:', (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import console from 'console';
|
||||
import { app, ipcMain, powerMonitor } from 'electron';
|
||||
import { access, rm } from 'fs/promises';
|
||||
import uniq from 'lodash/uniq';
|
||||
@@ -7,7 +6,7 @@ import { pid } from 'node:process';
|
||||
import process from 'process';
|
||||
|
||||
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
||||
import { createLog } from '../../../utils';
|
||||
import log from '../../../logger';
|
||||
import { store } from '../settings';
|
||||
|
||||
import { isMacOS, isWindows } from '/@/main/env';
|
||||
@@ -49,24 +48,29 @@ type NodeMpvError = {
|
||||
};
|
||||
|
||||
const mpvLog = (
|
||||
data: { action: string; toast?: 'info' | 'success' | 'warning' },
|
||||
data: {
|
||||
action: string;
|
||||
level?: 'debug' | 'error' | 'info' | 'warn';
|
||||
toast?: 'info' | 'success' | 'warning';
|
||||
},
|
||||
err?: NodeMpvError,
|
||||
) => {
|
||||
const { action, toast } = data;
|
||||
|
||||
if (err) {
|
||||
const message = `[AUDIO PLAYER] ${action} - mpv errorcode ${err.errcode} - ${
|
||||
const message = `${action} - mpv errorcode ${err.errcode} - ${
|
||||
NodeMpvErrorCode[err.errcode as keyof typeof NodeMpvErrorCode]
|
||||
}`;
|
||||
|
||||
sendToastToRenderer({ message, type: 'error' });
|
||||
createLog({ message, type: 'error' });
|
||||
log.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = `[AUDIO PLAYER] ${action}`;
|
||||
createLog({ message, type: 'error' });
|
||||
const level = data.level ?? 'info';
|
||||
log[level](action);
|
||||
if (toast) {
|
||||
sendToastToRenderer({ message, type: toast });
|
||||
sendToastToRenderer({ message: action, type: toast });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,8 +160,9 @@ const createMpv = async (data: {
|
||||
|
||||
try {
|
||||
await mpv.start();
|
||||
log.info('mpv initialized', { binary: resolvedBinaryPath ?? 'bundled/default' });
|
||||
} catch (error: any) {
|
||||
console.error('mpv failed to start', error);
|
||||
log.error('mpv failed to start', error);
|
||||
} finally {
|
||||
await mpv.setMultipleProperties(properties || {});
|
||||
}
|
||||
@@ -265,11 +270,14 @@ const quit = async (instance?: MpvAPI | null) => {
|
||||
};
|
||||
|
||||
const setAudioPlayerFallback = (isError: boolean) => {
|
||||
if (isError) {
|
||||
log.warn('Falling back to web player');
|
||||
}
|
||||
getMainWindow()?.webContents.send('renderer-player-fallback', isError);
|
||||
};
|
||||
|
||||
ipcMain.on('player-set-properties', async (_event, data: Record<string, any>) => {
|
||||
mpvLog({ action: `Setting properties: ${JSON.stringify(data)}` });
|
||||
mpvLog({ action: `Setting properties: ${JSON.stringify(data)}`, level: 'debug' });
|
||||
if (data.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -291,6 +299,7 @@ ipcMain.handle(
|
||||
try {
|
||||
mpvLog({
|
||||
action: `Attempting to initialize mpv with parameters: ${JSON.stringify(data)}`,
|
||||
level: 'debug',
|
||||
});
|
||||
|
||||
// Clean up previous mpv instance
|
||||
@@ -318,6 +327,7 @@ ipcMain.handle(
|
||||
try {
|
||||
mpvLog({
|
||||
action: `Attempting to initialize mpv with parameters: ${JSON.stringify(data)}`,
|
||||
level: 'debug',
|
||||
});
|
||||
mpvInstance = await createMpv(data);
|
||||
setAudioPlayerFallback(false);
|
||||
@@ -778,7 +788,7 @@ process.on('SIGTERM', async () => {
|
||||
|
||||
// Handle uncaught exceptions - cleanup mpv before crashing
|
||||
process.on('uncaughtException', async (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
log.error('Uncaught exception:', error);
|
||||
await cleanupMpv(true).catch(() => {
|
||||
// Ignore cleanup errors during crash
|
||||
});
|
||||
@@ -786,7 +796,7 @@ process.on('uncaughtException', async (error) => {
|
||||
|
||||
// Handle unhandled rejections - cleanup mpv
|
||||
process.on('unhandledRejection', async (reason) => {
|
||||
console.error('Unhandled rejection:', reason);
|
||||
log.error('Unhandled rejection:', reason);
|
||||
await cleanupMpv(true).catch(() => {
|
||||
// Ignore cleanup errors
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import manifest from './manifest.json';
|
||||
|
||||
import { isLinux } from '/@/main/env';
|
||||
import { getMainWindow } from '/@/main/index';
|
||||
import log from '/@/main/logger';
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
|
||||
import { PlayerRepeat, PlayerStatus, SongState } from '/@/shared/types/types';
|
||||
@@ -76,6 +77,10 @@ function send({ client, data, event }: SendData): void {
|
||||
}
|
||||
|
||||
export const shutdownServer = () => {
|
||||
if (wsServer || server) {
|
||||
log.info('Remote server shutting down');
|
||||
}
|
||||
|
||||
if (wsServer) {
|
||||
wsServer.clients.forEach((client) => client.close(4000));
|
||||
wsServer.close();
|
||||
@@ -332,24 +337,44 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(config.port, resolve);
|
||||
let settled = false;
|
||||
const settle = (fn: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
fn();
|
||||
};
|
||||
|
||||
server.listen(config.port, () => {
|
||||
log.info('Remote server listening', { port: config.port });
|
||||
settle(() => resolve());
|
||||
});
|
||||
server.on('error', (error) => {
|
||||
log.error('Remote server listen failed', { error, port: config.port });
|
||||
settle(() => reject(error));
|
||||
});
|
||||
wsServer = new WebSocketServer<typeof StatefulWebSocket>({ server });
|
||||
|
||||
wsServer!.on('connection', (ws: StatefulWebSocket) => {
|
||||
let authFail: number | undefined;
|
||||
ws.alive = true;
|
||||
log.info('Remote client connected', { clients: wsServer?.clients.size });
|
||||
|
||||
if (!settings.username && !settings.password) {
|
||||
ws.auth = true;
|
||||
} else {
|
||||
authFail = setTimeout(() => {
|
||||
if (!ws.auth) {
|
||||
log.warn('Remote client auth timeout');
|
||||
ws.close();
|
||||
}
|
||||
}, 10000) as unknown as number;
|
||||
}
|
||||
|
||||
ws.on('error', console.error);
|
||||
ws.on('error', log.error);
|
||||
|
||||
ws.on('close', () => {
|
||||
log.info('Remote client disconnected', { clients: wsServer?.clients.size });
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
@@ -365,7 +390,9 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
|
||||
if (login === settings.username && password === settings.password) {
|
||||
ws.auth = true;
|
||||
log.info('Remote client authenticated');
|
||||
} else {
|
||||
log.warn('Remote client auth failed');
|
||||
ws.close();
|
||||
}
|
||||
|
||||
@@ -488,7 +515,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -516,7 +543,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Server did not come up'));
|
||||
settle(() => reject(new Error('Server did not come up')));
|
||||
}, UP_TIMEOUT_MS);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
|
||||
@@ -15,6 +15,8 @@ import Store from 'electron-store';
|
||||
import { promises as fs, watch as fsWatch } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import log from '/@/main/logger';
|
||||
|
||||
const getFrame = () => {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
@@ -52,7 +54,7 @@ const readCustomCss = async (): Promise<{ content: string; exists: boolean }> =>
|
||||
return { content: '', exists: false };
|
||||
}
|
||||
|
||||
console.error('Failed to read custom css file', error);
|
||||
log.error('Failed to read custom css file', error);
|
||||
return { content: '', exists: false };
|
||||
}
|
||||
};
|
||||
@@ -75,7 +77,7 @@ const scheduleCustomCssUpdate = () => {
|
||||
|
||||
customCssDebounce = setTimeout(() => {
|
||||
notifyCustomCssUpdate().catch((error) => {
|
||||
console.error('Failed to broadcast custom css update', error);
|
||||
log.error('Failed to broadcast custom css update', error);
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
@@ -94,13 +96,13 @@ const startCustomCssWatcher = async () => {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to watch custom css file', error);
|
||||
log.error('Failed to watch custom css file', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const store = new Store<any>({
|
||||
beforeEachMigration: (_store, context) => {
|
||||
console.log(`settings migrate from ${context.fromVersion} → ${context.toVersion}`);
|
||||
log.info(`settings migrate from ${context.fromVersion} → ${context.toVersion}`);
|
||||
},
|
||||
cwd: storePath,
|
||||
defaults: {
|
||||
@@ -164,6 +166,7 @@ ipcMain.handle('password-get', (_event, server: string): null | string => {
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
log.warn('Password encryption unavailable', { serverId: server });
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -173,6 +176,7 @@ ipcMain.on('password-remove', (_event, server: string) => {
|
||||
delete passwords[server];
|
||||
}
|
||||
store.set({ server: passwords });
|
||||
log.info('Password removed', { serverId: server });
|
||||
});
|
||||
|
||||
ipcMain.handle('password-set', (_event, password: string, server: string) => {
|
||||
@@ -182,8 +186,11 @@ ipcMain.handle('password-set', (_event, password: string, server: string) => {
|
||||
passwords[server] = encrypted.toString('hex');
|
||||
store.set({ server: passwords });
|
||||
|
||||
log.info('Password saved', { serverId: server });
|
||||
return true;
|
||||
}
|
||||
|
||||
log.warn('Password encryption unavailable', { serverId: server });
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -226,7 +233,7 @@ ipcMain.handle('custom-css-open-folder', async () => {
|
||||
|
||||
app.whenReady()
|
||||
.then(() => startCustomCssWatcher())
|
||||
.catch((error) => console.error('Failed to start custom css watcher', error));
|
||||
.catch((error) => log.error('Failed to start custom css watcher', error));
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (customCssWatcher) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
|
||||
import Player from 'mpris-service';
|
||||
|
||||
import { getMainWindow } from '/@/main/index';
|
||||
import log from '/@/main/logger';
|
||||
import { MPV_VOLUME_MAX_CEILING } from '/@/shared/constants/volume';
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
|
||||
@@ -200,7 +201,7 @@ ipcMain.on(
|
||||
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
log.error(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
+98
-41
@@ -21,7 +21,6 @@ import {
|
||||
Tray,
|
||||
} from 'electron';
|
||||
import electronLocalShortcut from 'electron-localshortcut';
|
||||
import log from 'electron-log/main';
|
||||
import { AppImageUpdater, autoUpdater, MacUpdater, NsisUpdater } from 'electron-updater';
|
||||
import { access, constants } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
@@ -32,9 +31,10 @@ import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-
|
||||
import { shutdownServer } from './features/core/remote';
|
||||
import { store } from './features/core/settings';
|
||||
import { canHandleVisualizerDisplayMedia } from './features/core/visualizer';
|
||||
import log, { autoUpdaterLogInterface } from './logger';
|
||||
import MenuBuilder, { MenuPlaybackState } from './menu';
|
||||
import './features';
|
||||
import { autoUpdaterLogInterface, createLog, hotkeyToElectronAccelerator } from './utils';
|
||||
import { hotkeyToElectronAccelerator } from './utils';
|
||||
|
||||
import { disableAutoUpdates, isLinux, isMacOS, isWindows } from '/@/main/env';
|
||||
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||
@@ -62,13 +62,21 @@ type UpdaterInstance = AppImageUpdater | MacUpdater | NsisUpdater | typeof autoU
|
||||
class AppUpdater {
|
||||
constructor() {
|
||||
const effectiveChannel = store.get('release_channel') as string;
|
||||
console.log('Effective update channel:', effectiveChannel);
|
||||
log.info('Effective update channel:', effectiveChannel);
|
||||
if (effectiveChannel === 'alpha') {
|
||||
checkAllChannelsAndGetBest().then(({ result, updater: updaterInstance }) => {
|
||||
attachUpdaterMilestoneLogs(updaterInstance);
|
||||
|
||||
if (!result?.isUpdateAvailable) {
|
||||
log.info('Updater check complete', { available: false });
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Updater check complete', {
|
||||
available: true,
|
||||
version: result.updateInfo.version,
|
||||
});
|
||||
|
||||
updaterInstance.autoInstallOnAppQuit = true;
|
||||
updaterInstance.autoRunAppAfterInstall = true;
|
||||
if (isMacOS()) {
|
||||
@@ -77,6 +85,7 @@ class AppUpdater {
|
||||
result.updateInfo.version,
|
||||
);
|
||||
} else {
|
||||
log.info('Updater download starting', { version: result.updateInfo.version });
|
||||
updaterInstance.autoDownload = true;
|
||||
updaterInstance.checkForUpdatesAndNotify();
|
||||
}
|
||||
@@ -84,26 +93,65 @@ class AppUpdater {
|
||||
return;
|
||||
}
|
||||
|
||||
configureAndGetUpdater();
|
||||
const updater = configureAndGetUpdater();
|
||||
attachUpdaterMilestoneLogs(updater);
|
||||
|
||||
if (isMacOS()) {
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater
|
||||
.checkForUpdates()
|
||||
.then((result) => {
|
||||
if (result?.isUpdateAvailable) {
|
||||
log.info('Updater check complete', {
|
||||
available: true,
|
||||
version: result.updateInfo.version,
|
||||
});
|
||||
getMainWindow()?.webContents.send(
|
||||
'update-available',
|
||||
result.updateInfo.version,
|
||||
);
|
||||
} else {
|
||||
log.info('Updater check complete', { available: false });
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error('Check for updates failed', err));
|
||||
.catch((err) => log.error('Check for updates failed', err));
|
||||
} else {
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachUpdaterMilestoneLogs(updater: UpdaterInstance): void {
|
||||
let downloadStarted = false;
|
||||
|
||||
updater.on('checking-for-update', () => {
|
||||
log.info('Updater checking for update');
|
||||
});
|
||||
|
||||
updater.on('update-available', (info) => {
|
||||
log.info('Updater update available', { version: info.version });
|
||||
});
|
||||
|
||||
updater.on('update-not-available', (info) => {
|
||||
log.info('Updater update not available', { version: info.version });
|
||||
});
|
||||
|
||||
updater.on('download-progress', () => {
|
||||
if (!downloadStarted) {
|
||||
downloadStarted = true;
|
||||
log.info('Updater download starting');
|
||||
}
|
||||
});
|
||||
|
||||
updater.on('update-downloaded', (info) => {
|
||||
log.info('Updater download complete', { version: info.version });
|
||||
});
|
||||
|
||||
updater.on('error', (error) => {
|
||||
log.error('Updater error', error);
|
||||
});
|
||||
}
|
||||
|
||||
// When release channel is alpha, check alpha and latest for updates and return
|
||||
// the updater + result for the newest version found (so alpha users can receive
|
||||
// latest updates when they are newer than the current alpha).
|
||||
@@ -121,7 +169,7 @@ async function checkAllChannelsAndGetBest(): Promise<{
|
||||
const alphaUpdater = createAlphaUpdaterInstance({ probeOnly: true });
|
||||
|
||||
try {
|
||||
console.log('Checking for updates on alpha channel');
|
||||
log.info('Checking for updates on alpha channel');
|
||||
const alphaResult = await alphaUpdater.checkForUpdates();
|
||||
if (
|
||||
alphaResult?.updateInfo?.version &&
|
||||
@@ -137,7 +185,7 @@ async function checkAllChannelsAndGetBest(): Promise<{
|
||||
|
||||
try {
|
||||
const latestUpdater = createGithubUpdaterInstance('latest', { probeOnly: true });
|
||||
console.log('Checking for updates on latest channel (GitHub)');
|
||||
log.info('Checking for updates on latest channel (GitHub)');
|
||||
const latestResult = await latestUpdater.checkForUpdates();
|
||||
if (
|
||||
latestResult?.updateInfo?.version &&
|
||||
@@ -173,13 +221,13 @@ function configureAndGetUpdater(): UpdaterInstance {
|
||||
let releaseChannel = store.get('release_channel');
|
||||
const isNotConfigured = !releaseChannel;
|
||||
|
||||
console.log('Release channel:', releaseChannel);
|
||||
console.log('Is beta version:', isBetaVersion);
|
||||
console.log('Is alpha version:', isAlphaVersion);
|
||||
console.log('Is not configured:', isNotConfigured);
|
||||
log.info('Release channel:', releaseChannel);
|
||||
log.info('Is beta version:', isBetaVersion);
|
||||
log.info('Is alpha version:', isAlphaVersion);
|
||||
log.info('Is not configured:', isNotConfigured);
|
||||
|
||||
if (isNotConfigured) {
|
||||
console.log('Release channel not configured, setting default channel');
|
||||
log.info('Release channel not configured, setting default channel');
|
||||
const defaultChannel = isAlphaVersion ? 'alpha' : isBetaVersion ? 'beta' : 'latest';
|
||||
store.set('release_channel', defaultChannel);
|
||||
releaseChannel = defaultChannel;
|
||||
@@ -188,11 +236,9 @@ function configureAndGetUpdater(): UpdaterInstance {
|
||||
const effectiveChannel = store.get('release_channel') as string;
|
||||
|
||||
if (effectiveChannel === 'alpha') {
|
||||
log.transports.file.level = 'info';
|
||||
return createAlphaUpdaterInstance();
|
||||
}
|
||||
|
||||
log.transports.file.level = 'info';
|
||||
autoUpdater.logger = autoUpdaterLogInterface;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
@@ -216,7 +262,6 @@ function configureAndGetUpdater(): UpdaterInstance {
|
||||
* Used when checking multiple channels or when the winning channel is beta/latest.
|
||||
*/
|
||||
function configureAutoUpdaterForChannel(channel: 'beta' | 'latest'): void {
|
||||
log.transports.file.level = 'info';
|
||||
autoUpdater.logger = autoUpdaterLogInterface;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
@@ -296,7 +341,7 @@ protocol.registerSchemesAsPrivileged([
|
||||
]);
|
||||
|
||||
process.on('uncaughtException', (error: any) => {
|
||||
console.error('Error in main process', error);
|
||||
log.error('Error in main process', error);
|
||||
});
|
||||
|
||||
if (store.get('ignore_ssl')) {
|
||||
@@ -320,6 +365,13 @@ let currentPrivateMode = false;
|
||||
let currentRepeatMode: PlayerRepeat = PlayerRepeat.NONE;
|
||||
let currentSidebarCollapsed = false;
|
||||
let currentShuffleEnabled = false;
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (isMacOS()) {
|
||||
forceQuit = true;
|
||||
}
|
||||
log.info('App quitting', { reason: exitFromTray ? 'tray' : 'before-quit' });
|
||||
});
|
||||
let playbackMenuAccelerators: MenuPlaybackState['accelerators'] = {};
|
||||
let inputFocused = false;
|
||||
|
||||
@@ -357,10 +409,7 @@ const installExtensions = async () => {
|
||||
{ forceDownload },
|
||||
)
|
||||
.then((installedExtensions) => {
|
||||
createLog({
|
||||
message: `Installed extension: ${installedExtensions}`,
|
||||
type: 'info',
|
||||
});
|
||||
log.info(`Installed extension: ${installedExtensions}`);
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore
|
||||
@@ -542,7 +591,7 @@ const validateUrl = (url: string): boolean => {
|
||||
|
||||
async function createWindow(first = true): Promise<void> {
|
||||
if (isDevelopment) {
|
||||
await installExtensions().catch(console.log);
|
||||
await installExtensions().catch((error) => log.error(error));
|
||||
}
|
||||
|
||||
const nativeFrame = store.get('window_window_bar_style', 'linux') === 'linux';
|
||||
@@ -635,6 +684,7 @@ async function createWindow(first = true): Promise<void> {
|
||||
});
|
||||
|
||||
ipcMain.on('window-quit', () => {
|
||||
log.info('App quitting', { reason: 'window-quit' });
|
||||
shutdownServer();
|
||||
mainWindow?.close();
|
||||
app.exit();
|
||||
@@ -700,9 +750,23 @@ async function createWindow(first = true): Promise<void> {
|
||||
mainWindow.show();
|
||||
createWinThumbarButtons();
|
||||
}
|
||||
|
||||
log.info('Main window created', { startMinimized: startWindowMinimized && first });
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('render-process-gone', (_event, details) => {
|
||||
log.error('Renderer process gone', {
|
||||
exitCode: details.exitCode,
|
||||
reason: details.reason,
|
||||
});
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('unresponsive', () => {
|
||||
log.error('Renderer process unresponsive');
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
log.info('Main window closed');
|
||||
ipcMain.removeHandler('window-clear-cache');
|
||||
ipcMain.removeHandler('app-check-for-updates');
|
||||
mainWindow = null;
|
||||
@@ -715,10 +779,12 @@ async function createWindow(first = true): Promise<void> {
|
||||
|
||||
if (!exitFromTray && store.get('window_exit_to_tray')) {
|
||||
event.preventDefault();
|
||||
log.info('Main window hidden to tray');
|
||||
mainWindow?.hide();
|
||||
}
|
||||
|
||||
if (forceQuit) {
|
||||
log.info('App quitting', { reason: 'forceQuit' });
|
||||
app.exit();
|
||||
}
|
||||
});
|
||||
@@ -726,6 +792,7 @@ async function createWindow(first = true): Promise<void> {
|
||||
(mainWindow as any).on('minimize', (event: any) => {
|
||||
if (store.get('window_minimize_to_tray') === true) {
|
||||
event.preventDefault();
|
||||
log.info('Main window minimized to tray');
|
||||
mainWindow?.hide();
|
||||
}
|
||||
});
|
||||
@@ -734,12 +801,6 @@ async function createWindow(first = true): Promise<void> {
|
||||
app.setAppUserModelId('org.jeffvli.feishin');
|
||||
}
|
||||
|
||||
if (isMacOS()) {
|
||||
app.on('before-quit', () => {
|
||||
forceQuit = true;
|
||||
});
|
||||
}
|
||||
|
||||
menuBuilder = new MenuBuilder(mainWindow);
|
||||
rebuildMainMenu();
|
||||
|
||||
@@ -949,19 +1010,6 @@ ipcMain.on(
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
'logger',
|
||||
(
|
||||
_event,
|
||||
data: {
|
||||
message: string;
|
||||
type: 'debug' | 'error' | 'info' | 'success' | 'verbose' | 'warning';
|
||||
},
|
||||
) => {
|
||||
createLog(data);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle('power-save-blocker-start', (_event, { full }: { full: boolean }) => {
|
||||
if (powerSaveBlockerId !== null) {
|
||||
return powerSaveBlockerId;
|
||||
@@ -1044,6 +1092,15 @@ if (!singleInstance) {
|
||||
|
||||
app.whenReady()
|
||||
.then(() => {
|
||||
log.info('App ready', {
|
||||
arch: process.arch,
|
||||
electron: process.versions.electron,
|
||||
ignoreCors: !!store.get('ignore_cors'),
|
||||
ignoreSsl: !!store.get('ignore_ssl'),
|
||||
platform: process.platform,
|
||||
version: packageJson.version,
|
||||
});
|
||||
|
||||
protocol.handle('feishin', async () => {
|
||||
const filePath = store.get('local_font_path');
|
||||
if (typeof filePath !== 'string') {
|
||||
@@ -1114,7 +1171,7 @@ if (!singleInstance) {
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(console.log);
|
||||
.catch((error) => log.error(error));
|
||||
}
|
||||
|
||||
// Register 'open-item' handler globally, ensuring it is only registered once
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import type { LogLevel } from '/@/shared/logger/types';
|
||||
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron';
|
||||
import log from 'electron-log/main';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import util from 'node:util';
|
||||
|
||||
import { createZipBuffer } from '/@/main/utils/zip';
|
||||
import { sanitizeForDiagnostics } from '/@/shared/utils/sanitize-for-diagnostics';
|
||||
|
||||
export type { LogLevel };
|
||||
export type { LogSeverity } from '/@/shared/logger/types';
|
||||
|
||||
const PROCESS_WIDTH = 10; // width of "[renderer]"
|
||||
const LEVEL_WIDTH = 5; // width of "DEBUG" / "ERROR"
|
||||
const RESET = '\x1B[0m';
|
||||
|
||||
const levelColors: Record<string, string> = {
|
||||
debug: '\x1B[38;2;100;149;237m', // #6495ED
|
||||
error: '\x1B[38;2;255;100;100m', // #ff6464
|
||||
info: '\x1B[38;2;76;175;80m', // #4caf50
|
||||
warn: '\x1B[38;2;225;125;50m', // #e17d32
|
||||
};
|
||||
|
||||
const formatLogLine = ({
|
||||
colorize = false,
|
||||
data,
|
||||
level,
|
||||
message,
|
||||
}: {
|
||||
colorize?: boolean;
|
||||
data: unknown[];
|
||||
level: string;
|
||||
message: { date: Date; variables?: { processType?: string } };
|
||||
}): string[] => {
|
||||
const processType = message.variables?.processType === 'renderer' ? 'renderer' : 'main';
|
||||
const paddedLevel = String(level).toUpperCase().padEnd(LEVEL_WIDTH, ' ');
|
||||
const levelLabel =
|
||||
colorize && levelColors[level]
|
||||
? `${levelColors[level]}${paddedLevel}${RESET}`
|
||||
: paddedLevel;
|
||||
const processLabel = `[${processType}]`.padEnd(PROCESS_WIDTH, ' ');
|
||||
const text = data
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
}
|
||||
|
||||
return util.inspect(item, {
|
||||
breakLength: 80,
|
||||
colors: colorize,
|
||||
compact: false,
|
||||
depth: null,
|
||||
});
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return [`${message.date.toISOString()} ${levelLabel} ${processLabel} ${text}`];
|
||||
};
|
||||
|
||||
const isLogLevel = (value: unknown): value is LogLevel => {
|
||||
return value === 'debug' || value === 'info';
|
||||
};
|
||||
|
||||
export const setLogLevel = (level: LogLevel) => {
|
||||
log.transports.file.level = level;
|
||||
log.transports.console.level = level;
|
||||
};
|
||||
|
||||
log.initialize();
|
||||
setLogLevel(
|
||||
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true' ? 'debug' : 'info',
|
||||
);
|
||||
log.transports.file.format = (params) => formatLogLine({ ...params, colorize: false });
|
||||
log.transports.file.maxSize = 1024 * 1024 * 10; // 10MB
|
||||
log.transports.console.format = (params) => formatLogLine({ ...params, colorize: true });
|
||||
|
||||
ipcMain.on('logger-set-level', (_event, level: unknown) => {
|
||||
if (isLogLevel(level)) {
|
||||
setLogLevel(level);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('logger-open-folder', async () => {
|
||||
const logFilePath = log.transports.file.getFile().path;
|
||||
const logsPath = path.dirname(logFilePath);
|
||||
await shell.openPath(logsPath);
|
||||
return true;
|
||||
});
|
||||
|
||||
type ExportDiagnosticsPayload = {
|
||||
logLevel?: null | string;
|
||||
rendererSettings?: null | Record<string, unknown>;
|
||||
server?: null | Record<string, unknown>;
|
||||
};
|
||||
|
||||
const exportDiagnosticsArchive = async (payload: ExportDiagnosticsPayload = {}) => {
|
||||
const parentWindow = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
|
||||
const stamp = new Date().toISOString().slice(0, 10);
|
||||
const saveOptions = {
|
||||
defaultPath: `feishin-diagnostics-${stamp}.zip`,
|
||||
filters: [{ extensions: ['zip'], name: 'Zip' }],
|
||||
};
|
||||
const result = parentWindow
|
||||
? await dialog.showSaveDialog(parentWindow, saveOptions)
|
||||
: await dialog.showSaveDialog(saveOptions);
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
const logFile = log.transports.file.getFile();
|
||||
const logsDir = path.dirname(logFile.path);
|
||||
const entries: { data: Buffer; name: string }[] = [];
|
||||
|
||||
const { store } = await import('/@/main/features/core/settings');
|
||||
|
||||
const diagnostics = {
|
||||
app: {
|
||||
name: app.getName(),
|
||||
version: app.getVersion(),
|
||||
},
|
||||
arch: process.arch,
|
||||
chrome: process.versions.chrome,
|
||||
electron: process.versions.electron,
|
||||
exportedAt: new Date().toISOString(),
|
||||
locale: app.getLocale(),
|
||||
logLevel: payload.logLevel ?? null,
|
||||
node: process.versions.node,
|
||||
os: {
|
||||
release: os.release(),
|
||||
type: os.type(),
|
||||
version: typeof os.version === 'function' ? os.version() : undefined,
|
||||
},
|
||||
platform: process.platform,
|
||||
server: payload.server ?? null,
|
||||
settings: {
|
||||
ignoreCors: store.get('ignore_cors'),
|
||||
ignoreSsl: store.get('ignore_ssl'),
|
||||
releaseChannel: store.get('release_channel'),
|
||||
},
|
||||
};
|
||||
|
||||
entries.push({
|
||||
data: Buffer.from(`${JSON.stringify(diagnostics, null, 2)}\n`, 'utf8'),
|
||||
name: 'diagnostics.json',
|
||||
});
|
||||
|
||||
try {
|
||||
const configRaw = await fs.readFile(store.path, 'utf8');
|
||||
const configJson = JSON.parse(configRaw) as unknown;
|
||||
entries.push({
|
||||
data: Buffer.from(
|
||||
`${JSON.stringify(sanitizeForDiagnostics(configJson), null, 2)}\n`,
|
||||
'utf8',
|
||||
),
|
||||
name: 'main-config.json',
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn('Failed to read main config for diagnostics export', error);
|
||||
}
|
||||
|
||||
if (payload.rendererSettings) {
|
||||
entries.push({
|
||||
data: Buffer.from(
|
||||
`${JSON.stringify(sanitizeForDiagnostics(payload.rendererSettings), null, 2)}\n`,
|
||||
'utf8',
|
||||
),
|
||||
name: 'renderer-settings.json',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const logFiles = await fs.readdir(logsDir);
|
||||
for (const fileName of logFiles) {
|
||||
if (!fileName.endsWith('.log')) {
|
||||
continue;
|
||||
}
|
||||
const filePath = path.join(logsDir, fileName);
|
||||
const data = await fs.readFile(filePath);
|
||||
entries.push({ data, name: `logs/${fileName}` });
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn('Failed to read log files for export', error);
|
||||
}
|
||||
|
||||
if (!entries.some((entry) => entry.name.startsWith('logs/'))) {
|
||||
try {
|
||||
entries.push({
|
||||
data: await fs.readFile(logFile.path),
|
||||
name: `logs/${path.basename(logFile.path)}`,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('Failed to read active log file for export', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const zipBuffer = createZipBuffer(entries);
|
||||
await fs.writeFile(result.filePath, zipBuffer);
|
||||
|
||||
return { canceled: false, path: result.filePath };
|
||||
};
|
||||
|
||||
ipcMain.handle('logger-export-diagnostics', async (_event, payload?: ExportDiagnosticsPayload) => {
|
||||
return exportDiagnosticsArchive(payload);
|
||||
});
|
||||
|
||||
export default log;
|
||||
|
||||
export const autoUpdaterLogInterface = {
|
||||
debug: (message: string) => {
|
||||
log.debug(message);
|
||||
},
|
||||
|
||||
error: (message: string) => {
|
||||
log.error(message);
|
||||
},
|
||||
|
||||
info: (message: string) => {
|
||||
log.info(message);
|
||||
},
|
||||
|
||||
warn: (message: string) => {
|
||||
log.warn(message);
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import log from 'electron-log/main';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import { URL } from 'url';
|
||||
@@ -42,46 +41,3 @@ export const hotkeyToElectronAccelerator = (hotkey: string) => {
|
||||
|
||||
return accelerator;
|
||||
};
|
||||
|
||||
const logMethod = {
|
||||
debug: log.debug,
|
||||
error: log.error,
|
||||
info: log.info,
|
||||
success: log.info,
|
||||
verbose: log.verbose,
|
||||
warning: log.warn,
|
||||
};
|
||||
|
||||
const logColor = {
|
||||
debug: 'blue',
|
||||
error: 'red',
|
||||
info: 'blue',
|
||||
success: 'green',
|
||||
verbose: 'blue',
|
||||
warning: 'yellow',
|
||||
};
|
||||
|
||||
export const createLog = (data: {
|
||||
message: string;
|
||||
type: 'debug' | 'error' | 'info' | 'success' | 'verbose' | 'warning';
|
||||
}) => {
|
||||
logMethod[data.type](`%c${data.message}`, `color: ${logColor[data.type]}`);
|
||||
};
|
||||
|
||||
export const autoUpdaterLogInterface = {
|
||||
debug: (message: string) => {
|
||||
createLog({ message: `[SYSTEM] ${message}`, type: 'debug' });
|
||||
},
|
||||
|
||||
error: (message: string) => {
|
||||
createLog({ message: `[SYSTEM] ${message}`, type: 'error' });
|
||||
},
|
||||
|
||||
info: (message: string) => {
|
||||
createLog({ message: `[SYSTEM] ${message}`, type: 'info' });
|
||||
},
|
||||
|
||||
warn: (message: string) => {
|
||||
createLog({ message: `[SYSTEM] ${message}`, type: 'warning' });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { deflateRawSync } from 'node:zlib';
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const table = new Uint32Array(256);
|
||||
for (let i = 0; i < 256; i += 1) {
|
||||
let crc = i;
|
||||
for (let j = 0; j < 8; j += 1) {
|
||||
crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
||||
}
|
||||
table[i] = crc >>> 0;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
const crc32 = (data: Buffer): number => {
|
||||
let crc = 0xffffffff;
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
crc = CRC_TABLE[(crc ^ data[i]) & 0xff]! ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
};
|
||||
|
||||
export type ZipEntry = {
|
||||
data: Buffer;
|
||||
name: string;
|
||||
};
|
||||
|
||||
/** Build a zip archive (deflate) without external dependencies. */
|
||||
export const createZipBuffer = (entries: ZipEntry[]): Buffer => {
|
||||
const localParts: Buffer[] = [];
|
||||
const centralParts: Buffer[] = [];
|
||||
let offset = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
const nameBuf = Buffer.from(entry.name, 'utf8');
|
||||
const compressed = deflateRawSync(entry.data);
|
||||
const checksum = crc32(entry.data);
|
||||
|
||||
const localHeader = Buffer.alloc(30);
|
||||
localHeader.writeUInt32LE(0x04034b50, 0);
|
||||
localHeader.writeUInt16LE(20, 4); // version needed
|
||||
localHeader.writeUInt16LE(0, 6); // flags
|
||||
localHeader.writeUInt16LE(8, 8); // deflate
|
||||
localHeader.writeUInt16LE(0, 10); // time
|
||||
localHeader.writeUInt16LE(0, 12); // date
|
||||
localHeader.writeUInt32LE(checksum, 14);
|
||||
localHeader.writeUInt32LE(compressed.length, 18);
|
||||
localHeader.writeUInt32LE(entry.data.length, 22);
|
||||
localHeader.writeUInt16LE(nameBuf.length, 26);
|
||||
localHeader.writeUInt16LE(0, 28); // extra length
|
||||
|
||||
localParts.push(localHeader, nameBuf, compressed);
|
||||
|
||||
const centralHeader = Buffer.alloc(46);
|
||||
centralHeader.writeUInt32LE(0x02014b50, 0);
|
||||
centralHeader.writeUInt16LE(20, 4); // version made by
|
||||
centralHeader.writeUInt16LE(20, 6); // version needed
|
||||
centralHeader.writeUInt16LE(0, 8);
|
||||
centralHeader.writeUInt16LE(8, 10);
|
||||
centralHeader.writeUInt16LE(0, 12);
|
||||
centralHeader.writeUInt16LE(0, 14);
|
||||
centralHeader.writeUInt32LE(checksum, 16);
|
||||
centralHeader.writeUInt32LE(compressed.length, 20);
|
||||
centralHeader.writeUInt32LE(entry.data.length, 24);
|
||||
centralHeader.writeUInt16LE(nameBuf.length, 28);
|
||||
centralHeader.writeUInt16LE(0, 30);
|
||||
centralHeader.writeUInt16LE(0, 32);
|
||||
centralHeader.writeUInt16LE(0, 34);
|
||||
centralHeader.writeUInt16LE(0, 36);
|
||||
centralHeader.writeUInt32LE(0, 38);
|
||||
centralHeader.writeUInt32LE(offset, 42);
|
||||
|
||||
centralParts.push(centralHeader, nameBuf);
|
||||
offset += localHeader.length + nameBuf.length + compressed.length;
|
||||
}
|
||||
|
||||
const centralSize = centralParts.reduce((sum, part) => sum + part.length, 0);
|
||||
const end = Buffer.alloc(22);
|
||||
end.writeUInt32LE(0x06054b50, 0);
|
||||
end.writeUInt16LE(0, 4);
|
||||
end.writeUInt16LE(0, 6);
|
||||
end.writeUInt16LE(entries.length, 8);
|
||||
end.writeUInt16LE(entries.length, 10);
|
||||
end.writeUInt32LE(centralSize, 12);
|
||||
end.writeUInt32LE(offset, 16);
|
||||
end.writeUInt16LE(0, 20);
|
||||
|
||||
return Buffer.concat([...localParts, ...centralParts, end]);
|
||||
};
|
||||
@@ -67,6 +67,18 @@ const openCustomCssFolder = async () => {
|
||||
return ipcRenderer.invoke('custom-css-open-folder');
|
||||
};
|
||||
|
||||
const openLogsFolder = async () => {
|
||||
return ipcRenderer.invoke('logger-open-folder');
|
||||
};
|
||||
|
||||
const exportDiagnostics = async (payload?: {
|
||||
logLevel?: null | string;
|
||||
rendererSettings?: null | Record<string, unknown>;
|
||||
server?: null | Record<string, unknown>;
|
||||
}): Promise<{ canceled: boolean; path?: string }> => {
|
||||
return ipcRenderer.invoke('logger-export-diagnostics', payload);
|
||||
};
|
||||
|
||||
const customCssUpdatedListener = (
|
||||
cb: (data: { content?: string; exists?: boolean; path?: string }) => void,
|
||||
) => {
|
||||
@@ -164,6 +176,7 @@ export const utils = {
|
||||
customCssUpdatedListener,
|
||||
disableAutoUpdates,
|
||||
download,
|
||||
exportDiagnostics,
|
||||
forceGarbageCollection,
|
||||
getCustomCss,
|
||||
isLinux,
|
||||
@@ -175,6 +188,7 @@ export const utils = {
|
||||
openApplicationDirectory,
|
||||
openCustomCssFolder,
|
||||
openItem,
|
||||
openLogsFolder,
|
||||
playerErrorListener,
|
||||
readLocalImage,
|
||||
readSongMetadataBatch,
|
||||
|
||||
+57
-153
@@ -3,8 +3,7 @@ import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { createWithEqualityFn } from 'zustand/traditional';
|
||||
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/shared/types/remote-types';
|
||||
|
||||
@@ -42,9 +41,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
immer((set, get) => ({
|
||||
actions: {
|
||||
reconnect: async () => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].reconnectInitiated, {
|
||||
category: LogCategory.REMOTE,
|
||||
});
|
||||
logger.info('Reconnect initiated');
|
||||
const existing = get().socket;
|
||||
|
||||
if (existing) {
|
||||
@@ -52,9 +49,8 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
existing.readyState === WebSocket.OPEN ||
|
||||
existing.readyState === WebSocket.CONNECTING
|
||||
) {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].closingExistingSocket, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { readyState: existing.readyState },
|
||||
logger.debug('Closing existing socket', {
|
||||
readyState: existing.readyState,
|
||||
});
|
||||
existing.natural = true;
|
||||
existing.close(4001);
|
||||
@@ -64,28 +60,17 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
let authHeader: string | undefined;
|
||||
|
||||
try {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].fetchingCredentials, {
|
||||
category: LogCategory.REMOTE,
|
||||
});
|
||||
logger.debug('Fetching credentials');
|
||||
const credentials = await fetch('/credentials');
|
||||
authHeader = await credentials.text();
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].credentialsFetched, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { hasAuthHeader: !!authHeader },
|
||||
});
|
||||
logger.debug('Credentials fetched', { hasAuthHeader: !!authHeader });
|
||||
} catch (error) {
|
||||
logFn.error(logMsg[LogCategory.REMOTE].failedToGetCredentials, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { error },
|
||||
});
|
||||
logger.error('Failed to get credentials', { error });
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const wsUrl = location.href.replace('http', 'ws');
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].creatingWebSocket, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { url: wsUrl },
|
||||
});
|
||||
logger.info('Creating new WebSocket', { url: wsUrl });
|
||||
const socket = new WebSocket(wsUrl) as StatefulWebSocket;
|
||||
|
||||
socket.natural = false;
|
||||
@@ -93,34 +78,19 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
socket.addEventListener('message', (message) => {
|
||||
const { data, event } = JSON.parse(message.data) as ServerEvent;
|
||||
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].webSocketMessageReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { data, event },
|
||||
});
|
||||
logger.debug('WebSocket message received', { data, event });
|
||||
|
||||
switch (event) {
|
||||
case 'error': {
|
||||
logFn.error(
|
||||
logMsg[LogCategory.REMOTE].webSocketErrorEvent,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { data },
|
||||
},
|
||||
);
|
||||
logger.error('WebSocket error event', { data });
|
||||
toast.error({ message: data, title: 'Socket error' });
|
||||
break;
|
||||
}
|
||||
case 'favorite': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].favoriteEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
favorite: data.favorite,
|
||||
id: data.id,
|
||||
},
|
||||
},
|
||||
);
|
||||
logger.debug('Favorite event received', {
|
||||
favorite: data.favorite,
|
||||
id: data.id,
|
||||
});
|
||||
set((state) => {
|
||||
if (state.info.song?.id === data.id) {
|
||||
state.info.song.userFavorite = data.favorite;
|
||||
@@ -129,38 +99,23 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
break;
|
||||
}
|
||||
case 'playback': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].playbackEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { status: data },
|
||||
},
|
||||
);
|
||||
logger.debug('Playback event received', { status: data });
|
||||
set((state) => {
|
||||
state.info.status = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'position': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].positionEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { position: data },
|
||||
},
|
||||
);
|
||||
logger.debug('Position event received', { position: data });
|
||||
set((state) => {
|
||||
state.info.position = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'proxy': {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].proxyEventReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
dataLength: data?.length,
|
||||
hasData: !!data,
|
||||
},
|
||||
logger.debug('Proxy event received (image update)', {
|
||||
dataLength: data?.length,
|
||||
hasData: !!data,
|
||||
});
|
||||
set((state) => {
|
||||
if (state.info.song) {
|
||||
@@ -170,16 +125,10 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
break;
|
||||
}
|
||||
case 'rating': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].ratingEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
id: data.id,
|
||||
rating: data.rating,
|
||||
},
|
||||
},
|
||||
);
|
||||
logger.debug('Rating event received', {
|
||||
id: data.id,
|
||||
rating: data.rating,
|
||||
});
|
||||
set((state) => {
|
||||
if (state.info.song?.id === data.id) {
|
||||
state.info.song.userRating = data.rating;
|
||||
@@ -188,39 +137,24 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
break;
|
||||
}
|
||||
case 'repeat': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].repeatEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { repeat: data },
|
||||
},
|
||||
);
|
||||
logger.debug('Repeat event received', { repeat: data });
|
||||
set((state) => {
|
||||
state.info.repeat = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'shuffle': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].shuffleEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { shuffle: data },
|
||||
},
|
||||
);
|
||||
logger.debug('Shuffle event received', { shuffle: data });
|
||||
set((state) => {
|
||||
state.info.shuffle = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'song': {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].songEventReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
artistName: data?.artistName,
|
||||
id: data?.id,
|
||||
name: data?.name,
|
||||
},
|
||||
logger.debug('Song event received', {
|
||||
artistName: data?.artistName,
|
||||
id: data?.id,
|
||||
name: data?.name,
|
||||
});
|
||||
set((state) => {
|
||||
state.info.song = data;
|
||||
@@ -228,14 +162,11 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
break;
|
||||
}
|
||||
case 'state': {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].stateEventReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
hasSong: !!data.song,
|
||||
position: data.position,
|
||||
status: data.status,
|
||||
volume: data.volume,
|
||||
},
|
||||
logger.debug('State event received (full state update)', {
|
||||
hasSong: !!data.song,
|
||||
position: data.position,
|
||||
status: data.status,
|
||||
volume: data.volume,
|
||||
});
|
||||
set((state) => {
|
||||
state.info = data;
|
||||
@@ -243,13 +174,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
break;
|
||||
}
|
||||
case 'volume': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].volumeEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { volume: data },
|
||||
},
|
||||
);
|
||||
logger.debug('Volume event received', { volume: data });
|
||||
set((state) => {
|
||||
state.info.volume = data;
|
||||
});
|
||||
@@ -258,17 +183,12 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
});
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].webSocketOpened, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
hasAuthHeader: !!authHeader,
|
||||
readyState: socket.readyState,
|
||||
},
|
||||
logger.info('WebSocket opened', {
|
||||
hasAuthHeader: !!authHeader,
|
||||
readyState: socket.readyState,
|
||||
});
|
||||
if (authHeader) {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].sendingAuthentication, {
|
||||
category: LogCategory.REMOTE,
|
||||
});
|
||||
logger.debug('Sending authentication');
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
event: 'authenticate',
|
||||
@@ -280,40 +200,28 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
});
|
||||
|
||||
socket.addEventListener('close', (reason) => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].webSocketClosed, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
code: reason.code,
|
||||
natural: socket.natural,
|
||||
reason: reason.reason,
|
||||
wasClean: reason.wasClean,
|
||||
},
|
||||
logger.info('WebSocket closed', {
|
||||
code: reason.code,
|
||||
natural: socket.natural,
|
||||
reason: reason.reason,
|
||||
wasClean: reason.wasClean,
|
||||
});
|
||||
if (reason.code === 4002 || reason.code === 4003) {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].reloadingPage, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { code: reason.code },
|
||||
logger.debug('Reloading page due to close code', {
|
||||
code: reason.code,
|
||||
});
|
||||
location.reload();
|
||||
} else if (reason.code === 4000) {
|
||||
logFn.warn(logMsg[LogCategory.REMOTE].serverIsDown, {
|
||||
category: LogCategory.REMOTE,
|
||||
});
|
||||
logger.warn('Server is down');
|
||||
toast.warn({
|
||||
message: 'Feishin remote server is down',
|
||||
title: 'Connection closed',
|
||||
});
|
||||
} else if (reason.code !== 4001 && !socket.natural) {
|
||||
logFn.error(
|
||||
logMsg[LogCategory.REMOTE].socketClosedUnexpectedly,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
code: reason.code,
|
||||
reason: reason.reason,
|
||||
},
|
||||
},
|
||||
);
|
||||
logger.error('Socket closed unexpectedly', {
|
||||
code: reason.code,
|
||||
reason: reason.reason,
|
||||
});
|
||||
toast.error({
|
||||
message: 'Socket closed for unexpected reason',
|
||||
title: 'Connection closed',
|
||||
@@ -331,19 +239,15 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
send: (data: ClientEvent) => {
|
||||
const socket = get().socket;
|
||||
if (socket) {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].sendingEventToServer, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
data: data,
|
||||
event: data.event,
|
||||
readyState: socket.readyState,
|
||||
},
|
||||
logger.debug('Sending event to server', {
|
||||
data: data,
|
||||
event: data.event,
|
||||
readyState: socket.readyState,
|
||||
});
|
||||
socket.send(JSON.stringify(data));
|
||||
} else {
|
||||
logFn.warn(logMsg[LogCategory.REMOTE].cannotSendEvent, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { event: data.event },
|
||||
logger.warn('Cannot send event - socket not available', {
|
||||
event: data.event,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-control
|
||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder';
|
||||
import { getServerById, useAuthStore, useSettingsStore } from '/@/renderer/store';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
AuthenticationResponse,
|
||||
@@ -26,6 +27,95 @@ const endpoints: ApiController = {
|
||||
subsonic: SubsonicController,
|
||||
};
|
||||
|
||||
const SENSITIVE_KEY_PATTERN = /password|token|credential|authorization|secret|cookie/i;
|
||||
const MAX_ARRAY_ITEMS = 20;
|
||||
const MAX_STRING_LENGTH = 200;
|
||||
|
||||
const sanitizeValue = (value: unknown, depth = 0): unknown => {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value.length > MAX_STRING_LENGTH
|
||||
? `${value.slice(0, MAX_STRING_LENGTH)}…(${value.length})`
|
||||
: value;
|
||||
}
|
||||
|
||||
if (typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (depth >= 4) {
|
||||
return '[Truncated]';
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const items = value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitizeValue(item, depth + 1));
|
||||
if (value.length > MAX_ARRAY_ITEMS) {
|
||||
items.push(`…(+${value.length - MAX_ARRAY_ITEMS} more)`);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
|
||||
if (SENSITIVE_KEY_PATTERN.test(key)) {
|
||||
result[key] = '[Redacted]';
|
||||
continue;
|
||||
}
|
||||
result[key] = sanitizeValue(nested, depth + 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const OMITTED_CONTROLLER_ARG_KEYS = new Set([
|
||||
'apiClientProps',
|
||||
'body',
|
||||
'context',
|
||||
'query',
|
||||
'signal',
|
||||
]);
|
||||
|
||||
const sanitizeControllerArgs = (endpoint: string, args: unknown[]): unknown => {
|
||||
if (endpoint === 'authenticate') {
|
||||
const [url] = args;
|
||||
return { url: typeof url === 'string' ? url : undefined };
|
||||
}
|
||||
|
||||
const first = args[0];
|
||||
if (!first || typeof first !== 'object') {
|
||||
return args.length === 0 ? undefined : sanitizeValue(args);
|
||||
}
|
||||
|
||||
const input = first as Record<string, unknown>;
|
||||
const sanitized: Record<string, unknown> = {};
|
||||
|
||||
if (input.query !== undefined) {
|
||||
sanitized.query = sanitizeValue(input.query);
|
||||
}
|
||||
|
||||
if (input.body !== undefined) {
|
||||
sanitized.body = sanitizeValue(input.body);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (OMITTED_CONTROLLER_ARG_KEYS.has(key)) {
|
||||
continue;
|
||||
}
|
||||
sanitized[key] = sanitizeValue(value);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
const OMITTED_CONTROLLER_LOG_ENDPOINTS = new Set<keyof ControllerEndpoint>([
|
||||
'getDownloadUrl',
|
||||
'getImageRequest',
|
||||
'getImageUrl',
|
||||
'getStreamUrl',
|
||||
]);
|
||||
|
||||
const apiController = <K extends keyof ControllerEndpoint>(
|
||||
endpoint: K,
|
||||
type?: ServerType,
|
||||
@@ -37,27 +127,71 @@ const apiController = <K extends keyof ControllerEndpoint>(
|
||||
message: i18n.t('error.serverNotSelectedError') as string,
|
||||
title: i18n.t('error.apiRouteError') as string,
|
||||
});
|
||||
|
||||
logger.error('No server selected', {
|
||||
serverType,
|
||||
});
|
||||
throw new Error(`No server selected`);
|
||||
}
|
||||
|
||||
const controllerFn = endpoints?.[serverType]?.[endpoint];
|
||||
|
||||
if (typeof controllerFn !== 'function') {
|
||||
toast.error({
|
||||
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
|
||||
title: i18n.t('error.apiRouteError') as string,
|
||||
logger.error(`Endpoint ${endpoint} is not implemented for ${serverType}`, {
|
||||
endpoint,
|
||||
serverType,
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
i18n.t('error.endpointNotImplementedError', {
|
||||
endpoint,
|
||||
|
||||
serverType,
|
||||
}) as string,
|
||||
);
|
||||
}
|
||||
|
||||
return controllerFn;
|
||||
if (OMITTED_CONTROLLER_LOG_ENDPOINTS.has(endpoint)) {
|
||||
return controllerFn;
|
||||
}
|
||||
|
||||
return ((...args: unknown[]) => {
|
||||
const started = performance.now();
|
||||
const serverId = (args[0] as undefined | { apiClientProps?: { serverId?: string } })
|
||||
?.apiClientProps?.serverId;
|
||||
|
||||
const logResult = (error?: unknown, value?: unknown) => {
|
||||
logger.debug(`Controller ${String(endpoint)}${error != null ? ' failed' : ''}`, {
|
||||
args: sanitizeControllerArgs(String(endpoint), args),
|
||||
durationMs: Math.round(performance.now() - started),
|
||||
serverId,
|
||||
serverType,
|
||||
...(error != null
|
||||
? { error: error instanceof Error ? error.message : String(error) }
|
||||
: { result: sanitizeValue(value) }),
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = (controllerFn as (...a: unknown[]) => unknown)(...args);
|
||||
if (result instanceof Promise) {
|
||||
return result.then(
|
||||
(value) => {
|
||||
logResult(undefined, value);
|
||||
return value;
|
||||
},
|
||||
(error) => {
|
||||
logResult(error);
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
}
|
||||
logResult(undefined, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logResult(error);
|
||||
throw error;
|
||||
}
|
||||
}) as NonNullable<InternalControllerEndpoint[K]>;
|
||||
};
|
||||
|
||||
const getPathReplaceSettings = () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import qs from 'qs';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { authenticationFailure } from '/@/renderer/api/utils';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
|
||||
import { resultWithHeaders } from '/@/shared/api/utils';
|
||||
@@ -443,7 +444,7 @@ axiosClient.interceptors.response.use(
|
||||
console.error('Error when trying to reauthenticate: ', newError);
|
||||
|
||||
if (isAxiosError(newError) && newError.code === 'ERR_NETWORK') {
|
||||
console.log(
|
||||
logger.warn(
|
||||
'Network error during reauthentication - preserving credentials',
|
||||
);
|
||||
} else {
|
||||
@@ -460,7 +461,7 @@ axiosClient.interceptors.response.use(
|
||||
}
|
||||
|
||||
if (isAxiosError(error) && error.code === 'ERR_NETWORK') {
|
||||
console.log('Network error during authentication - preserving credentials');
|
||||
logger.warn('Network error during authentication - preserving credentials');
|
||||
} else {
|
||||
limitedFail(currentServer);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
getDirectPlayProfiles,
|
||||
} from '/@/renderer/features/player/components/audio-players';
|
||||
import { randomString } from '/@/renderer/utils';
|
||||
import { logFn } from '/@/renderer/utils/logger';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
|
||||
import {
|
||||
@@ -1426,13 +1426,13 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
if (jukeboxStatus.status === 200 && !(jukeboxStatus.body as any)?.error) {
|
||||
features[ServerFeature.JUKEBOX] = [1];
|
||||
} else {
|
||||
console.log(
|
||||
logger.warn(
|
||||
'Jukebox endpoint returned an error payload:',
|
||||
(jukeboxStatus.body as any)?.error,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Jukebox is not supported by this server:', error);
|
||||
logger.warn('Jukebox is not supported by this server:', error);
|
||||
}
|
||||
|
||||
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion };
|
||||
@@ -1949,7 +1949,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
// If the server returns an error for transcodeDecision, fall back to direct stream so that we don't break the player
|
||||
if (transcodeDecision.status !== 200) {
|
||||
logFn.error(
|
||||
logger.error(
|
||||
`Failed to get transcode decision for song ${id}, falling back to direct stream`,
|
||||
);
|
||||
return streamUrl;
|
||||
@@ -1963,7 +1963,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return streamUrl;
|
||||
}
|
||||
|
||||
logFn.info(`Song ${id} requires transcoding: ${[td.transcodeReason].join(', ')}`);
|
||||
logger.info(`Song ${id} requires transcoding: ${[td.transcodeReason].join(', ')}`);
|
||||
|
||||
// If the server does not return transcode params, manually create the transcode params
|
||||
if (!td.transcodeParams) {
|
||||
|
||||
@@ -16,8 +16,7 @@ import {
|
||||
usePlayerStore,
|
||||
useSettingsStore,
|
||||
} from '/@/renderer/store';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { LyricSource, ServerType } from '/@/shared/types/domain-types';
|
||||
import { FontType, Platform, PlayerStyle, PlayerType } from '/@/shared/types/types';
|
||||
|
||||
@@ -275,10 +274,7 @@ export const useAppTracker = () => {
|
||||
if (lastTrackedDate !== todayUTC) {
|
||||
appTrackerInFlight = true;
|
||||
const properties = getProperties();
|
||||
logFn.info(logMsg[LogCategory.ANALYTICS].appTracked, {
|
||||
category: LogCategory.ANALYTICS,
|
||||
meta: { properties, todayUTC },
|
||||
});
|
||||
logger.debug('Analytics sent', { properties, todayUTC });
|
||||
|
||||
trackAppViewMutation(undefined, {
|
||||
onError: () => {},
|
||||
@@ -295,10 +291,7 @@ export const useAppTracker = () => {
|
||||
appTrackerLastSentDate = utcDate;
|
||||
localStorage.setItem('analytics_app_tracker_timestamp', utcDate);
|
||||
|
||||
logFn.debug(logMsg[LogCategory.ANALYTICS].appTracked, {
|
||||
category: LogCategory.ANALYTICS,
|
||||
meta: { properties },
|
||||
});
|
||||
logger.debug('Analytics sent', { properties });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import { useLocation } from 'react-router';
|
||||
|
||||
import { isAnalyticsDisabled } from '/@/renderer/features/analytics/hooks/use-analytics-disabled';
|
||||
import { getRoutePattern } from '/@/renderer/features/analytics/utils/get-route-pattern';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
const trackPageView = (routePattern: string) => {
|
||||
window.umami?.track((props) => ({
|
||||
language: props.language,
|
||||
@@ -28,10 +26,7 @@ export const usePageTracker = () => {
|
||||
|
||||
trackPageViewMutation(routePattern, {
|
||||
onSettled: () => {
|
||||
logFn.debug(logMsg[LogCategory.ANALYTICS].pageViewTracked, {
|
||||
category: LogCategory.ANALYTICS,
|
||||
meta: { route: routePattern },
|
||||
});
|
||||
logger.debug('Page view tracked', { route: routePattern });
|
||||
},
|
||||
});
|
||||
}, [routePattern, trackPageViewMutation]);
|
||||
|
||||
@@ -22,8 +22,7 @@ import {
|
||||
useTimestampStoreBase,
|
||||
} from '/@/renderer/store';
|
||||
import { sentenceCase } from '/@/renderer/utils';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
|
||||
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
@@ -112,13 +111,10 @@ export const useDiscordRpc = () => {
|
||||
reason = 'paused_with_show_paused_disabled';
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcActivityCleared, {
|
||||
category: LogCategory.EXTERNAL,
|
||||
meta: {
|
||||
reason,
|
||||
status: current[2],
|
||||
trigger,
|
||||
},
|
||||
logger.debug('Activity was cleared for Discord RPC', {
|
||||
reason,
|
||||
status: current[2],
|
||||
trigger,
|
||||
});
|
||||
return discordRpc?.clearActivity();
|
||||
}
|
||||
@@ -151,24 +147,20 @@ export const useDiscordRpc = () => {
|
||||
|
||||
const isConnected = await discordRpc?.isConnected();
|
||||
if (!isConnected) {
|
||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcInitialized, {
|
||||
category: LogCategory.EXTERNAL,
|
||||
meta: { clientId: discordSettings.clientId },
|
||||
logger.info('Discord RPC was initialized', {
|
||||
clientId: discordSettings.clientId,
|
||||
});
|
||||
previousEnabledRef.current = true;
|
||||
await discordRpc?.initialize(discordSettings.clientId);
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcSetActivity, {
|
||||
category: LogCategory.EXTERNAL,
|
||||
meta: {
|
||||
currentStatus: current[2],
|
||||
reason: 'radio',
|
||||
showAsListening: discordSettings.showAsListening,
|
||||
stationName: stationName || 'Radio',
|
||||
title,
|
||||
trigger,
|
||||
},
|
||||
logger.debug('Activity was set for Discord RPC', {
|
||||
currentStatus: current[2],
|
||||
reason: 'radio',
|
||||
showAsListening: discordSettings.showAsListening,
|
||||
stationName: stationName || 'Radio',
|
||||
title,
|
||||
trigger,
|
||||
});
|
||||
discordRpc?.setActivity(activity);
|
||||
return;
|
||||
@@ -179,13 +171,10 @@ export const useDiscordRpc = () => {
|
||||
}
|
||||
|
||||
if (trackChanged) {
|
||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcTrackChanged, {
|
||||
category: LogCategory.EXTERNAL,
|
||||
meta: {
|
||||
artistName: song.artists?.[0]?.name,
|
||||
songId: song._uniqueId,
|
||||
songName: song.name,
|
||||
},
|
||||
logger.debug('Track was changed for Discord RPC', {
|
||||
artistName: song.artists?.[0]?.name,
|
||||
songId: song._uniqueId,
|
||||
songName: song.name,
|
||||
});
|
||||
setlastUniqueId(song._uniqueId);
|
||||
}
|
||||
@@ -319,11 +308,8 @@ export const useDiscordRpc = () => {
|
||||
// Initialize if needed
|
||||
const isConnected = await discordRpc?.isConnected();
|
||||
if (!isConnected) {
|
||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcInitialized, {
|
||||
category: LogCategory.EXTERNAL,
|
||||
meta: {
|
||||
clientId: discordSettings.clientId,
|
||||
},
|
||||
logger.info('Discord RPC was initialized', {
|
||||
clientId: discordSettings.clientId,
|
||||
});
|
||||
|
||||
previousEnabledRef.current = true;
|
||||
@@ -331,22 +317,19 @@ export const useDiscordRpc = () => {
|
||||
await discordRpc?.initialize(discordSettings.clientId);
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcSetActivity, {
|
||||
category: LogCategory.EXTERNAL,
|
||||
meta: {
|
||||
albumName: song.album,
|
||||
artistName: song.artists?.[0]?.name,
|
||||
currentStatus: current[2],
|
||||
currentTime: current[1],
|
||||
displayType: discordSettings.displayType,
|
||||
hasLargeImage: !!activity.largeImageKey,
|
||||
hasTimestamps: !!(activity.startTimestamp && activity.endTimestamp),
|
||||
reason,
|
||||
showAsListening: discordSettings.showAsListening,
|
||||
songName: song.name,
|
||||
trackChanged,
|
||||
trigger,
|
||||
},
|
||||
logger.debug('Activity was set for Discord RPC', {
|
||||
albumName: song.album,
|
||||
artistName: song.artists?.[0]?.name,
|
||||
currentStatus: current[2],
|
||||
currentTime: current[1],
|
||||
displayType: discordSettings.displayType,
|
||||
hasLargeImage: !!activity.largeImageKey,
|
||||
hasTimestamps: !!(activity.startTimestamp && activity.endTimestamp),
|
||||
reason,
|
||||
showAsListening: discordSettings.showAsListening,
|
||||
songName: song.name,
|
||||
trackChanged,
|
||||
trigger,
|
||||
});
|
||||
discordRpc?.setActivity(activity);
|
||||
},
|
||||
@@ -374,12 +357,9 @@ export const useDiscordRpc = () => {
|
||||
// Quit Discord RPC if it was enabled and is now disabled
|
||||
useEffect(() => {
|
||||
if ((!discordSettings.enabled || privateMode) && Boolean(previousEnabledRef.current)) {
|
||||
logFn.info(logMsg[LogCategory.EXTERNAL].discordRpcQuit, {
|
||||
category: LogCategory.EXTERNAL,
|
||||
meta: {
|
||||
enabled: discordSettings.enabled,
|
||||
privateMode,
|
||||
},
|
||||
logger.info('Discord RPC was quit', {
|
||||
enabled: discordSettings.enabled,
|
||||
privateMode,
|
||||
});
|
||||
|
||||
previousEnabledRef.current = false;
|
||||
|
||||
@@ -5,8 +5,7 @@ import { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'r
|
||||
|
||||
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
||||
import { convertToLogVolume } from '/@/renderer/features/player/audio-player/utils/player-utils';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export interface WebPlayerEngineHandle extends AudioPlayer {
|
||||
@@ -174,6 +173,21 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
||||
player2Ref.current?.getInternalPlayer()?.pause();
|
||||
}, []);
|
||||
|
||||
const mediaErrorLabel = (code: number | undefined) => {
|
||||
switch (code) {
|
||||
case MediaError.MEDIA_ERR_ABORTED:
|
||||
return 'ABORTED';
|
||||
case MediaError.MEDIA_ERR_DECODE:
|
||||
return 'DECODE';
|
||||
case MediaError.MEDIA_ERR_NETWORK:
|
||||
return 'NETWORK';
|
||||
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
||||
return 'SRC_NOT_SUPPORTED';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnError = (
|
||||
playerRef: React.RefObject<null | ReactPlayer>,
|
||||
onEnded: () => void,
|
||||
@@ -188,27 +202,30 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
||||
}
|
||||
|
||||
const { error } = target;
|
||||
|
||||
logFn.error(logMsg[LogCategory.PLAYER].playbackError, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { error },
|
||||
});
|
||||
const code = error?.code;
|
||||
const label = mediaErrorLabel(code);
|
||||
|
||||
const isNetworkError =
|
||||
error?.code === MediaError.MEDIA_ERR_NETWORK ||
|
||||
error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
|
||||
code === MediaError.MEDIA_ERR_NETWORK ||
|
||||
code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
|
||||
|
||||
if (isNetworkError) {
|
||||
if (networkRetryCountRef.current < MAX_NETWORK_RETRIES) {
|
||||
networkRetryCountRef.current += 1;
|
||||
logger.warn('Playback error, retrying', {
|
||||
code,
|
||||
label,
|
||||
retryCount: networkRetryCountRef.current,
|
||||
});
|
||||
const audio = target;
|
||||
setTimeout(() => {
|
||||
pauseBothPlayers();
|
||||
audio.load();
|
||||
audio.play().catch(() => {
|
||||
logFn.error(logMsg[LogCategory.PLAYER].playbackError, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { error: 'Failed to play audio after network error' },
|
||||
logger.error('Playback error, retries exhausted', {
|
||||
code,
|
||||
label,
|
||||
retryCount: networkRetryCountRef.current,
|
||||
});
|
||||
});
|
||||
}, NETWORK_RETRY_DELAY_MS);
|
||||
@@ -216,14 +233,24 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (error?.code !== MediaError.MEDIA_ERR_DECODE && !isNetworkError) {
|
||||
if (code !== MediaError.MEDIA_ERR_DECODE && !isNetworkError) {
|
||||
return;
|
||||
}
|
||||
|
||||
pauseBothPlayers();
|
||||
if (error?.code === MediaError.MEDIA_ERR_DECODE) {
|
||||
if (code === MediaError.MEDIA_ERR_DECODE) {
|
||||
logger.error('Playback decode error, skipping track', {
|
||||
code,
|
||||
label,
|
||||
retryCount: networkRetryCountRef.current,
|
||||
});
|
||||
onEnded();
|
||||
} else {
|
||||
logger.error('Playback error, pausing', {
|
||||
code,
|
||||
label,
|
||||
retryCount: networkRetryCountRef.current,
|
||||
});
|
||||
if (onErrorPause) {
|
||||
onErrorPause();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
usePlaybackType,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store';
|
||||
import { logFn } from '/@/renderer/utils/logger';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
@@ -99,7 +99,7 @@ function detectBrowserProfile() {
|
||||
}
|
||||
}
|
||||
|
||||
logFn.info('DIRECT_PLAY_PROFILES', { meta: DIRECT_PLAY_PROFILES });
|
||||
logger.debug('DIRECT_PLAY_PROFILES', DIRECT_PLAY_PROFILES);
|
||||
|
||||
return DIRECT_PLAY_PROFILES;
|
||||
}
|
||||
@@ -158,6 +158,8 @@ export const AudioPlayers = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const mpvPlayerListener = isElectron() ? window.api.mpvPlayerListener : null;
|
||||
|
||||
const AudioPlayersContent = ({
|
||||
audioContext,
|
||||
audioDeviceId,
|
||||
@@ -179,6 +181,24 @@ const AudioPlayersContent = ({
|
||||
}) => {
|
||||
const isRadioActive = useIsRadioActive();
|
||||
|
||||
useEffect(() => {
|
||||
logger.info('Playback engine', { playbackType });
|
||||
}, [playbackType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mpvPlayerListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
mpvPlayerListener.rendererPlayerFallback((isFallback: boolean) => {
|
||||
if (isFallback) {
|
||||
logger.warn('Playback engine fell back to web');
|
||||
} else {
|
||||
logger.info('Playback engine using local (mpv)');
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (webAudio && 'AudioContext' in window) {
|
||||
let context: AudioContext;
|
||||
|
||||
@@ -18,8 +18,7 @@ import {
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||
import { AddToQueueType, usePlayerActions, useSettingsStore } from '/@/renderer/store';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { shuffle as shuffleArray } from '/@/renderer/utils/shuffle';
|
||||
import { sortSongsByFetchedOrder } from '/@/shared/api/utils';
|
||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||
@@ -229,22 +228,20 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
|
||||
const edge = type.edge === 'top' ? 'top' : 'bottom';
|
||||
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].addToQueueByData, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: {
|
||||
data: data.length,
|
||||
edge,
|
||||
filtered: filteredData.length,
|
||||
type,
|
||||
uniqueId: type.uniqueId,
|
||||
},
|
||||
logger.debug('Added to queue by data', {
|
||||
data: data.length,
|
||||
edge,
|
||||
filtered: filteredData.length,
|
||||
type,
|
||||
uniqueId: type.uniqueId,
|
||||
});
|
||||
|
||||
storeActions.addToQueueByUniqueId(filteredData, type.uniqueId, edge, playSongId);
|
||||
} else {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].addToQueueByType, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { data: data.length, filtered: filteredData.length, type },
|
||||
logger.debug('Added to queue by type', {
|
||||
data: data.length,
|
||||
filtered: filteredData.length,
|
||||
type,
|
||||
});
|
||||
|
||||
storeActions.addToQueueByType(filteredData, type as Play, playSongId);
|
||||
@@ -281,10 +278,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
try {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].addToQueueByFetch, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { ids: id, itemType, serverId, type },
|
||||
});
|
||||
logger.debug('Added to queue by fetch', { ids: id, itemType, serverId, type });
|
||||
|
||||
const songs = await queryClient.fetchQuery({
|
||||
gcTime: 0,
|
||||
@@ -361,10 +355,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
let toastId: null | string = null;
|
||||
let fetchId: null | string = null;
|
||||
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].addToQueueByListQuery, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { itemType, query, serverId, type },
|
||||
});
|
||||
logger.debug('Added to queue by list query', { itemType, query, serverId, type });
|
||||
|
||||
try {
|
||||
let totalCount = 0;
|
||||
@@ -440,10 +431,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
autoClose: false,
|
||||
message: t('player.playbackFetchCancel'),
|
||||
onClose: () => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].cancelledFetch, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { itemType, serverId },
|
||||
});
|
||||
logger.debug('Cancelled fetch', { itemType, serverId });
|
||||
|
||||
queryClient.cancelQueries({
|
||||
exact: false,
|
||||
@@ -538,19 +526,14 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
);
|
||||
|
||||
const clearQueue = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].clearQueue, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Cleared queue');
|
||||
|
||||
storeActions.clearQueue();
|
||||
}, [storeActions]);
|
||||
|
||||
const clearSelected = useCallback(
|
||||
(items: QueueSong[]) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].clearSelected, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { items: items.length },
|
||||
});
|
||||
logger.debug('Cleared selected', { items: items.length });
|
||||
|
||||
storeActions.clearSelected(items);
|
||||
},
|
||||
@@ -559,10 +542,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const decreaseVolume = useCallback(
|
||||
(amount: number) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].decreaseVolume, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { amount },
|
||||
});
|
||||
logger.debug('Decreased volume', { amount });
|
||||
|
||||
storeActions.decreaseVolume(amount);
|
||||
},
|
||||
@@ -570,9 +550,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
);
|
||||
|
||||
const getQueue = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].clearQueue, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Cleared queue');
|
||||
|
||||
const queue = storeActions.getQueue();
|
||||
return queue.items;
|
||||
@@ -580,10 +558,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const increaseVolume = useCallback(
|
||||
(amount: number) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].increaseVolume, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { amount },
|
||||
});
|
||||
logger.debug('Increased volume', { amount });
|
||||
|
||||
storeActions.increaseVolume(amount);
|
||||
},
|
||||
@@ -592,9 +567,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const mediaNext = useCallback(
|
||||
(toNextAlbum: boolean) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaNext, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Media next');
|
||||
|
||||
storeActions.mediaNext(toNextAlbum);
|
||||
},
|
||||
@@ -602,19 +575,14 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
);
|
||||
|
||||
const mediaPause = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaPause, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Media pause');
|
||||
|
||||
storeActions.mediaPause();
|
||||
}, [storeActions]);
|
||||
|
||||
const mediaPlay = useCallback(
|
||||
(id?: string) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaPlay, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { id },
|
||||
});
|
||||
logger.debug('Media play', { id });
|
||||
|
||||
storeActions.mediaPlay(id);
|
||||
},
|
||||
@@ -623,10 +591,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const mediaPlayByIndex = useCallback(
|
||||
(index: number) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaPlayByIndex, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { index },
|
||||
});
|
||||
logger.debug('Media play by index', { index });
|
||||
|
||||
storeActions.mediaPlayByIndex(index);
|
||||
},
|
||||
@@ -635,9 +600,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const mediaPrevious = useCallback(
|
||||
(toPreviousAlbum: boolean) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaPrevious, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Media previous');
|
||||
|
||||
storeActions.mediaPrevious(toPreviousAlbum);
|
||||
},
|
||||
@@ -646,10 +609,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const mediaStop = useCallback(
|
||||
(options?: { reset?: boolean }) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaStop, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { reset: options?.reset },
|
||||
});
|
||||
logger.debug('Media stop', { reset: options?.reset });
|
||||
|
||||
storeActions.mediaStop(options);
|
||||
},
|
||||
@@ -658,10 +618,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const mediaSeekToTimestamp = useCallback(
|
||||
(timestamp: number) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaSeekToTimestamp, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { timestamp },
|
||||
});
|
||||
logger.debug('Media seek to timestamp', { timestamp });
|
||||
|
||||
storeActions.mediaSeekToTimestamp(timestamp);
|
||||
},
|
||||
@@ -669,30 +626,23 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
);
|
||||
|
||||
const mediaSkipBackward = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaSkipBackward, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Media skip backward');
|
||||
|
||||
storeActions.mediaSkipBackward();
|
||||
}, [storeActions]);
|
||||
|
||||
const mediaSkipForward = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaSkipForward, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Media skip forward');
|
||||
|
||||
storeActions.mediaSkipForward();
|
||||
}, [storeActions]);
|
||||
|
||||
const setQueue = useCallback(
|
||||
(data: Song[], index?: number, position?: number) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].setQueue, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: {
|
||||
data: data.length,
|
||||
index,
|
||||
position,
|
||||
},
|
||||
logger.debug('Set queue', {
|
||||
data: data.length,
|
||||
index,
|
||||
position,
|
||||
});
|
||||
|
||||
storeActions.setQueue(data, index, position);
|
||||
@@ -702,10 +652,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const setSpeed = useCallback(
|
||||
(speed: number) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].setSpeed, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { speed },
|
||||
});
|
||||
logger.debug('Set speed', { speed });
|
||||
|
||||
storeActions.setSpeed(speed);
|
||||
},
|
||||
@@ -713,27 +660,20 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
);
|
||||
|
||||
const mediaToggleMute = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaToggleMute, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Media toggle mute');
|
||||
|
||||
storeActions.mediaToggleMute();
|
||||
}, [storeActions]);
|
||||
|
||||
const mediaTogglePlayPause = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaTogglePlayPause, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Media toggle play pause');
|
||||
|
||||
storeActions.mediaTogglePlayPause();
|
||||
}, [storeActions]);
|
||||
|
||||
const moveSelectedTo = useCallback(
|
||||
(items: QueueSong[], edge: 'bottom' | 'top', uniqueId: string) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].moveSelectedTo, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { edge, items, uniqueId },
|
||||
});
|
||||
logger.debug('Moved selected to', { edge, items, uniqueId });
|
||||
|
||||
storeActions.moveSelectedTo(items, uniqueId, edge);
|
||||
},
|
||||
@@ -742,10 +682,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const moveSelectedToBottom = useCallback(
|
||||
(items: QueueSong[]) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].moveSelectedToBottom, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { items },
|
||||
});
|
||||
logger.debug('Moved selected to bottom', { items });
|
||||
|
||||
storeActions.moveSelectedToBottom(items);
|
||||
},
|
||||
@@ -754,10 +691,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const moveSelectedToNext = useCallback(
|
||||
(items: QueueSong[]) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].moveSelectedToNext, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { items },
|
||||
});
|
||||
logger.debug('Moved selected to next', { items });
|
||||
|
||||
storeActions.moveSelectedToNext(items);
|
||||
},
|
||||
@@ -766,10 +700,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const moveSelectedToTop = useCallback(
|
||||
(items: QueueSong[]) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].moveSelectedToTop, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { items },
|
||||
});
|
||||
logger.debug('Moved selected to top', { items });
|
||||
|
||||
storeActions.moveSelectedToTop(items);
|
||||
},
|
||||
@@ -778,10 +709,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const setVolume = useCallback(
|
||||
(volume: number) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].setVolume, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { volume },
|
||||
});
|
||||
logger.debug('Set volume', { volume });
|
||||
|
||||
storeActions.setVolume(volume);
|
||||
},
|
||||
@@ -790,10 +718,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const setRepeat = useCallback(
|
||||
(repeat: PlayerRepeat) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].setRepeat, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { repeat },
|
||||
});
|
||||
logger.debug('Set repeat', { repeat });
|
||||
|
||||
storeActions.setRepeat(repeat);
|
||||
},
|
||||
@@ -802,10 +727,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const setShuffle = useCallback(
|
||||
(shuffle: PlayerShuffle) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].setShuffle, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { shuffle },
|
||||
});
|
||||
logger.debug('Set shuffle', { shuffle });
|
||||
|
||||
storeActions.setShuffle(shuffle);
|
||||
},
|
||||
@@ -813,27 +735,20 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
);
|
||||
|
||||
const shuffle = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].shuffle, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Shuffle');
|
||||
|
||||
storeActions.shuffle();
|
||||
}, [storeActions]);
|
||||
|
||||
const shuffleAll = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].shuffleAll, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Shuffle all');
|
||||
|
||||
storeActions.shuffleAll();
|
||||
}, [storeActions]);
|
||||
|
||||
const shuffleSelected = useCallback(
|
||||
(items: QueueSong[]) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].shuffleSelected, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { items },
|
||||
});
|
||||
logger.debug('Shuffle selected', { items });
|
||||
|
||||
storeActions.shuffleSelected(items);
|
||||
},
|
||||
@@ -841,17 +756,13 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
);
|
||||
|
||||
const toggleRepeat = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].toggleRepeat, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Toggle repeat');
|
||||
|
||||
storeActions.toggleRepeat();
|
||||
}, [storeActions]);
|
||||
|
||||
const toggleShuffle = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].toggleShuffle, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
logger.debug('Toggle shuffle');
|
||||
|
||||
storeActions.toggleShuffle();
|
||||
}, [storeActions]);
|
||||
|
||||
@@ -16,8 +16,7 @@ import {
|
||||
usePlayerStoreBase,
|
||||
useSettingsStore,
|
||||
} from '/@/renderer/store';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
@@ -65,9 +64,9 @@ export const useAutoDJ = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].autoPlayTriggered, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { remaining: properties.remaining, songId: properties.song?.id },
|
||||
logger.info('Auto play triggered', {
|
||||
remaining: properties.remaining,
|
||||
songId: properties.song?.id,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -143,9 +142,9 @@ export const useAutoDJ = () => {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logFn.error(logMsg[LogCategory.PLAYER].autoPlayFailed, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: { error: (error as Error).message, songId: properties.song?.id },
|
||||
logger.error('Auto play failed', {
|
||||
error: (error as Error).message,
|
||||
songId: properties.song?.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
useSettingsStore,
|
||||
useTimestampStoreBase,
|
||||
} from '/@/renderer/store';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
@@ -211,12 +210,9 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledTimeupdate, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: song.id,
|
||||
reason: 'after submission',
|
||||
},
|
||||
logger.debug('Scrobbled a timeupdate event', {
|
||||
id: song.id,
|
||||
reason: 'after submission',
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -305,8 +301,7 @@ export const useScrobble = () => {
|
||||
// },
|
||||
// {
|
||||
// onSuccess: () => {
|
||||
// logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledTimeupdate, {
|
||||
// category: LogCategory.SCROBBLE,
|
||||
// logFn.debug("Scrobbled a timeupdate event", {
|
||||
// meta: {
|
||||
// id: currentSong.id,
|
||||
// },
|
||||
@@ -342,12 +337,9 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledSubmission, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
reason: 'from listened time',
|
||||
},
|
||||
logger.info('Scrobbled a submission event', {
|
||||
id: currentSong.id,
|
||||
reason: 'from listened time',
|
||||
});
|
||||
sendProgressAfterSubmission(currentSong);
|
||||
},
|
||||
@@ -399,11 +391,8 @@ export const useScrobble = () => {
|
||||
silent: true,
|
||||
});
|
||||
} catch (error) {
|
||||
logFn.error('an error occurred while sending a desktop notification', {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
error: error as Error,
|
||||
},
|
||||
logger.error('an error occurred while sending a desktop notification', {
|
||||
error: error as Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -447,11 +436,8 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledStart, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
},
|
||||
logger.info('Scrobbled a start event', {
|
||||
id: currentSong.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -480,11 +466,8 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledStop, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: previousSong.id,
|
||||
},
|
||||
logger.info('Scrobbled a stop event', {
|
||||
id: previousSong.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -578,11 +561,8 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledTimeupdate, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
},
|
||||
logger.debug('Scrobbled a timeupdate event', {
|
||||
id: currentSong.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -633,11 +613,8 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledPause, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
},
|
||||
logger.debug('Scrobbled a pause event', {
|
||||
id: currentSong.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -661,11 +638,8 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledUnpause, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
},
|
||||
logger.debug('Scrobbled an unpause event', {
|
||||
id: currentSong.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -694,11 +668,8 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledStart, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
},
|
||||
logger.info('Scrobbled a start event', {
|
||||
id: currentSong.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -726,11 +697,8 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledStop, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
},
|
||||
logger.info('Scrobbled a stop event', {
|
||||
id: currentSong.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -777,12 +745,9 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledStart, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
reason: 'from repeat',
|
||||
},
|
||||
logger.info('Scrobbled a start event', {
|
||||
id: currentSong.id,
|
||||
reason: 'from repeat',
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -840,12 +805,9 @@ export const useScrobble = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledSubmission, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: song.id,
|
||||
reason: 'forced from UI',
|
||||
},
|
||||
logger.info('Scrobbled a submission event', {
|
||||
id: song.id,
|
||||
reason: 'forced from UI',
|
||||
});
|
||||
sendProgressAfterSubmission(song);
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
import { updateQueueSong } from '/@/renderer/store/player.store';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { QueueSong, SongDetailQuery } from '/@/shared/types/domain-types';
|
||||
|
||||
export const useUpdateCurrentSong = () => {
|
||||
@@ -43,22 +43,16 @@ export const useUpdateCurrentSong = () => {
|
||||
if (!isEqual(currentSongData, updatedSong)) {
|
||||
updateQueueSong(currentSong.id, updatedSong);
|
||||
|
||||
logFn.debug('Song updated in queue', {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
name: updatedSong.name,
|
||||
},
|
||||
logger.debug('Song updated in queue', {
|
||||
id: currentSong.id,
|
||||
name: updatedSong.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logFn.error('Failed to update song in queue', {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
id: currentSong.id,
|
||||
},
|
||||
logger.error('Failed to update song in queue', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
id: currentSong.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,8 +4,7 @@ import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { folderQueries } from '/@/renderer/features/folders/api/folder-api';
|
||||
import { PlayerFilter, useSettingsStore } from '/@/renderer/store';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { resolveSongPath } from '/@/renderer/utils/resolve-song-path';
|
||||
import { sortSongList } from '/@/shared/api/utils';
|
||||
import {
|
||||
@@ -435,23 +434,20 @@ export const filterSongsByPlayerFilters = (songs: Song[], filters: PlayerFilter[
|
||||
});
|
||||
|
||||
if (filteredSongs.length > 0) {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].playerFiltersApplied, {
|
||||
category: LogCategory.PLAYER,
|
||||
meta: {
|
||||
filteredCount: filteredSongs.length,
|
||||
filteredSongs: filteredSongs.map(({ filter, song }) => ({
|
||||
artist: song.artistName,
|
||||
condition: {
|
||||
field: filter.field,
|
||||
operator: filter.operator,
|
||||
value: filter.value,
|
||||
},
|
||||
songId: song.id,
|
||||
songName: song.name,
|
||||
})),
|
||||
originalCount: songs.length,
|
||||
remainingCount: filtered.length,
|
||||
},
|
||||
logger.debug('Player filters applied', {
|
||||
filteredCount: filteredSongs.length,
|
||||
filteredSongs: filteredSongs.map(({ filter, song }) => ({
|
||||
artist: song.artistName,
|
||||
condition: {
|
||||
field: filter.field,
|
||||
operator: filter.operator,
|
||||
value: filter.value,
|
||||
},
|
||||
songId: song.id,
|
||||
songName: song.name,
|
||||
})),
|
||||
originalCount: songs.length,
|
||||
remainingCount: filtered.length,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ import { useDeleteInternetRadioStationImage } from '/@/renderer/features/radio/m
|
||||
import { useUpdateRadioStation } from '/@/renderer/features/radio/mutations/update-radio-station-mutation';
|
||||
import { useUploadInternetRadioStationImage } from '/@/renderer/features/radio/mutations/upload-internet-radio-station-image-mutation';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Box } from '/@/shared/components/box/box';
|
||||
@@ -108,9 +107,7 @@ export const EditRadioStationForm = ({ onCancel, station }: EditRadioStationForm
|
||||
});
|
||||
closeAllModals();
|
||||
} catch (err: unknown) {
|
||||
logFn.error(logMsg.other.error, {
|
||||
meta: { error: err as Error },
|
||||
});
|
||||
logger.error('An error occurred', { error: err as Error });
|
||||
|
||||
toast.error({
|
||||
message: (err as Error)?.message,
|
||||
|
||||
@@ -7,8 +7,7 @@ import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
|
||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||
import { usePlayerActions, usePlayerStore, useRemoteSettings } from '/@/renderer/store';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { PlayerShuffle } from '/@/shared/types/types';
|
||||
@@ -32,13 +31,10 @@ export const useRemote = () => {
|
||||
// we must send this EVEN IF the remote is disabled, as this is what
|
||||
// makes sure that the main process gets the port/username/password on startup
|
||||
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].initializingRemoteSettings, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
enabled: remoteSettings.enabled,
|
||||
port: remoteSettings.port,
|
||||
username: remoteSettings.username,
|
||||
},
|
||||
logger.info('Initializing remote settings', {
|
||||
enabled: remoteSettings.enabled,
|
||||
port: remoteSettings.port,
|
||||
username: remoteSettings.username,
|
||||
});
|
||||
|
||||
remote
|
||||
@@ -49,10 +45,7 @@ export const useRemote = () => {
|
||||
remoteSettings.password,
|
||||
)
|
||||
.catch((error) => {
|
||||
logFn.error(logMsg[LogCategory.REMOTE].failedToEnableRemote, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { error },
|
||||
});
|
||||
logger.error('Failed to enable remote', { error });
|
||||
toast.warn({ message: error, title: 'Failed to enable remote' });
|
||||
});
|
||||
// We only want to fire this once
|
||||
@@ -65,42 +58,35 @@ export const useRemote = () => {
|
||||
}
|
||||
|
||||
remote.requestPosition((data: { position: number }) => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].requestPositionReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { position: data.position },
|
||||
});
|
||||
logger.debug('Request position received', { position: data.position });
|
||||
const newTime = data.position;
|
||||
player.mediaSeekToTimestamp(newTime);
|
||||
});
|
||||
|
||||
remote.requestSeek((data: { offset: number }) => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].requestSeekReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { offset: data.offset },
|
||||
});
|
||||
logger.debug('Request seek received', { offset: data.offset });
|
||||
mediaSkipForward(data.offset);
|
||||
});
|
||||
|
||||
remote.requestRating((data: { id: string; rating: number; serverId: string }) => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].requestRatingReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { id: data.id, rating: data.rating, serverId: data.serverId },
|
||||
logger.debug('Request rating received', {
|
||||
id: data.id,
|
||||
rating: data.rating,
|
||||
serverId: data.serverId,
|
||||
});
|
||||
setRating(data.serverId, [data.id], LibraryItem.SONG, data.rating);
|
||||
});
|
||||
|
||||
remote.requestVolume((data: { volume: number }) => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].requestVolumeReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { volume: data.volume },
|
||||
});
|
||||
logger.debug('Request volume received', { volume: data.volume });
|
||||
setVolume(data.volume);
|
||||
});
|
||||
|
||||
remote.requestFavorite((data: { favorite: boolean; id: string; serverId: string }) => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].requestFavoriteReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { favorite: data.favorite, id: data.id, serverId: data.serverId },
|
||||
logger.debug('Request favorite received', {
|
||||
favorite: data.favorite,
|
||||
id: data.id,
|
||||
serverId: data.serverId,
|
||||
});
|
||||
const mutator = data.favorite ? addToFavoritesMutation : removeFromFavoritesMutation;
|
||||
mutator.mutate({
|
||||
@@ -141,13 +127,10 @@ export const useRemote = () => {
|
||||
const currentSong = player.getCurrentSong();
|
||||
|
||||
if (currentSong) {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].sendingInitialSong, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
artistName: currentSong.artistName,
|
||||
id: currentSong.id,
|
||||
name: currentSong.name,
|
||||
},
|
||||
logger.debug('Sending initial song', {
|
||||
artistName: currentSong.artistName,
|
||||
id: currentSong.id,
|
||||
name: currentSong.name,
|
||||
});
|
||||
|
||||
const imageUrl =
|
||||
@@ -171,14 +154,11 @@ export const useRemote = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].updateSongSent, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
artistName: properties.song?.artistName,
|
||||
id: properties.song?.id,
|
||||
index: properties.index,
|
||||
name: properties.song?.name,
|
||||
},
|
||||
logger.debug('Update song sent', {
|
||||
artistName: properties.song?.artistName,
|
||||
id: properties.song?.id,
|
||||
index: properties.index,
|
||||
name: properties.song?.name,
|
||||
});
|
||||
if (properties.song) {
|
||||
const song = properties.song;
|
||||
@@ -202,10 +182,7 @@ export const useRemote = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].updatePositionSent, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { timestamp: properties.timestamp },
|
||||
});
|
||||
logger.debug('Update position sent', { timestamp: properties.timestamp });
|
||||
remote.updatePosition(properties.timestamp);
|
||||
},
|
||||
onPlayerRepeat: (properties) => {
|
||||
@@ -213,10 +190,7 @@ export const useRemote = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].updateRepeatSent, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { repeat: properties.repeat },
|
||||
});
|
||||
logger.debug('Update repeat sent', { repeat: properties.repeat });
|
||||
remote.updateRepeat(properties.repeat);
|
||||
},
|
||||
onPlayerShuffle: (properties) => {
|
||||
@@ -225,9 +199,9 @@ export const useRemote = () => {
|
||||
}
|
||||
|
||||
const isShuffleEnabled = properties.shuffle !== PlayerShuffle.NONE;
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].updateShuffleSent, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { isShuffleEnabled, shuffle: properties.shuffle },
|
||||
logger.debug('Update shuffle sent', {
|
||||
isShuffleEnabled,
|
||||
shuffle: properties.shuffle,
|
||||
});
|
||||
remote.updateShuffle(isShuffleEnabled);
|
||||
},
|
||||
@@ -236,10 +210,7 @@ export const useRemote = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].updatePlaybackSent, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { status: properties.status },
|
||||
});
|
||||
logger.debug('Update playback sent', { status: properties.status });
|
||||
remote.updatePlayback(properties.status);
|
||||
},
|
||||
onPlayerVolume: (properties) => {
|
||||
@@ -247,10 +218,7 @@ export const useRemote = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].updateVolumeSent, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { volume: properties.volume },
|
||||
});
|
||||
logger.debug('Update volume sent', { volume: properties.volume });
|
||||
remote.updateVolume(properties.volume);
|
||||
},
|
||||
onUserFavorite: (properties) => {
|
||||
@@ -258,13 +226,10 @@ export const useRemote = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].updateFavoriteSent, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
favorite: properties.favorite,
|
||||
id: properties.id,
|
||||
serverId: properties.serverId,
|
||||
},
|
||||
logger.debug('Update favorite sent', {
|
||||
favorite: properties.favorite,
|
||||
id: properties.id,
|
||||
serverId: properties.serverId,
|
||||
});
|
||||
remote.updateFavorite(properties.favorite, properties.serverId, properties.id);
|
||||
},
|
||||
@@ -273,13 +238,10 @@ export const useRemote = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].updateRatingSent, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
id: properties.id,
|
||||
rating: properties.rating || 0,
|
||||
serverId: properties.serverId,
|
||||
},
|
||||
logger.debug('Update rating sent', {
|
||||
id: properties.id,
|
||||
rating: properties.rating || 0,
|
||||
serverId: properties.serverId,
|
||||
});
|
||||
remote.updateRating(properties.rating || 0, properties.serverId, properties.id);
|
||||
},
|
||||
|
||||
@@ -1,71 +1,160 @@
|
||||
import { memo } from 'react';
|
||||
import { ComboboxItem, ComboboxLikeRenderOptionInput } from '@mantine/core';
|
||||
import isElectron from 'is-electron';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { logFn, LogLevel } from '/@/renderer/utils/logger';
|
||||
import { useCurrentServer, useSettingsStore } from '/@/renderer/store';
|
||||
import { logger, LogLevel, normalizeLogLevel } from '/@/renderer/utils/logger';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { isLocalUrl } from '/@/shared/utils/is-local-url';
|
||||
|
||||
const DEFAULT_LOG_LEVEL: LogLevel = process.env.NODE_ENV === 'production' ? 'info' : 'debug';
|
||||
const utils = isElectron() ? window.api.utils : null;
|
||||
|
||||
const LOG_LEVEL_DESCRIPTION_CONTEXT: Record<LogLevel, string> = {
|
||||
debug: 'optionDebugDescription',
|
||||
info: 'optionInfoDescription',
|
||||
};
|
||||
|
||||
const LogLevelSelectOption = ({ option }: ComboboxLikeRenderOptionInput<ComboboxItem>) => {
|
||||
const { t } = useTranslation();
|
||||
const level = normalizeLogLevel(option.value);
|
||||
|
||||
return (
|
||||
<Stack gap={2} style={{ flex: 1, paddingBlock: 4 }}>
|
||||
<Text fw={500}>{option.label}</Text>
|
||||
<Text isMuted size="sm">
|
||||
{t('setting.logLevel', {
|
||||
context: LOG_LEVEL_DESCRIPTION_CONTEXT[level],
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const getRendererSettingsForExport = (): Record<string, unknown> => {
|
||||
const state = { ...useSettingsStore.getState() } as Record<string, unknown>;
|
||||
delete state.actions;
|
||||
return state;
|
||||
};
|
||||
|
||||
export const LoggerSettings = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const currentServer = useCurrentServer();
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const getCurrentLogLevel = (): LogLevel => {
|
||||
const stored = localStorage.getItem('log_level');
|
||||
if (stored && ['debug', 'error', 'info', 'warn'].includes(stored)) {
|
||||
return stored as LogLevel;
|
||||
}
|
||||
return DEFAULT_LOG_LEVEL;
|
||||
return normalizeLogLevel(localStorage.getItem('log_level') ?? DEFAULT_LOG_LEVEL);
|
||||
};
|
||||
|
||||
const handleLogLevelChange = (value: null | string) => {
|
||||
if (!value) return;
|
||||
|
||||
const logLevel = value as LogLevel;
|
||||
const logLevel = normalizeLogLevel(value);
|
||||
localStorage.setItem('log_level', logLevel);
|
||||
logger.updateLogLevel(logLevel);
|
||||
};
|
||||
|
||||
// Update the logger dynamically
|
||||
if (logFn.updateLogLevel) {
|
||||
logFn.updateLogLevel(logLevel);
|
||||
const handleOpenLogsFolder = async () => {
|
||||
await utils?.openLogsFolder();
|
||||
};
|
||||
|
||||
const handleExportDiagnostics = async () => {
|
||||
if (!utils || isExporting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsExporting(true);
|
||||
try {
|
||||
const result = await utils.exportDiagnostics({
|
||||
logLevel: localStorage.getItem('log_level'),
|
||||
rendererSettings: getRendererSettingsForExport(),
|
||||
server: currentServer
|
||||
? {
|
||||
isAdmin: currentServer.isAdmin,
|
||||
isLocalUrl: isLocalUrl(currentServer.url),
|
||||
musicFolderId: currentServer.musicFolderId,
|
||||
name: currentServer.name,
|
||||
preferInstantMix: currentServer.preferInstantMix,
|
||||
preferRemoteUrl: currentServer.preferRemoteUrl,
|
||||
...(currentServer.remoteUrl
|
||||
? {
|
||||
isLocalRemoteUrl: isLocalUrl(currentServer.remoteUrl),
|
||||
remoteUrl: '[Redacted]',
|
||||
}
|
||||
: {}),
|
||||
type: currentServer.type,
|
||||
url: '[Redacted]',
|
||||
version: currentServer.version,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
||||
if (result.canceled) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error({
|
||||
message: (error as Error).message,
|
||||
});
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loggerOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{
|
||||
label: t('setting.logLevel', {
|
||||
context: 'optionDebug',
|
||||
}),
|
||||
value: 'debug',
|
||||
},
|
||||
{
|
||||
label: t('setting.logLevel', {
|
||||
context: 'optionInfo',
|
||||
}),
|
||||
value: 'info',
|
||||
},
|
||||
{
|
||||
label: t('setting.logLevel', {
|
||||
context: 'optionWarn',
|
||||
}),
|
||||
value: 'warn',
|
||||
},
|
||||
{
|
||||
label: t('setting.logLevel', {
|
||||
context: 'optionError',
|
||||
}),
|
||||
value: 'error',
|
||||
},
|
||||
]}
|
||||
defaultValue={getCurrentLogLevel()}
|
||||
onChange={handleLogLevelChange}
|
||||
/>
|
||||
<Stack>
|
||||
<Select
|
||||
data={[
|
||||
{
|
||||
label: t('setting.logLevel', {
|
||||
context: 'optionInfo',
|
||||
}),
|
||||
value: 'info',
|
||||
},
|
||||
{
|
||||
label: t('setting.logLevel', {
|
||||
context: 'optionDebug',
|
||||
}),
|
||||
value: 'debug',
|
||||
},
|
||||
]}
|
||||
defaultValue={getCurrentLogLevel()}
|
||||
onChange={handleLogLevelChange}
|
||||
renderOption={LogLevelSelectOption}
|
||||
width={240}
|
||||
/>
|
||||
{utils && (
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
<Button
|
||||
loading={isExporting}
|
||||
onClick={handleExportDiagnostics}
|
||||
size="compact-sm"
|
||||
variant="subtle"
|
||||
>
|
||||
{t('setting.exportDiagnostics')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleOpenLogsFolder}
|
||||
size="compact-sm"
|
||||
variant="subtle"
|
||||
>
|
||||
{t('common.openFolder', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
),
|
||||
description: t('setting.logLevel', {
|
||||
context: 'description',
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useCurrentServer, usePlaybackType, usePlayerStatus } from '/@/renderer/store';
|
||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
|
||||
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
|
||||
@@ -25,14 +27,13 @@ const getAudioDevices = async () => {
|
||||
|
||||
const getMpvAudioDevices = async () => {
|
||||
if (!mpvPlayer) {
|
||||
console.log('mpvPlayer not found');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return await mpvPlayer.getAudioDevices();
|
||||
} catch (error) {
|
||||
console.error('Failed to get MPV audio devices:', error);
|
||||
logger.error('Failed to get MPV audio devices:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,7 +162,6 @@ export const PlaylistRowButton = memo(
|
||||
};
|
||||
},
|
||||
onDrag: () => {
|
||||
console.log('started drag');
|
||||
return;
|
||||
},
|
||||
onDragLeave: () => {
|
||||
|
||||
@@ -9,8 +9,7 @@ import { api } from '/@/renderer/api';
|
||||
import { controller } from '/@/renderer/api/controller';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { getServerById, useAuthStoreActions, useCurrentServerId } from '/@/renderer/store';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { AuthState } from '/@/shared/types/types';
|
||||
|
||||
@@ -67,14 +66,11 @@ export const useServerAuthenticated = () => {
|
||||
}
|
||||
|
||||
// First, try getUserInfo to check if current credentials are still valid
|
||||
logFn.info(logMsg[LogCategory.SYSTEM].authenticatingServer, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
method: 'getUserInfo',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
},
|
||||
logger.info('Authenticating server', {
|
||||
method: 'getUserInfo',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -122,27 +118,21 @@ export const useServerAuthenticated = () => {
|
||||
}
|
||||
} catch (serverInfoError) {
|
||||
// Log but don't fail authentication if server info fetch fails
|
||||
logFn.warn(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
action: 'server_info_fetch_failed',
|
||||
error: (serverInfoError as Error).message,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
},
|
||||
logger.warn('Server info fetch failed after auth', {
|
||||
action: 'server_info_fetch_failed',
|
||||
error: (serverInfoError as Error).message,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
});
|
||||
}
|
||||
|
||||
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
isAdmin: userInfo.isAdmin,
|
||||
method: 'getUserInfo',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
userId: userInfo.id,
|
||||
},
|
||||
logger.info('Server authentication successful', {
|
||||
isAdmin: userInfo.isAdmin,
|
||||
method: 'getUserInfo',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
userId: userInfo.id,
|
||||
});
|
||||
|
||||
const elapsedTime = Date.now() - authStartTime;
|
||||
@@ -167,16 +157,13 @@ export const useServerAuthenticated = () => {
|
||||
const password = await localSettings.passwordGet(serverWithAuth.id);
|
||||
|
||||
if (password) {
|
||||
logFn.info(logMsg[LogCategory.SYSTEM].authenticatingServer, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
method: 'authenticate',
|
||||
reason: 'getUserInfo failed with forbidden error',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
url: serverWithAuth.url,
|
||||
},
|
||||
logger.info('Authenticating server', {
|
||||
method: 'authenticate',
|
||||
reason: 'getUserInfo failed with forbidden error',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
url: serverWithAuth.url,
|
||||
});
|
||||
|
||||
// Authenticate using the API controller
|
||||
@@ -232,28 +219,22 @@ export const useServerAuthenticated = () => {
|
||||
}
|
||||
} catch (serverInfoError) {
|
||||
// Log but don't fail authentication if server info fetch fails
|
||||
logFn.warn(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
action: 'server_info_fetch_failed',
|
||||
error: (serverInfoError as Error).message,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
},
|
||||
logger.warn('Server info fetch failed after auth', {
|
||||
action: 'server_info_fetch_failed',
|
||||
error: (serverInfoError as Error).message,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
});
|
||||
}
|
||||
|
||||
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
isAdmin: authData.isAdmin,
|
||||
method: 'authenticate',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
userId: authData.userId,
|
||||
username: authData.username,
|
||||
},
|
||||
logger.info('Server authentication successful', {
|
||||
isAdmin: authData.isAdmin,
|
||||
method: 'authenticate',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
userId: authData.userId,
|
||||
username: authData.username,
|
||||
});
|
||||
|
||||
// Ensure minimum delay before completing authentication
|
||||
@@ -280,18 +261,15 @@ export const useServerAuthenticated = () => {
|
||||
if (isNetwork && retryAttempt < MAX_NETWORK_RETRIES) {
|
||||
const nextRetry = retryAttempt + 1;
|
||||
|
||||
logFn.warn(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
action: 'network_error_retry',
|
||||
attempt: nextRetry,
|
||||
error: errorMessage,
|
||||
maxRetries: MAX_NETWORK_RETRIES,
|
||||
retryDelayMs: NETWORK_RETRY_DELAY_MS,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
},
|
||||
logger.warn('Server authentication failed', {
|
||||
action: 'network_error_retry',
|
||||
attempt: nextRetry,
|
||||
error: errorMessage,
|
||||
maxRetries: MAX_NETWORK_RETRIES,
|
||||
retryDelayMs: NETWORK_RETRY_DELAY_MS,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
});
|
||||
|
||||
// Wait before retrying
|
||||
@@ -304,16 +282,13 @@ export const useServerAuthenticated = () => {
|
||||
|
||||
// If network error and retries exhausted, redirect to no-network page
|
||||
if (isNetwork && retryAttempt >= MAX_NETWORK_RETRIES) {
|
||||
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
action: 'network_error_max_retries_exceeded',
|
||||
attempts: retryAttempt + 1,
|
||||
error: errorMessage,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
},
|
||||
logger.error('Server authentication failed', {
|
||||
action: 'network_error_max_retries_exceeded',
|
||||
attempts: retryAttempt + 1,
|
||||
error: errorMessage,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
});
|
||||
|
||||
// Don't clear credentials on network failure - preserve them for when network returns
|
||||
@@ -323,14 +298,11 @@ export const useServerAuthenticated = () => {
|
||||
}
|
||||
|
||||
// For non-network errors, handle normally
|
||||
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
error: errorMessage,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
},
|
||||
logger.error('Server authentication failed', {
|
||||
error: errorMessage,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
});
|
||||
|
||||
// Clear server credentials and saved password on failure
|
||||
@@ -360,11 +332,8 @@ export const useServerAuthenticated = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!serverId) {
|
||||
logFn.debug(logMsg[LogCategory.SYSTEM].serverAuthenticationInvalid, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
reason: 'No server selected',
|
||||
},
|
||||
logger.info('Server authentication invalid', {
|
||||
reason: 'No server selected',
|
||||
});
|
||||
setReady(AuthState.INVALID);
|
||||
return;
|
||||
@@ -376,12 +345,9 @@ export const useServerAuthenticated = () => {
|
||||
retryCountRef.current = 0; // Reset retry count when server changes
|
||||
|
||||
if (!serverWithAuth) {
|
||||
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationError, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
reason: 'Server not found in store',
|
||||
serverId,
|
||||
},
|
||||
logger.error('Server authentication error', {
|
||||
reason: 'Server not found in store',
|
||||
serverId,
|
||||
});
|
||||
setReady(AuthState.INVALID);
|
||||
return;
|
||||
|
||||
@@ -4,9 +4,7 @@ import { useEffect, useRef } from 'react';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { openRestartRequiredToast } from '/@/renderer/features/settings/restart-toast';
|
||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||
import { logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
// Synchronizes settings from the renderer store to the main process electron store
|
||||
// on app initialization. If there are differences, it updates the main store and shows
|
||||
// a restart required toast.
|
||||
@@ -125,13 +123,14 @@ export const useSyncSettingsToMain = () => {
|
||||
JSON.stringify(rendererValueNormalized)
|
||||
) {
|
||||
hasDifferences = true;
|
||||
logFn.warn(logMsg.system.settingsSynchronized, {
|
||||
meta: {
|
||||
logger.warn(
|
||||
'Differences found between renderer and main process settings',
|
||||
{
|
||||
mainStoreKey: mapping.mainStoreKey,
|
||||
mainValue: mainValueNormalized,
|
||||
rendererValue: rendererValueNormalized,
|
||||
},
|
||||
});
|
||||
);
|
||||
localSettings.set(mapping.mainStoreKey, rendererValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { LogCategory } from '/@/renderer/utils/logger';
|
||||
|
||||
export const logMsg = {
|
||||
[LogCategory.ANALYTICS]: {
|
||||
appTracked: 'Analytics sent',
|
||||
pageViewTracked: 'Page view tracked',
|
||||
},
|
||||
[LogCategory.API]: {},
|
||||
[LogCategory.EXTERNAL]: {
|
||||
discordRpcActivityCleared: 'Activity was cleared for Discord RPC',
|
||||
discordRpcInitialized: 'Discord RPC was initialized',
|
||||
discordRpcQuit: 'Discord RPC was quit',
|
||||
discordRpcSetActivity: 'Activity was set for Discord RPC',
|
||||
discordRpcTrackChanged: 'Track was changed for Discord RPC',
|
||||
discordRpcUpdateSkipped: 'Activity was not updated for Discord RPC',
|
||||
},
|
||||
[LogCategory.OTHER]: {
|
||||
error: 'An error occurred',
|
||||
warning: 'A warning occurred',
|
||||
},
|
||||
[LogCategory.PLAYER]: {
|
||||
addToQueueByData: 'Added to queue by data',
|
||||
addToQueueByFetch: 'Added to queue by fetch',
|
||||
addToQueueByListQuery: 'Added to queue by list query',
|
||||
addToQueueByType: 'Added to queue by type',
|
||||
autoPlayFailed: 'Auto play failed',
|
||||
autoPlayTriggered: 'Auto play triggered',
|
||||
cancelledFetch: 'Cancelled fetch',
|
||||
clearQueue: 'Cleared queue',
|
||||
clearSelected: 'Cleared selected',
|
||||
decreaseVolume: 'Decreased volume',
|
||||
increaseVolume: 'Increased volume',
|
||||
mediaNext: 'Media next',
|
||||
mediaPause: 'Media pause',
|
||||
mediaPlay: 'Media play',
|
||||
mediaPlayByIndex: 'Media play by index',
|
||||
mediaPrevious: 'Media previous',
|
||||
mediaSeekToTimestamp: 'Media seek to timestamp',
|
||||
mediaSkipBackward: 'Media skip backward',
|
||||
mediaSkipForward: 'Media skip forward',
|
||||
mediaStop: 'Media stop',
|
||||
mediaToggleMute: 'Media toggle mute',
|
||||
mediaTogglePlayPause: 'Media toggle play pause',
|
||||
moveSelectedTo: 'Moved selected to',
|
||||
moveSelectedToBottom: 'Moved selected to bottom',
|
||||
moveSelectedToNext: 'Moved selected to next',
|
||||
moveSelectedToTop: 'Moved selected to top',
|
||||
playbackError: 'An error occurred during playback',
|
||||
playerFiltersApplied: 'Player filters applied',
|
||||
setFavorite: 'Set favorite',
|
||||
setQueue: 'Set queue',
|
||||
setRating: 'Set rating',
|
||||
setRepeat: 'Set repeat',
|
||||
setShuffle: 'Set shuffle',
|
||||
setSpeed: 'Set speed',
|
||||
setVolume: 'Set volume',
|
||||
shuffle: 'Shuffle',
|
||||
shuffleAll: 'Shuffle all',
|
||||
shuffleSelected: 'Shuffle selected',
|
||||
toggleRepeat: 'Toggle repeat',
|
||||
toggleShuffle: 'Toggle shuffle',
|
||||
},
|
||||
[LogCategory.REMOTE]: {
|
||||
cannotSendEvent: 'Cannot send event - socket not available',
|
||||
closingExistingSocket: 'Closing existing socket',
|
||||
creatingWebSocket: 'Creating new WebSocket',
|
||||
credentialsFetched: 'Credentials fetched',
|
||||
failedToEnableRemote: 'Failed to enable remote',
|
||||
failedToGetCredentials: 'Failed to get credentials',
|
||||
favoriteEventReceived: 'Favorite event received',
|
||||
fetchingCredentials: 'Fetching credentials',
|
||||
initializingRemoteSettings: 'Initializing remote settings',
|
||||
playbackEventReceived: 'Playback event received',
|
||||
positionEventReceived: 'Position event received',
|
||||
proxyEventReceived: 'Proxy event received (image update)',
|
||||
ratingEventReceived: 'Rating event received',
|
||||
reconnectInitiated: 'Reconnect initiated',
|
||||
reloadingPage: 'Reloading page due to close code',
|
||||
repeatEventReceived: 'Repeat event received',
|
||||
requestFavoriteReceived: 'Request favorite received',
|
||||
requestPositionReceived: 'Request position received',
|
||||
requestRatingReceived: 'Request rating received',
|
||||
requestSeekReceived: 'Request seek received',
|
||||
requestVolumeReceived: 'Request volume received',
|
||||
sendingAuthentication: 'Sending authentication',
|
||||
sendingEventToServer: 'Sending event to server',
|
||||
sendingInitialSong: 'Sending initial song',
|
||||
serverIsDown: 'Server is down',
|
||||
shuffleEventReceived: 'Shuffle event received',
|
||||
socketClosedUnexpectedly: 'Socket closed unexpectedly',
|
||||
songEventReceived: 'Song event received',
|
||||
stateEventReceived: 'State event received (full state update)',
|
||||
updateFavoriteSent: 'Update favorite sent',
|
||||
updatePlaybackSent: 'Update playback sent',
|
||||
updatePositionSent: 'Update position sent',
|
||||
updateRatingSent: 'Update rating sent',
|
||||
updateRepeatSent: 'Update repeat sent',
|
||||
updateShuffleSent: 'Update shuffle sent',
|
||||
updateSongSent: 'Update song sent',
|
||||
updateVolumeSent: 'Update volume sent',
|
||||
volumeEventReceived: 'Volume event received',
|
||||
webSocketClosed: 'WebSocket closed',
|
||||
webSocketErrorEvent: 'WebSocket error event',
|
||||
webSocketMessageReceived: 'WebSocket message received',
|
||||
webSocketOpened: 'WebSocket opened',
|
||||
},
|
||||
[LogCategory.SCROBBLE]: {
|
||||
scrobbledPause: 'Scrobbled a pause event',
|
||||
scrobbledStart: 'Scrobbled a start event',
|
||||
scrobbledStop: 'Scrobbled a stop event',
|
||||
scrobbledSubmission: 'Scrobbled a submission event',
|
||||
scrobbledTimeupdate: 'Scrobbled a timeupdate event',
|
||||
scrobbledUnpause: 'Scrobbled an unpause event',
|
||||
},
|
||||
[LogCategory.SYSTEM]: {
|
||||
authenticatingServer: 'Authenticating server',
|
||||
serverAuthenticationAborted: 'Server authentication aborted',
|
||||
serverAuthenticationError: 'Server authentication error',
|
||||
serverAuthenticationFailed: 'Server authentication failed',
|
||||
serverAuthenticationInvalid: 'Server authentication invalid',
|
||||
serverAuthenticationSuccess: 'Server authentication successful',
|
||||
settingsSynchronized: 'Differences found between renderer and main process settings',
|
||||
},
|
||||
};
|
||||
+111
-66
@@ -1,27 +1,21 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { LogLevel, LogSeverity } from '/@/shared/logger/types';
|
||||
|
||||
export enum LogCategory {
|
||||
ANALYTICS = 'analytics',
|
||||
API = 'api',
|
||||
EXTERNAL = 'external',
|
||||
GENERAL = 'general',
|
||||
OTHER = 'other',
|
||||
PLAYER = 'player',
|
||||
REMOTE = 'remote',
|
||||
SCROBBLE = 'scrobble',
|
||||
SYSTEM = 'system',
|
||||
}
|
||||
export type { LogLevel, LogSeverity };
|
||||
|
||||
export type LogLevel = 'debug' | 'error' | 'info' | 'warn';
|
||||
type ElectronLogApi = {
|
||||
debug: (...params: any[]) => void;
|
||||
error: (...params: any[]) => void;
|
||||
info: (...params: any[]) => void;
|
||||
sendToMain?: (message: {
|
||||
data: any[];
|
||||
level: LogSeverity;
|
||||
variables?: { processType: string };
|
||||
}) => void;
|
||||
warn: (...params: any[]) => void;
|
||||
};
|
||||
|
||||
interface LogFn {
|
||||
(
|
||||
message?: string,
|
||||
options?: {
|
||||
category?: string;
|
||||
meta?: any;
|
||||
},
|
||||
): void;
|
||||
(message?: string, meta?: any): void;
|
||||
}
|
||||
|
||||
interface Logger {
|
||||
@@ -32,18 +26,89 @@ interface Logger {
|
||||
warn: LogFn;
|
||||
}
|
||||
|
||||
const DEFAULT_LOG_LEVEL = process.env.NODE_ENV === 'production' ? 'info' : 'debug';
|
||||
const DEFAULT_LOG_LEVEL: LogLevel = process.env.NODE_ENV === 'production' ? 'info' : 'debug';
|
||||
const PROCESS_LABEL = '[renderer]';
|
||||
const PROCESS_WIDTH = 10;
|
||||
const LEVEL_WIDTH = 5;
|
||||
const RESET = '\x1B[0m';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const NO_OP: LogFn = (_message?: string, ..._optionalParams: any[]) => {};
|
||||
|
||||
const colors = {
|
||||
const levelColors: Record<LogSeverity, string> = {
|
||||
debug: '\x1B[38;2;100;149;237m', // #6495ED
|
||||
error: '\x1B[38;2;255;100;100m', // #ff6464
|
||||
info: '\x1B[38;2;76;175;80m', // #4caf50
|
||||
warn: '\x1B[38;2;225;125;50m', // #e17d32
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const NO_OP: LogFn = (_message?: string, ..._optionalParams: any[]) => {};
|
||||
|
||||
const getElectronLog = (): ElectronLogApi | null => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const electronLog = (window as Window & { __electronLog?: ElectronLogApi }).__electronLog;
|
||||
return electronLog ?? null;
|
||||
};
|
||||
|
||||
const formatLogLine = (level: LogSeverity, message: string, count = 1): string => {
|
||||
const countStr = count > 1 ? ` (x${count})` : '';
|
||||
const levelLabel = `${levelColors[level]}${level.toUpperCase().padEnd(LEVEL_WIDTH, ' ')}${RESET}`;
|
||||
const processLabel = PROCESS_LABEL.padEnd(PROCESS_WIDTH, ' ');
|
||||
return `${new Date().toISOString()} ${levelLabel} ${processLabel} ${message}${countStr}`;
|
||||
};
|
||||
|
||||
const forwardToElectronLog = (level: LogSeverity, message: string, meta?: any, count = 1) => {
|
||||
const electronLog = getElectronLog();
|
||||
if (!electronLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const countStr = count > 1 ? ` (x${count})` : '';
|
||||
const forwardMessage = `${message}${countStr}`;
|
||||
const data = meta !== undefined ? [forwardMessage, meta] : [forwardMessage];
|
||||
|
||||
if (typeof electronLog.sendToMain === 'function') {
|
||||
electronLog.sendToMain({
|
||||
data,
|
||||
level,
|
||||
variables: { processType: 'renderer' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (meta !== undefined) {
|
||||
electronLog[level](forwardMessage, meta);
|
||||
} else {
|
||||
electronLog[level](forwardMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const syncLogLevelToMain = (level: LogLevel) => {
|
||||
if (typeof window === 'undefined' || !window.api?.ipc) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.api.ipc.send('logger-set-level', level);
|
||||
};
|
||||
|
||||
export const normalizeLogLevel = (value: null | string | undefined): LogLevel => {
|
||||
if (value === 'debug' || value === 'info') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Legacy warn/error/trace thresholds map to nearby levels.
|
||||
if (value === 'warn' || value === 'error') {
|
||||
return 'info';
|
||||
}
|
||||
|
||||
if (value === 'trace') {
|
||||
return 'debug';
|
||||
}
|
||||
|
||||
return DEFAULT_LOG_LEVEL;
|
||||
};
|
||||
|
||||
// Debounce configuration
|
||||
const DEBOUNCE_INTERVAL = 200; // milliseconds
|
||||
const DEBOUNCE_MAP = new Map<string, { count: number; lastLog: number }>();
|
||||
@@ -53,22 +118,18 @@ setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of DEBOUNCE_MAP.entries()) {
|
||||
if (now - value.lastLog >= DEBOUNCE_INTERVAL) {
|
||||
const [level, message, category, meta] = JSON.parse(key);
|
||||
const timestampStr = `${dayjs().format('HH:mm:ss')}`;
|
||||
const levelStr = `${colors[level as keyof typeof colors]}[${String(level).toUpperCase().padEnd(5, ' ')}]\x1B[0m`;
|
||||
const countStr = value.count > 1 ? ` (x${value.count})` : '';
|
||||
const categoryStr = category
|
||||
? String(`[${category.padEnd(9, ' ')}]`).toUpperCase()
|
||||
: '';
|
||||
const [level, message, meta] = JSON.parse(key) as [LogSeverity, string, any];
|
||||
const messageStr = message ? String(message) : '';
|
||||
const logStr = `[${timestampStr}] ${levelStr} ${categoryStr} ${messageStr}${countStr}`;
|
||||
const logStr = formatLogLine(level, messageStr, value.count);
|
||||
|
||||
if (meta) {
|
||||
if (meta !== undefined && meta !== null) {
|
||||
console.log(logStr, meta);
|
||||
} else {
|
||||
console.log(logStr);
|
||||
}
|
||||
|
||||
forwardToElectronLog(level, messageStr, meta, value.count);
|
||||
|
||||
DEBOUNCE_MAP.delete(key);
|
||||
}
|
||||
}
|
||||
@@ -82,19 +143,24 @@ class ConsoleLogger implements Logger {
|
||||
warn: LogFn = NO_OP;
|
||||
|
||||
constructor() {
|
||||
const level = (localStorage.getItem('log_level') || DEFAULT_LOG_LEVEL) as LogLevel;
|
||||
const level = normalizeLogLevel(localStorage.getItem('log_level'));
|
||||
if (localStorage.getItem('log_level') !== level) {
|
||||
localStorage.setItem('log_level', level);
|
||||
}
|
||||
|
||||
this.initializeLoggers(level);
|
||||
syncLogLevelToMain(level);
|
||||
|
||||
this.updateLogLevel = (newLevel: LogLevel) => {
|
||||
this.initializeLoggers(newLevel);
|
||||
syncLogLevelToMain(newLevel);
|
||||
};
|
||||
}
|
||||
|
||||
private initializeLoggers(level: LogLevel) {
|
||||
// Create timestamp wrapper function with colors and debouncing
|
||||
const withTimestamp = (logLevel: string): LogFn => {
|
||||
return (message?: any, options?: { category?: string; meta?: any }) => {
|
||||
const { category, meta } = options || {};
|
||||
const key = JSON.stringify([logLevel, message, category, meta]);
|
||||
const withDebounce = (logLevel: LogSeverity): LogFn => {
|
||||
return (message?: any, meta?: any) => {
|
||||
const key = JSON.stringify([logLevel, message, meta]);
|
||||
const now = Date.now();
|
||||
const existing = DEBOUNCE_MAP.get(key);
|
||||
|
||||
@@ -107,32 +173,11 @@ class ConsoleLogger implements Logger {
|
||||
};
|
||||
};
|
||||
|
||||
this.error = withTimestamp('error');
|
||||
|
||||
if (level === 'error') {
|
||||
this.warn = NO_OP;
|
||||
this.info = NO_OP;
|
||||
this.debug = NO_OP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.warn = withTimestamp('warn');
|
||||
|
||||
if (level === 'warn') {
|
||||
this.info = NO_OP;
|
||||
this.debug = NO_OP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.info = withTimestamp('info');
|
||||
|
||||
if (level === 'info') {
|
||||
this.debug = NO_OP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.debug = withTimestamp('debug');
|
||||
this.error = withDebounce('error');
|
||||
this.warn = withDebounce('warn');
|
||||
this.info = withDebounce('info');
|
||||
this.debug = level === 'debug' ? withDebounce('debug') : NO_OP;
|
||||
}
|
||||
}
|
||||
|
||||
export const logFn = new ConsoleLogger();
|
||||
export const logger = new ConsoleLogger();
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// Minimum verbosity configured in settings (and applied to main transports).
|
||||
export type LogLevel = 'debug' | 'info';
|
||||
|
||||
// Severity of an individual log message.
|
||||
export type LogSeverity = 'debug' | 'error' | 'info' | 'warn';
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Returns true when the URL host is loopback or a private/LAN address.
|
||||
*/
|
||||
export const isLocalUrl = (value: string): boolean => {
|
||||
try {
|
||||
const { hostname } = new URL(value);
|
||||
|
||||
if (
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname === '::1' ||
|
||||
hostname === '[::1]' ||
|
||||
hostname === '0.0.0.0' ||
|
||||
hostname.endsWith('.local')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv4 private / link-local ranges
|
||||
const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (ipv4) {
|
||||
const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
|
||||
if (a === 10) return true;
|
||||
if (a === 127) return true;
|
||||
if (a === 192 && b === 168) return true;
|
||||
if (a === 169 && b === 254) return true;
|
||||
if (a === 172 && b >= 16 && b <= 31) return true;
|
||||
}
|
||||
|
||||
// IPv6 unique local / link-local (fc00::/7, fe80::/10)
|
||||
const normalized = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
||||
if (normalized.startsWith('fc') || normalized.startsWith('fd')) {
|
||||
return true;
|
||||
}
|
||||
if (/^fe[89ab]/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
const SENSITIVE_KEY_PATTERN = /password|token|credential|authorization|secret|cookie/i;
|
||||
const MAX_DEPTH = 20;
|
||||
|
||||
/**
|
||||
* Deep-clone a value for diagnostics exports, redacting sensitive keys.
|
||||
* Does not truncate arrays/strings — intended for config dumps, not log lines.
|
||||
*/
|
||||
export const sanitizeForDiagnostics = (value: unknown, depth = 0): unknown => {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (depth >= MAX_DEPTH) {
|
||||
return '[Truncated]';
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => sanitizeForDiagnostics(item, depth + 1));
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
|
||||
if (SENSITIVE_KEY_PATTERN.test(key)) {
|
||||
result[key] = '[Redacted]';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Main-process electron-store password map: { [serverId]: encryptedHex }
|
||||
if (
|
||||
key === 'server' &&
|
||||
nested &&
|
||||
typeof nested === 'object' &&
|
||||
!Array.isArray(nested) &&
|
||||
Object.values(nested as Record<string, unknown>).every((v) => typeof v === 'string')
|
||||
) {
|
||||
result[key] = Object.fromEntries(
|
||||
Object.keys(nested as Record<string, unknown>).map((id) => [id, '[Redacted]']),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
result[key] = sanitizeForDiagnostics(nested, depth + 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -25,6 +25,9 @@
|
||||
"/@/shared/*": [
|
||||
"src/shared/*"
|
||||
],
|
||||
"/@/main/*": [
|
||||
"src/main/*"
|
||||
],
|
||||
"/@/i18n/*": [
|
||||
"src/i18n/*"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user