mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-24 03:16:43 +02:00
Add support for OpenSubsonic enhanced lyrics (#2208)
This commit is contained in:
@@ -115,7 +115,10 @@ const AUTO_DJ_MODES = new Set(['albums', 'songs']);
|
||||
const AUTO_DJ_STRATEGIES = new Set(['library_random', 'similar']);
|
||||
|
||||
export type EnvSettingsOverrides = DeepPartial<
|
||||
Pick<SettingsState, 'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'playback'>
|
||||
Pick<
|
||||
SettingsState,
|
||||
'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'lyricsDisplay' | 'playback'
|
||||
>
|
||||
>;
|
||||
|
||||
type DeepPartial<T> = {
|
||||
@@ -403,6 +406,16 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
|
||||
{ key: 'FS_LYRICS_FETCH', path: ['lyrics', 'fetch'], type: 'bool' },
|
||||
{ key: 'FS_LYRICS_FOLLOW', path: ['lyrics', 'follow'], type: 'bool' },
|
||||
{ key: 'FS_LYRICS_DELAY_MS', path: ['lyrics', 'delayMs'], type: 'num' },
|
||||
{ key: 'FS_LYRICS_LINE_LEAD_TIME_MS', path: ['lyrics', 'lineLeadTimeMs'], type: 'num' },
|
||||
{
|
||||
key: 'FS_LYRICS_FOLLOW_SCROLL_ALIGNMENT',
|
||||
path: ['lyrics', 'followScrollAlignment'],
|
||||
transform: (s) => {
|
||||
const n = parseNum(s);
|
||||
return n !== undefined ? Math.min(50, Math.max(-50, Math.round(n))) : undefined;
|
||||
},
|
||||
type: 'num',
|
||||
},
|
||||
{ key: 'FS_LYRICS_PREFER_LOCAL', path: ['lyrics', 'preferLocalLyrics'], type: 'bool' },
|
||||
{ key: 'FS_LYRICS_SHOW_MATCH', path: ['lyrics', 'showMatch'], type: 'bool' },
|
||||
{ key: 'FS_LYRICS_SHOW_PROVIDER', path: ['lyrics', 'showProvider'], type: 'bool' },
|
||||
@@ -424,6 +437,24 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
|
||||
path: ['lyrics', 'alignment'],
|
||||
type: 'enum',
|
||||
},
|
||||
{
|
||||
key: 'FS_LYRICS_PADDING_LEFT',
|
||||
path: ['lyricsDisplay', 'default', 'paddingLeft'],
|
||||
transform: (s) => {
|
||||
const n = parseNum(s);
|
||||
return n !== undefined ? Math.min(20, Math.max(0, Math.round(n))) : undefined;
|
||||
},
|
||||
type: 'num',
|
||||
},
|
||||
{
|
||||
key: 'FS_LYRICS_PADDING_RIGHT',
|
||||
path: ['lyricsDisplay', 'default', 'paddingRight'],
|
||||
transform: (s) => {
|
||||
const n = parseNum(s);
|
||||
return n !== undefined ? Math.min(20, Math.max(0, Math.round(n))) : undefined;
|
||||
},
|
||||
type: 'num',
|
||||
},
|
||||
{
|
||||
enumSet: AUTO_DJ_STRATEGIES,
|
||||
key: 'FS_AUTO_DJ_ALBUM_STRATEGY',
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createWithEqualityFn } from 'zustand/traditional';
|
||||
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
const SCROBBLE_DEBUG_POLL_INTERVAL_MS = 1000;
|
||||
|
||||
export type ScrobbleDebugSnapshot = {
|
||||
eligibilityMet: boolean;
|
||||
lastListenSampleTimeSec: null | number;
|
||||
@@ -38,6 +41,26 @@ export const useScrobbleDebugStore = createWithEqualityFn<ScrobbleDebugStore>()(
|
||||
snapshot: initialSnapshot,
|
||||
}));
|
||||
|
||||
export const useScrobbleDebugSnapshot = () => {
|
||||
const [snapshot, setLocalSnapshot] = useState(() => useScrobbleDebugStore.getState().snapshot);
|
||||
|
||||
useEffect(() => {
|
||||
const syncSnapshot = () => {
|
||||
const nextSnapshot = useScrobbleDebugStore.getState().snapshot;
|
||||
setLocalSnapshot((prevSnapshot) =>
|
||||
prevSnapshot !== nextSnapshot ? nextSnapshot : prevSnapshot,
|
||||
);
|
||||
};
|
||||
|
||||
syncSnapshot();
|
||||
const interval = setInterval(syncSnapshot, SCROBBLE_DEBUG_POLL_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
export const publishScrobbleDebug = (partial: Partial<ScrobbleDebugSnapshot>) => {
|
||||
useScrobbleDebugStore.setState((state) => ({
|
||||
snapshot: { ...state.snapshot, ...partial },
|
||||
|
||||
@@ -571,6 +571,8 @@ const LyricsDisplaySettingsSchema = z.object({
|
||||
gap: z.number(),
|
||||
gapUnsync: z.number(),
|
||||
opacityNonActive: z.number(),
|
||||
paddingLeft: z.number(),
|
||||
paddingRight: z.number(),
|
||||
scaleNonActive: z.number(),
|
||||
});
|
||||
|
||||
@@ -583,6 +585,8 @@ const LyricsSettingsSchema = z.object({
|
||||
enableRomaji: z.boolean().optional(),
|
||||
fetch: z.boolean(),
|
||||
follow: z.boolean(),
|
||||
followScrollAlignment: z.number(),
|
||||
lineLeadTimeMs: z.number(),
|
||||
preferLocalLyrics: z.boolean(),
|
||||
showMatch: z.boolean(),
|
||||
showProvider: z.boolean(),
|
||||
@@ -1857,6 +1861,8 @@ const initialState: SettingsState = {
|
||||
enableRomaji: false,
|
||||
fetch: true,
|
||||
follow: true,
|
||||
followScrollAlignment: 0,
|
||||
lineLeadTimeMs: 800,
|
||||
preferLocalLyrics: true,
|
||||
showMatch: true,
|
||||
showProvider: true,
|
||||
@@ -1872,6 +1878,8 @@ const initialState: SettingsState = {
|
||||
gap: 24,
|
||||
gapUnsync: 24,
|
||||
opacityNonActive: 0.2,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
scaleNonActive: 0.95,
|
||||
},
|
||||
},
|
||||
@@ -2539,10 +2547,31 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
||||
);
|
||||
}
|
||||
|
||||
if (version < 30) {
|
||||
for (const [key, displaySettings] of Object.entries(state.lyricsDisplay)) {
|
||||
const legacySettings = displaySettings as typeof displaySettings & {
|
||||
paddingX?: number;
|
||||
};
|
||||
const legacyPaddingX = legacySettings.paddingX ?? 0;
|
||||
|
||||
state.lyricsDisplay[key] = {
|
||||
...displaySettings,
|
||||
paddingLeft: displaySettings.paddingLeft ?? legacyPaddingX,
|
||||
paddingRight: displaySettings.paddingRight ?? legacyPaddingX,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (version < 31) {
|
||||
if (state.lyrics.followScrollAlignment === undefined) {
|
||||
state.lyrics.followScrollAlignment = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return persistedState;
|
||||
},
|
||||
name: 'store_settings',
|
||||
version: 28,
|
||||
version: 31,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { del, get, set } from 'idb-keyval';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { createWithEqualityFn } from 'zustand/traditional';
|
||||
|
||||
const PLAYER_TIMESTAMP_POLL_INTERVAL_MS = 500;
|
||||
|
||||
interface TimestampState {
|
||||
setTimestamp: (timestamp: number) => void;
|
||||
timestamp: number;
|
||||
@@ -60,7 +63,25 @@ export const usePlayerProgress = () => {
|
||||
};
|
||||
|
||||
export const usePlayerTimestamp = () => {
|
||||
return useTimestampStoreBase((state) => state.timestamp);
|
||||
const [timestamp, setLocalTimestamp] = useState(
|
||||
() => useTimestampStoreBase.getState().timestamp,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const syncTimestamp = () => {
|
||||
const nextTimestamp = useTimestampStoreBase.getState().timestamp;
|
||||
setLocalTimestamp((prevTimestamp) =>
|
||||
prevTimestamp !== nextTimestamp ? nextTimestamp : prevTimestamp,
|
||||
);
|
||||
};
|
||||
|
||||
syncTimestamp();
|
||||
const interval = setInterval(syncTimestamp, PLAYER_TIMESTAMP_POLL_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return timestamp;
|
||||
};
|
||||
|
||||
export const setTimestamp = (timestamp: number) => {
|
||||
|
||||
Reference in New Issue
Block a user