Files
feishin/src/renderer/features/analytics/hooks/use-app-tracker.ts
T
riccardo 16ac536f93 feat(lyrics): simpmusic lyrics provider (#1820)
* feat(lyrics): simpmusic lyrics provider
2026-03-11 01:04:55 -07:00

363 lines
13 KiB
TypeScript

import { mutationOptions, useMutation } from '@tanstack/react-query';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import isElectron from 'is-electron';
import { useEffect, useRef } from 'react';
dayjs.extend(utc);
import packageJson from '../../../../../package.json';
import { isAnalyticsDisabled } from '/@/renderer/features/analytics/hooks/use-analytics-disabled';
import {
PlayerbarSliderType,
SideQueueType,
useAuthStore,
usePlayerStore,
useSettingsStore,
} from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { LyricSource, ServerType } from '/@/shared/types/domain-types';
import { FontType, Platform, PlayerStyle, PlayerType } from '/@/shared/types/types';
const utils = isElectron() ? window.api.utils : null;
let appTrackerInFlight = false;
let appTrackerLastSentDate: null | string = null;
const getVersion = (): AppTrackerProperties['_version'] => {
return packageJson.version;
};
const getPlatform = (): AppTrackerProperties['_platform'] => {
if (!isElectron()) {
return Platform.WEB;
}
if (utils?.isWindows()) {
return Platform.WINDOWS;
}
if (utils?.isMacOS()) {
return Platform.MACOS;
}
if (utils?.isLinux()) {
return Platform.LINUX;
}
return 'unknown';
};
type AppTrackerProperties = PlayerProperties &
SettingsProperties & {
_platform: 'unknown' | Platform;
_server: 'unknown' | ServerType;
_version: string;
};
type PlayerProperties = {
'player.mediaSession': boolean;
'player.style': PlayerStyle;
'player.transcoding': boolean;
'player.type': PlayerType;
'player.webAudio': boolean;
};
type SettingsProperties = {
'settings.albumBackground': boolean;
'settings.artistBackground': boolean;
'settings.autoDJ': boolean;
'settings.autoDJItemCount': number;
'settings.autoDJTiming': number;
'settings.customCss': boolean;
'settings.disableAutoUpdate': boolean;
'settings.discord': boolean;
'settings.exitToTray': boolean;
'settings.followSystemTheme': boolean;
'settings.fontType': FontType;
'settings.globalHotkeys': boolean;
'settings.homeFeature': boolean;
'settings.language': string;
'settings.lyrics.enableAutoTranslation': boolean;
'settings.lyrics.enableNeteaseTranslation': boolean;
'settings.lyrics.fetch': boolean;
'settings.lyrics.sources.genius': boolean;
'settings.lyrics.sources.lrclib': boolean;
'settings.lyrics.sources.netease': boolean;
'settings.minimizeToTray': boolean;
'settings.nativeAspectRatio': boolean;
'settings.playerbarSliderType': PlayerbarSliderType;
'settings.preventSleepOnPlayback': boolean;
'settings.releaseChannel': string;
'settings.resume': boolean;
'settings.scrobble.enabled': boolean;
'settings.scrobble.notify': boolean;
'settings.showLyricsInSidebar': boolean;
'settings.showVisualizerInSidebar': boolean;
'settings.sideQueueType': SideQueueType;
'settings.skipButtons': boolean;
'settings.startMinimized': boolean;
'settings.theme': string;
'settings.themeDark': string;
'settings.themeLight': string;
'settings.tray': boolean;
'settings.useThemeAccentColor': boolean;
'settings.useThemePrimaryShade': boolean;
'settings.windowBarStyle': Platform;
'settings.zoomFactor': number;
};
const getPlayerProperties = (): Pick<
AppTrackerProperties,
| 'player.mediaSession'
| 'player.style'
| 'player.transcoding'
| 'player.type'
| 'player.webAudio'
> => {
const player = usePlayerStore.getState();
const playbackSettings = useSettingsStore.getState().playback;
return {
'player.mediaSession': ignoreWeb(playbackSettings.mediaSession),
'player.style': player.player.transitionType,
'player.transcoding': playbackSettings.transcode.enabled,
'player.type': ignoreWeb(playbackSettings.type),
'player.webAudio': ignoreWeb(playbackSettings.webAudio),
} as any;
};
function ignoreWeb<T>(value: T): T | undefined {
return isElectron() ? value : undefined;
}
const getSettingsProperties = (): SettingsProperties => {
const settings = useSettingsStore.getState();
return {
'settings.albumBackground': settings.general.albumBackground,
// 'settings.albumBackgroundBlur': settings.general.albumBackgroundBlur,
'settings.artistBackground': settings.general.artistBackground,
// 'settings.artistBackgroundBlur': settings.general.artistBackgroundBlur,
'settings.autoDJ': settings.autoDJ.enabled,
'settings.autoDJItemCount': settings.autoDJ.itemCount,
'settings.autoDJTiming': settings.autoDJ.timing,
'settings.customCss': settings.css.enabled,
'settings.disableAutoUpdate': ignoreWeb(settings.window.disableAutoUpdate),
'settings.discord': ignoreWeb(settings.discord.enabled),
'settings.exitToTray': ignoreWeb(settings.window.exitToTray),
'settings.followSystemTheme': settings.general.followSystemTheme,
'settings.fontType': settings.font.type,
'settings.globalHotkeys': settings.hotkeys.globalMediaHotkeys,
'settings.homeFeature': settings.general.homeFeature,
'settings.language': settings.general.language,
// 'settings.lastFM': settings.general.lastFM,
'settings.lyrics.enableAutoTranslation': ignoreWeb(settings.lyrics.enableAutoTranslation),
'settings.lyrics.enableNeteaseTranslation': ignoreWeb(
settings.lyrics.enableNeteaseTranslation,
),
'settings.lyrics.fetch': ignoreWeb(settings.lyrics.fetch),
'settings.lyrics.sources.genius': ignoreWeb(
settings.lyrics.sources.includes(LyricSource.GENIUS),
),
'settings.lyrics.sources.lrclib': ignoreWeb(
settings.lyrics.sources.includes(LyricSource.LRCLIB),
),
'settings.lyrics.sources.netease': ignoreWeb(
settings.lyrics.sources.includes(LyricSource.NETEASE),
),
'settings.lyrics.sources.simpmusic': ignoreWeb(
settings.lyrics.sources.includes(LyricSource.SIMPMUSIC),
),
'settings.minimizeToTray': ignoreWeb(settings.window.minimizeToTray),
// 'settings.musicBrainz': settings.general.musicBrainz,
'settings.nativeAspectRatio': settings.general.nativeAspectRatio,
'settings.playerbarSliderType': settings.general.playerbarSlider
.type as PlayerbarSliderType,
// 'settings.playerbarWaveformAlign': settings.general.playerbarSlider.barAlign as BarAlign,
// 'settings.playerbarWaveformBarWidth': settings.general.playerbarSlider.barWidth,
// 'settings.playerbarWaveformGap': settings.general.playerbarSlider.barGap,
// 'settings.playerbarWaveformRadius': settings.general.playerbarSlider.barRadius,
'settings.preventSleepOnPlayback': ignoreWeb(settings.window.preventSleepOnPlayback),
'settings.releaseChannel': ignoreWeb(settings.window.releaseChannel),
'settings.resume': settings.general.resume,
'settings.scrobble.enabled': settings.playback.scrobble.enabled,
'settings.scrobble.notify': ignoreWeb(settings.playback.scrobble.notify),
'settings.showLyricsInSidebar': settings.general.showLyricsInSidebar,
'settings.showVisualizerInSidebar': settings.general.showVisualizerInSidebar,
'settings.sideQueueType': settings.general.sideQueueType,
// 'settings.skipBackwardSeconds': settings.general.skipButtons.skipBackwardSeconds,
'settings.skipButtons': settings.general.skipButtons.enabled,
// 'settings.skipForwardSeconds': settings.general.skipButtons.skipForwardSeconds,
'settings.startMinimized': ignoreWeb(settings.window.startMinimized),
'settings.theme': settings.general.theme,
'settings.themeDark': settings.general.themeDark,
'settings.themeLight': settings.general.themeLight,
'settings.tray': ignoreWeb(settings.window.tray),
'settings.useThemeAccentColor': settings.general.useThemeAccentColor,
'settings.useThemePrimaryShade': settings.general.useThemePrimaryShade,
'settings.windowBarStyle': ignoreWeb(settings.window.windowBarStyle),
'settings.zoomFactor': ignoreWeb(settings.general.zoomFactor),
} as any;
};
const getServer = (): 'unknown' | ServerType => {
const auth = useAuthStore.getState();
const currentServer = auth.currentServer;
if (currentServer) {
return currentServer.type;
}
const serverList = auth.serverList;
const server = Object.values(serverList)[0];
if (server) {
return server.type;
}
return 'unknown';
};
export const useAppTracker = () => {
const { mutate: trackAppMutation } = useMutation(appTrackerMutation);
const { mutate: trackAppViewMutation } = useMutation(appViewMutation);
const hasRunOnMountRef = useRef(false);
useEffect(() => {
if (!window.umami || isAnalyticsDisabled()) {
return;
}
const waitForServer = async (): Promise<void> => {
if (useAuthStore.getState().currentServer) {
return;
}
const pollInterval = 1000 * 60;
while (!useAuthStore.getState().currentServer) {
await new Promise((resolve) => setTimeout(resolve, pollInterval));
}
};
const getProperties = () => {
const platform = getPlatform();
const version = getVersion();
const server = getServer();
const playerProperties = getPlayerProperties();
const settingsProperties = getSettingsProperties();
const properties: AppTrackerProperties = {
_platform: platform,
_server: server,
_version: version,
...playerProperties,
...settingsProperties,
};
return properties;
};
const checkAndTrack = () => {
// Prevent multiple simultaneous requests
if (appTrackerInFlight) {
return;
}
const lastSentDate = localStorage.getItem('analytics_app_tracker_timestamp');
const lastTrackedDate = appTrackerLastSentDate ?? lastSentDate;
const todayUTC = dayjs.utc().format('YYYY-MM-DD');
// Only send if it's a new day in UTC (ensures once per 24 hours)
if (lastTrackedDate !== todayUTC) {
appTrackerInFlight = true;
const properties = getProperties();
logFn.info(logMsg[LogCategory.ANALYTICS].appTracked, {
category: LogCategory.ANALYTICS,
meta: { properties, todayUTC },
});
trackAppViewMutation(undefined, {
onError: () => {},
});
trackAppMutation(properties, {
onError: () => {},
onSettled: () => {
appTrackerInFlight = false;
},
onSuccess: () => {
// Only update timestamp on success to ensure we only send once per 24 hours
const utcDate = dayjs.utc().format('YYYY-MM-DD');
appTrackerLastSentDate = utcDate;
localStorage.setItem('analytics_app_tracker_timestamp', utcDate);
logFn.debug(logMsg[LogCategory.ANALYTICS].appTracked, {
category: LogCategory.ANALYTICS,
meta: { properties },
});
},
});
}
};
// Check immediately on mount
if (!hasRunOnMountRef.current) {
waitForServer().then(() => {
checkAndTrack();
});
hasRunOnMountRef.current = true;
}
const interval = setInterval(checkAndTrack, 1000 * 60 * 60);
return () => clearInterval(interval);
}, [trackAppMutation, trackAppViewMutation]);
};
// Sends the app event to the analytics server which includes usage data
const appTrackerMutation = mutationOptions({
mutationFn: (properties: AppTrackerProperties) => {
try {
window.umami?.track((props) => ({
data: properties,
language: props.language,
name: 'app',
screen: props.screen,
website: props.website,
}));
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
},
mutationKey: ['analytics', 'settings-tracker'],
onSuccess: () => {},
retry: false,
throwOnError: false,
});
// Sends a view event to the analytics server which only includes language, screen, and website
// and triggers a page view event
const appViewMutation = mutationOptions({
mutationFn: () => {
try {
window.umami?.track((props) => ({
language: props.language,
screen: props.screen,
website: props.website,
}));
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
},
mutationKey: ['analytics', 'app-view'],
onSuccess: () => {},
retry: false,
throwOnError: false,
});