mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-21 18:06:30 +02:00
Add support for OpenSubsonic enhanced lyrics (#2208)
This commit is contained in:
@@ -956,7 +956,10 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
return res.body.Lyrics.map((lyric) => lyric.Text).join('\n');
|
||||
}
|
||||
|
||||
return res.body.Lyrics.map((lyric) => [lyric.Start! / 1e4, lyric.Text]);
|
||||
return res.body.Lyrics.map((lyric) => ({
|
||||
startMs: lyric.Start! / 1e4,
|
||||
text: lyric.Text,
|
||||
}));
|
||||
},
|
||||
getMusicFolderList: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
@@ -8,6 +8,7 @@ import md5 from 'md5';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { contract, ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { mapStructuredLyric } from '/@/renderer/api/subsonic/subsonic-structured-lyrics';
|
||||
import {
|
||||
getDefaultTranscodingProfiles,
|
||||
getDirectPlayProfiles,
|
||||
@@ -21,7 +22,13 @@ import {
|
||||
ssType,
|
||||
SubsonicExtensions,
|
||||
} from '/@/shared/api/subsonic/subsonic-types';
|
||||
import { hasFeature, sortAlbumArtistList, sortAlbumList, sortSongList } from '/@/shared/api/utils';
|
||||
import {
|
||||
hasFeature,
|
||||
hasFeatureWithVersion,
|
||||
sortAlbumArtistList,
|
||||
sortAlbumList,
|
||||
sortSongList,
|
||||
} from '/@/shared/api/utils';
|
||||
import {
|
||||
AlbumListSort,
|
||||
GenreListSort,
|
||||
@@ -1381,7 +1388,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
if (subsonicFeatures[SubsonicExtensions.SONG_LYRICS]) {
|
||||
features.lyricsMultipleStructured = [1];
|
||||
features.lyricsMultipleStructured = subsonicFeatures[SubsonicExtensions.SONG_LYRICS];
|
||||
}
|
||||
|
||||
if (subsonicFeatures[SubsonicExtensions.FORM_POST]) {
|
||||
@@ -1961,9 +1968,16 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
},
|
||||
getStructuredLyrics: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
const server = apiClientProps.server;
|
||||
const supportsEnhancedLyrics = hasFeatureWithVersion(
|
||||
server,
|
||||
ServerFeature.LYRICS_MULTIPLE_STRUCTURED,
|
||||
2,
|
||||
);
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getStructuredLyrics({
|
||||
query: {
|
||||
enhanced: supportsEnhancedLyrics ? true : undefined,
|
||||
id: query.songId,
|
||||
},
|
||||
});
|
||||
@@ -1978,28 +1992,9 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return [];
|
||||
}
|
||||
|
||||
return lyrics.map((lyric) => {
|
||||
const baseLyric = {
|
||||
artist: lyric.displayArtist || '',
|
||||
lang: lyric.lang,
|
||||
name: lyric.displayTitle || '',
|
||||
remote: false,
|
||||
source: apiClientProps.server?.name || 'music server',
|
||||
};
|
||||
const source = apiClientProps.server?.name || 'music server';
|
||||
|
||||
if (lyric.synced) {
|
||||
return {
|
||||
...baseLyric,
|
||||
lyrics: lyric.line.map((line) => [line.start!, line.value]),
|
||||
synced: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...baseLyric,
|
||||
lyrics: lyric.line.map((line) => [line.value]).join('\n'),
|
||||
synced: false,
|
||||
};
|
||||
});
|
||||
return lyrics.map((lyric) => mapStructuredLyric(lyric, source));
|
||||
},
|
||||
getTopSongs: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { sliceUtf8Bytes } from '/@/renderer/utils/utf8-byte-slice';
|
||||
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
||||
import {
|
||||
LyricAgent,
|
||||
LyricsKind,
|
||||
StructuredLyric,
|
||||
SyncedCueLine,
|
||||
SyncedWordCue,
|
||||
SynchronizedLyricLine,
|
||||
} from '/@/shared/types/domain-types';
|
||||
|
||||
type ApiStructuredLyric = NonNullable<
|
||||
z.infer<typeof ssType._response.structuredLyrics>['lyricsList']
|
||||
>['structuredLyrics'] extends Array<infer T> | undefined
|
||||
? T
|
||||
: never;
|
||||
|
||||
const mapCueWords = (
|
||||
apiCueLine: NonNullable<ApiStructuredLyric['cueLine']>[number],
|
||||
): SyncedWordCue[] => {
|
||||
if (!apiCueLine.cue?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return apiCueLine.cue.map((cue) => ({
|
||||
endMs: cue.end,
|
||||
startMs: cue.start,
|
||||
text: cue.value || sliceUtf8Bytes(apiCueLine.value, cue.byteStart, cue.byteEnd),
|
||||
}));
|
||||
};
|
||||
|
||||
const mapCueLine = (
|
||||
apiCueLine: NonNullable<ApiStructuredLyric['cueLine']>[number],
|
||||
): SyncedCueLine => ({
|
||||
agentId: apiCueLine.agentId,
|
||||
endMs: apiCueLine.end,
|
||||
index: apiCueLine.index,
|
||||
startMs: apiCueLine.start,
|
||||
value: apiCueLine.value,
|
||||
words: mapCueWords(apiCueLine),
|
||||
});
|
||||
|
||||
const attachCueLines = (
|
||||
lines: SynchronizedLyricLine[],
|
||||
apiCueLines: NonNullable<ApiStructuredLyric['cueLine']>,
|
||||
): SynchronizedLyricLine[] => {
|
||||
const cueLinesByIndex = new Map<number, SyncedCueLine[]>();
|
||||
|
||||
for (const apiCueLine of apiCueLines) {
|
||||
const mappedCueLine = mapCueLine(apiCueLine);
|
||||
const existing = cueLinesByIndex.get(apiCueLine.index) ?? [];
|
||||
existing.push(mappedCueLine);
|
||||
cueLinesByIndex.set(apiCueLine.index, existing);
|
||||
}
|
||||
|
||||
return lines.map((line, index) => {
|
||||
const cueLines = cueLinesByIndex.get(index);
|
||||
|
||||
if (!cueLines?.length) {
|
||||
return line;
|
||||
}
|
||||
|
||||
return {
|
||||
...line,
|
||||
cueLines,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const mapAgents = (agents: ApiStructuredLyric['agents']): LyricAgent[] | undefined => {
|
||||
if (!agents?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return agents.map((agent) => ({
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
role: agent.role,
|
||||
}));
|
||||
};
|
||||
|
||||
export const mapStructuredLyric = (lyric: ApiStructuredLyric, source: string): StructuredLyric => {
|
||||
const baseLyric = {
|
||||
artist: lyric.displayArtist || '',
|
||||
lang: lyric.lang,
|
||||
name: lyric.displayTitle || '',
|
||||
offsetMs: lyric.offset ?? 0,
|
||||
remote: false,
|
||||
source,
|
||||
};
|
||||
|
||||
if (lyric.synced) {
|
||||
let lines: SynchronizedLyricLine[] = lyric.line.map((line) => ({
|
||||
startMs: line.start ?? 0,
|
||||
text: line.value,
|
||||
}));
|
||||
|
||||
if (lyric.cueLine?.length) {
|
||||
lines = attachCueLines(lines, lyric.cueLine);
|
||||
}
|
||||
|
||||
return {
|
||||
...baseLyric,
|
||||
agents: mapAgents(lyric.agents),
|
||||
kind: (lyric.kind ?? 'main') as LyricsKind,
|
||||
lyrics: lines,
|
||||
synced: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseLyric,
|
||||
lyrics: lyric.line.map((line) => line.value).join('\n'),
|
||||
synced: false,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user