import { IAppConfig } from "@/types/core/config";
import { ITrackPlayer } from "@/types/core/trackPlayer";
import { IInjectable } from "@/types/infra";
import LyricParser, { IParsedLrcItem } from "@/utils/lrcParser";
import { getMediaExtraProperty, patchMediaExtra } from "@/utils/mediaExtra";
import { isSameMediaItem } from "@/utils/mediaUtils";
import minDistance from "@/utils/minDistance";
import { atom, getDefaultStore, useAtomValue } from "jotai";
import { Plugin } from "./pluginManager";
import pathConst from "@/constants/pathConst";
import LyricUtil from "@/native/lyricUtil";
import { checkAndCreateDir } from "@/utils/fileUtils";
import PersistStatus from "@/utils/persistStatus";
import CryptoJs from "crypto-js";
import { unlink, writeFile } from "react-native-fs";
import RNTrackPlayer, { Event } from "react-native-track-player";
import { TrackPlayerEvents } from "@/core.defination/trackPlayer";
import { IPluginManager } from "@/types/core/pluginManager";
interface ILyricState {
loading: boolean;
lyrics: IParsedLrcItem[];
hasTranslation: boolean;
meta?: Record<string, string>;
}
const defaultLyricState = {
loading: true,
lyrics: [],
hasTranslation: false,
};
const lyricStateAtom = atom<ILyricState>(defaultLyricState);
const currentLyricItemAtom = atom<IParsedLrcItem | null>(null);
class LyricManager implements IInjectable {
private trackPlayer!: ITrackPlayer;
private appConfig!: IAppConfig;
private pluginManager!: IPluginManager;
private lyricParser: LyricParser | null = null;
get currentLyricItem() {
return getDefaultStore().get(currentLyricItemAtom);
}
get lyricState() {
return getDefaultStore().get(lyricStateAtom);
}
injectDependencies(trackPlayerService: ITrackPlayer, appConfigService: IAppConfig, pluginManager: IPluginManager): void {
this.trackPlayer = trackPlayerService;
this.appConfig = appConfigService;
this.pluginManager = pluginManager;
}
setup() {
this.trackPlayer.on(TrackPlayerEvents.CurrentMusicChanged, (musicItem) => {
this.refreshLyric(true, true);
if (this.appConfig.getConfig("lyric.showStatusBarLyric")) {
if (musicItem) {
LyricUtil.setStatusBarLyricText(
`${musicItem.title} - ${musicItem.artist}`,);
} else {
LyricUtil.setStatusBarLyricText("MusicFree");
}
}
});
RNTrackPlayer.addEventListener(Event.PlaybackProgressUpdated, evt => {
const parser = this.lyricParser;
if (!parser || !this.trackPlayer.isCurrentMusic(parser.musicItem)) {
return;
}
const currentLyricItem = getDefaultStore().get(currentLyricItemAtom);
const newLyricItem = parser.getPosition(evt.position);
if (currentLyricItem?.lrc !== newLyricItem?.lrc) {
getDefaultStore().set(currentLyricItemAtom, newLyricItem ?? null);
const showTranslation = PersistStatus.get("lyric.showTranslation");
if (this.appConfig.getConfig("lyric.showStatusBarLyric")) {
LyricUtil.setStatusBarLyricText(
(newLyricItem?.lrc ?? "") +
(showTranslation
? `\n${newLyricItem?.translation ?? ""}`
: ""),
);
}
}
});
if (this.appConfig.getConfig("lyric.showStatusBarLyric")) {
const statusBarLyricConfig = {
topPercent: this.appConfig.getConfig("lyric.topPercent"),
leftPercent: this.appConfig.getConfig("lyric.leftPercent"),
align: this.appConfig.getConfig("lyric.align"),
color: this.appConfig.getConfig("lyric.color"),
backgroundColor: this.appConfig.getConfig("lyric.backgroundColor"),
widthPercent: this.appConfig.getConfig("lyric.widthPercent"),
fontSize: this.appConfig.getConfig("lyric.fontSize"),
};
LyricUtil.showStatusBarLyric(
"MusicFree",
statusBarLyricConfig ?? {}
);
}
this.refreshLyric(true);
}
associateLyric(musicItem: IMusic.IMusicItem, linkToMusicItem: ICommon.IMediaBase) {
if (!musicItem || !linkToMusicItem) {
return false;
}
if (isSameMediaItem(musicItem, linkToMusicItem)) {
patchMediaExtra(musicItem, {
associatedLrc: undefined,
});
return false;
} else {
patchMediaExtra(musicItem, {
associatedLrc: linkToMusicItem,
});
if (this.trackPlayer.isCurrentMusic(musicItem)) {
this.refreshLyric(false);
}
return true;
}
}
unassociateLyric(musicItem: IMusic.IMusicItem) {
if (!musicItem) {
return;
}
patchMediaExtra(musicItem, {
associatedLrc: undefined,
});
if (this.trackPlayer.isCurrentMusic(musicItem)) {
this.refreshLyric(false);
}
}
async uploadLocalLyric(musicItem: IMusic.IMusicItem, lyricContent: string, type: "raw" | "translation" = "raw") {
if (!musicItem) {
return;
}
const platformHash = CryptoJs.MD5(musicItem.platform).toString(
CryptoJs.enc.Hex,
);
const idHash: string = CryptoJs.MD5(musicItem.id).toString(
CryptoJs.enc.Hex,
);
await checkAndCreateDir(pathConst.localLrcPath + platformHash);
await writeFile(pathConst.localLrcPath +
platformHash +
"/" +
idHash +
(type === "raw" ? "" : ".tran") +
".lrc", lyricContent, "utf8");
if (this.trackPlayer.isCurrentMusic(musicItem)) {
this.refreshLyric(false, false);
}
}
async removeLocalLyric(musicItem: IMusic.IMusicItem) {
if (!musicItem) {
return;
}
const platformHash = CryptoJs.MD5(musicItem.platform).toString(
CryptoJs.enc.Hex,
);
const idHash: string = CryptoJs.MD5(musicItem.id).toString(
CryptoJs.enc.Hex,
);
const basePath =
pathConst.localLrcPath + platformHash + "/" + idHash;
await unlink(basePath + ".lrc").catch(() => { });
await unlink(basePath + ".tran.lrc").catch(() => { });
if (this.trackPlayer.isCurrentMusic(musicItem)) {
this.refreshLyric(false, false);
}
}
updateLyricOffset(musicItem: IMusic.IMusicItem, offset: number) {
if (!musicItem) {
return;
}
patchMediaExtra(musicItem, {
lyricOffset: offset,
});
if (this.trackPlayer.isCurrentMusic(musicItem)) {
this.refreshLyric(true, false);
}
}
private setLyricAsLoadingState() {
getDefaultStore().set(lyricStateAtom, {
loading: true,
lyrics: [],
hasTranslation: false,
});
getDefaultStore().set(currentLyricItemAtom, null);
}
private setLyricAsNoLyricState() {
getDefaultStore().set(lyricStateAtom, {
loading: false,
lyrics: [],
hasTranslation: false,
});
getDefaultStore().set(currentLyricItemAtom, null);
if (this.appConfig.getConfig("lyric.showStatusBarLyric")) {
const musicItem = this.trackPlayer.currentMusic;
LyricUtil.setStatusBarLyricText(musicItem ? `${musicItem.title} - ${musicItem.artist}` : "MusicFree");
}
}
private async refreshLyric(skipFetchLyricSourceIfSame: boolean = true, ignoreProgress: boolean = false) {
const currentMusicItem = this.trackPlayer.currentMusic;
if (!currentMusicItem) {
this.setLyricAsNoLyricState();
return;
}
try {
let lrcSource: ILyric.ILyricSource | null;
if (skipFetchLyricSourceIfSame && this.lyricParser && this.trackPlayer.isCurrentMusic(this.lyricParser.musicItem)) {
lrcSource = this.lyricParser.lyricSource ?? null;
} else {
this.setLyricAsLoadingState();
lrcSource = (await this.pluginManager.getByMedia(currentMusicItem)?.methods?.getLyric(currentMusicItem)) ?? null;
}
if (!this.trackPlayer.isCurrentMusic(currentMusicItem)) {
return;
}
if (!lrcSource && this.appConfig.getConfig("lyric.autoSearchLyric")) {
this.setLyricAsLoadingState();
lrcSource = await this.searchSimilarLyric(currentMusicItem);
}
if (!this.trackPlayer.isCurrentMusic(currentMusicItem)) {
return;
}
if (!lrcSource) {
this.setLyricAsNoLyricState();
this.lyricParser = null;
return;
}
this.lyricParser = new LyricParser(lrcSource.rawLrc!, {
extra: {
offset: (getMediaExtraProperty(currentMusicItem, "lyricOffset") || 0) * -1,
},
musicItem: currentMusicItem,
lyricSource: lrcSource,
translation: lrcSource.translation,
});
getDefaultStore().set(lyricStateAtom, {
loading: false,
lyrics: this.lyricParser.getLyricItems(),
hasTranslation: !!lrcSource.translation,
meta: this.lyricParser.getMeta(),
});
const currentLyric = ignoreProgress ? (this.lyricParser.getLyricItems()?.[0] ?? null) : this.lyricParser.getPosition((await this.trackPlayer.getProgress()).position);
getDefaultStore().set(currentLyricItemAtom, currentLyric || null);
if (this.appConfig.getConfig("lyric.showStatusBarLyric")) {
if (currentLyric) {
LyricUtil.setStatusBarLyricText(
(currentLyric?.lrc ?? "") +
(this.lyricParser.hasTranslation
? `\n${currentLyric?.translation ?? ""}`
: ""),
);
} else {
const musicItem = this.trackPlayer.currentMusic;
LyricUtil.setStatusBarLyricText(musicItem ? `${musicItem.title} - ${musicItem.artist}` : "MusicFree");
}
}
} catch (err) {
if (this.trackPlayer.isCurrentMusic(currentMusicItem)) {
this.lyricParser = null;
this.setLyricAsNoLyricState();
}
}
}
* 检索最接近的歌词
* @param musicItem
* @returns
*/
private async searchSimilarLyric(musicItem: IMusic.IMusicItem) {
const keyword = musicItem.alias || musicItem.title;
const plugins = this.pluginManager.getSearchablePlugins("lyric");
let distance = Infinity;
let minDistanceMusicItem;
let targetPlugin: Plugin | null = null;
for (let plugin of plugins) {
if (
!this.trackPlayer.isCurrentMusic(musicItem)
) {
return null;
}
if (plugin.name === musicItem.platform) {
continue;
}
const results = await plugin.methods
.search(keyword, 1, "lyric")
.catch(() => null);
const firstTwo = results?.data?.slice(0, 2) || [];
for (let item of firstTwo) {
if (
item.title === keyword &&
item.artist === musicItem.artist
) {
distance = 0;
minDistanceMusicItem = item;
targetPlugin = plugin;
break;
} else {
const dist =
minDistance(keyword, musicItem.title) +
minDistance(item.artist, musicItem.artist);
if (dist < distance) {
distance = dist;
minDistanceMusicItem = item;
targetPlugin = plugin;
}
}
}
if (distance === 0) {
break;
}
}
if (minDistanceMusicItem && targetPlugin) {
return await targetPlugin.methods
.getLyric(minDistanceMusicItem)
.catch(() => null);
}
return null;
}
}
const lyricManager = new LyricManager();
export default lyricManager;
export const useLyricState = () => useAtomValue(lyricStateAtom);
export const useCurrentLyricItem = () => useAtomValue(currentLyricItemAtom);