import {
CacheControl,
internalSerializeKey,
localPluginPlatform,
} from "@/constants/commonConst";
import pathConst from "@/constants/pathConst";
import Mp3Util from "@/native/mp3Util";
import Base64 from "@/utils/base64";
import delay from "@/utils/delay";
import { addFileScheme, getFileName } from "@/utils/fileUtils";
import { getMediaExtraProperty, patchMediaExtra } from "@/utils/mediaExtra";
import { getLocalPath, isSameMediaItem, resetMediaItem } from "@/utils/mediaUtils";
import notImplementedFunction from "@/utils/notImplementedFunction.ts";
import axios from "axios";
import bigInt from "big-integer";
import * as cheerio from "cheerio";
import { satisfies } from "compare-versions";
import CryptoJs from "crypto-js";
import dayjs from "dayjs";
import he from "he";
import { produce } from "immer";
import { nanoid } from "nanoid";
import objectPath from "object-path";
import qs from "qs";
import { default as DeviceInfo, default as deviceInfoModule } from "react-native-device-info";
import RNFS, { exists, readFile, stat, writeFile } from "react-native-fs";
import { URL } from "react-native-url-polyfill";
import * as webdav from "webdav";
import { devLog, errorLog, trace } from "../../utils/log";
import Network from "../../utils/network";
import MediaCache from "../mediaCache";
import _internalPluginMeta from "./meta";
import { IPluginManager } from "@/types/core/pluginManager";
axios.defaults.timeout = 2000;
axios.interceptors.response.use((response) => {
const setCookie = response.headers["set-cookie"];
if(setCookie && setCookie.length === 1) {
const splitedCookie = setCookie[0].split(",");
response.headers["set-cookie"] = splitedCookie;
response.headers["x-set-cookie"] = setCookie;
}
return response;
});
const sha256 = CryptoJs.SHA256;
const deprecatedCookieManager = {
get: notImplementedFunction,
set: notImplementedFunction,
flush: notImplementedFunction,
};
const packages: Record<string, any> = {
cheerio,
"crypto-js": CryptoJs,
axios,
dayjs,
"big-integer": bigInt,
qs,
he,
"@react-native-cookies/cookies": deprecatedCookieManager,
webdav,
};
const _require = (packageName: string) => {
let pkg = packages[packageName];
pkg.default = pkg;
return pkg;
};
const _consoleBind = function (
method: "log" | "error" | "info" | "warn",
...args: any
) {
const fn = console[method];
if (fn) {
fn(...args);
devLog(method, ...args);
}
};
const _console = {
log: _consoleBind.bind(null, "log"),
warn: _consoleBind.bind(null, "warn"),
info: _consoleBind.bind(null, "info"),
error: _consoleBind.bind(null, "error"),
};
const appVersion = deviceInfoModule.getVersion();
function formatAuthUrl(url: string) {
const urlObj = new URL(url);
try {
if (urlObj.username && urlObj.password) {
const auth = `Basic ${Base64.btoa(
`${decodeURIComponent(urlObj.username)}:${decodeURIComponent(
urlObj.password,
)}`,
)}`;
urlObj.username = "";
urlObj.password = "";
return {
url: urlObj.toString(),
auth,
};
}
} catch (e) {
return {
url,
};
}
return {
url,
};
}
export enum PluginState {
Initializing,
Loading,
Mounted,
Error
}
export enum PluginErrorReason {
VersionNotMatch,
CannotParse,
}
export interface ILazyProps {
name: string;
hash: string;
path: string;
supportedMethods?: string[];
loadFuncCode?: () => Promise<string>;
instance?: IPlugin.IPluginDefine;
}
class PluginMethodsWrapper implements IPlugin.IPluginInstanceMethods {
private plugin: Plugin;
private ensurePluginIsMounted: () => Promise<void>;
constructor(plugin: Plugin, ensurePluginIsMounted: () => Promise<void>) {
this.plugin = plugin;
this.ensurePluginIsMounted = ensurePluginIsMounted;
}
async search<T extends ICommon.SupportMediaType>(
query: string,
page: number,
type: T,
): Promise<IPlugin.ISearchResult<T>> {
await this.ensurePluginIsMounted();
if (!this.plugin.instance.search) {
return {
isEnd: true,
data: [],
};
}
const result =
(await this.plugin.instance.search(query, page, type)) ?? {};
if (Array.isArray(result.data)) {
result.data.forEach(_ => {
resetMediaItem(_, this.plugin.name);
});
return {
isEnd: result.isEnd ?? true,
data: result.data,
};
}
return {
isEnd: true,
data: [],
};
}
async getMediaSource(
musicItem: IMusic.IMusicItemBase,
quality: IMusic.IQualityKey = "standard",
retryCount = 1,
notUpdateCache = false,
): Promise<IPlugin.IMediaSourceResult | null> {
await this.ensurePluginIsMounted();
const localPathInMediaExtra = getMediaExtraProperty(musicItem, "localPath");
const localPath = getLocalPath(musicItem);
if (localPath && (await exists(localPath))) {
trace("本地播放", localPath);
if (localPathInMediaExtra !== localPath) {
patchMediaExtra(musicItem, {
localPath,
});
}
return {
url: addFileScheme(localPath),
};
} else if (localPathInMediaExtra) {
patchMediaExtra(musicItem, {
localPath: undefined,
});
}
if (musicItem.platform === localPluginPlatform) {
throw new Error("本地音乐不存在");
}
const mediaCache = MediaCache.getMediaCache(
musicItem,
) as IMusic.IMusicItem | null;
const pluginCacheControl =
this.plugin.instance.cacheControl ?? "no-cache";
if (
mediaCache &&
mediaCache?.source?.[quality]?.url &&
(pluginCacheControl === CacheControl.Cache ||
(pluginCacheControl === CacheControl.NoCache &&
Network.isOffline))
) {
trace("播放", "缓存播放");
const qualityInfo = mediaCache.source[quality];
return {
url: qualityInfo!.url,
headers: mediaCache.headers,
userAgent:
mediaCache.userAgent ?? mediaCache.headers?.["user-agent"],
};
}
const alternativePlugin = Plugin.pluginManager?.getAlternativePlugin(this.plugin) as Plugin | null;
const parserPlugin = alternativePlugin?.instance?.getMediaSource ? alternativePlugin : this.plugin;
if (alternativePlugin) {
devLog("info", "设置了替代插件,实际使用的插件为", parserPlugin.name);
}
if (!parserPlugin.instance.getMediaSource) {
const { url, auth } = formatAuthUrl(
musicItem?.qualities?.[quality]?.url ?? musicItem.url,
);
return {
url: url,
headers: auth
? {
Authorization: auth,
}
: undefined,
};
}
try {
const { url, headers } = (await parserPlugin.instance.getMediaSource(
musicItem,
quality,
)) ?? { url: musicItem?.qualities?.[quality]?.url };
if (!url) {
throw new Error("NOT RETRY");
}
trace("播放", "插件播放");
const result = {
url,
headers,
userAgent: headers?.["user-agent"],
} as IPlugin.IMediaSourceResult;
const authFormattedResult = formatAuthUrl(result.url!);
if (authFormattedResult.auth) {
result.url = authFormattedResult.url;
result.headers = {
...(result.headers ?? {}),
Authorization: authFormattedResult.auth,
};
}
if (
pluginCacheControl !== CacheControl.NoStore &&
!notUpdateCache
) {
const cacheSource = {
headers: result.headers,
userAgent: result.userAgent,
url,
};
let realMusicItem = {
...musicItem,
...(mediaCache || {}),
};
realMusicItem.source = {
...(realMusicItem.source || {}),
[quality]: cacheSource,
};
MediaCache.setMediaCache(realMusicItem);
}
return result;
} catch (e: any) {
if (retryCount > 0 && e?.message !== "NOT RETRY") {
await delay(150);
return this.getMediaSource(musicItem, quality, --retryCount);
}
errorLog("获取真实源失败", e?.message);
devLog("error", "获取真实源失败", e, e?.message);
return null;
}
}
async getMusicInfo(
musicItem: ICommon.IMediaBase,
): Promise<Partial<IMusic.IMusicItem> | null> {
await this.ensurePluginIsMounted();
if (!this.plugin.instance.getMusicInfo) {
return null;
}
try {
return (
this.plugin.instance.getMusicInfo(
resetMediaItem(musicItem, undefined, true),
) ?? null
);
} catch (e: any) {
devLog("error", "获取音乐详情失败", e, e?.message);
return null;
}
}
*
* getLyric(musicItem) => {
* lyric: string;
* trans: string;
* }
*
*/
async getLyric(
originalMusicItem: IMusic.IMusicItemBase,
): Promise<ILyric.ILyricSource | null> {
await this.ensurePluginIsMounted();
const associatedLrc = getMediaExtraProperty(originalMusicItem, "associatedLrc");
let musicItem: IMusic.IMusicItem;
if (associatedLrc) {
musicItem = associatedLrc as IMusic.IMusicItem;
} else {
musicItem = originalMusicItem as IMusic.IMusicItem;
}
const musicItemCache = MediaCache.getMediaCache(
musicItem,
) as IMusic.IMusicItemCache | null;
let rawLrc: string | null = musicItem.rawLrc || null;
let translation: string | null = null;
const platformHash = CryptoJs.MD5(musicItem.platform).toString(
CryptoJs.enc.Hex,
);
const idHash = CryptoJs.MD5(musicItem.id).toString(CryptoJs.enc.Hex);
if (
await RNFS.exists(
pathConst.localLrcPath + platformHash + "/" + idHash + ".lrc",
)
) {
rawLrc = await RNFS.readFile(
pathConst.localLrcPath + platformHash + "/" + idHash + ".lrc",
"utf8",
);
if (
await RNFS.exists(
pathConst.localLrcPath +
platformHash +
"/" +
idHash +
".tran.lrc",
)
) {
translation =
(await RNFS.readFile(
pathConst.localLrcPath +
platformHash +
"/" +
idHash +
".tran.lrc",
"utf8",
)) || null;
}
return {
rawLrc,
translation: translation || undefined,
};
}
if (musicItemCache?.lyric) {
let cacheLyric: ILyric.ILyricSource | null =
musicItemCache.lyric || null;
let localLyric: ILyric.ILyricSource | null =
musicItemCache.$localLyric || null;
if (cacheLyric.rawLrc || cacheLyric.translation) {
return {
rawLrc: cacheLyric.rawLrc,
translation: cacheLyric.translation,
};
}
if (localLyric) {
let needRefetch = false;
if (localLyric.rawLrc && (await exists(localLyric.rawLrc))) {
rawLrc = await readFile(localLyric.rawLrc, "utf8");
} else if (localLyric.rawLrc) {
needRefetch = true;
}
if (
localLyric.translation &&
(await exists(localLyric.translation))
) {
translation = await readFile(
localLyric.translation,
"utf8",
);
} else if (localLyric.translation) {
needRefetch = true;
}
if (!needRefetch && (rawLrc || translation)) {
return {
rawLrc: rawLrc || undefined,
translation: translation || undefined,
};
}
}
}
let lrcSource: ILyric.ILyricSource | null;
if (isSameMediaItem(originalMusicItem, musicItem)) {
lrcSource =
(await this.plugin.instance
?.getLyric?.(resetMediaItem(musicItem, undefined, true))
?.catch(() => null)) || null;
} else {
lrcSource =
(await Plugin.pluginManager?.getByMedia(musicItem)
?.instance?.getLyric?.(
resetMediaItem(musicItem, undefined, true),
)
?.catch(() => null)) || null;
}
if (lrcSource) {
rawLrc = lrcSource?.rawLrc || rawLrc;
translation = lrcSource?.translation || null;
const deprecatedLrcUrl = lrcSource?.lrc || musicItem.lrc;
let filename: string | undefined = `${pathConst.lrcCachePath
}${nanoid()}.lrc`;
let filenameTrans: string | undefined = `${pathConst.lrcCachePath
}${nanoid()}.lrc`;
if (!(rawLrc || translation)) {
if (deprecatedLrcUrl) {
rawLrc = (
await axios
.get(deprecatedLrcUrl, { timeout: 3000 })
.catch(() => null)
)?.data;
} else if (musicItem.rawLrc) {
rawLrc = musicItem.rawLrc;
}
}
if (rawLrc) {
await writeFile(filename, rawLrc, "utf8");
} else {
filename = undefined;
}
if (translation) {
await writeFile(filenameTrans, translation, "utf8");
} else {
filenameTrans = undefined;
}
if (rawLrc || translation) {
MediaCache.setMediaCache(
produce(musicItemCache || musicItem, draft => {
musicItemCache?.$localLyric?.rawLrc;
objectPath.set(draft, "$localLyric.rawLrc", filename);
objectPath.set(
draft,
"$localLyric.translation",
filenameTrans,
);
return draft;
}),
);
return {
rawLrc: rawLrc || undefined,
translation: translation || undefined,
};
}
}
const localFilePath = getLocalPath(originalMusicItem);
if (
originalMusicItem.platform !== localPluginPlatform &&
localFilePath
) {
const res = await localFilePluginDefine!.getLyric!(originalMusicItem);
devLog("info", "本地文件歌词");
if (res) {
return res;
}
}
devLog("warn", "无歌词");
return null;
}
async getAlbumInfo(
albumItem: IAlbum.IAlbumItemBase,
page: number = 1,
): Promise<IPlugin.IAlbumInfoResult | null> {
await this.ensurePluginIsMounted();
if (!this.plugin.instance.getAlbumInfo) {
return {
albumItem,
musicList: (albumItem?.musicList ?? []).map(
resetMediaItem,
this.plugin.name,
true,
),
isEnd: true,
};
}
try {
const result = await this.plugin.instance.getAlbumInfo(
resetMediaItem(albumItem, undefined, true),
page,
);
if (!result) {
throw new Error();
}
result?.musicList?.forEach(_ => {
resetMediaItem(_, this.plugin.name);
_.album = albumItem.title;
});
if (page <= 1) {
return {
albumItem: { ...albumItem, ...(result?.albumItem ?? {}) },
isEnd: result.isEnd === false ? false : true,
musicList: result.musicList,
};
} else {
return {
isEnd: result.isEnd === false ? false : true,
musicList: result.musicList,
};
}
} catch (e: any) {
trace("获取专辑信息失败", e?.message);
devLog("error", "获取专辑信息失败", e, e?.message);
return null;
}
}
async getMusicSheetInfo(
sheetItem: IMusic.IMusicSheetItem,
page: number = 1,
): Promise<IPlugin.ISheetInfoResult | null> {
await this.ensurePluginIsMounted();
if (!this.plugin.instance.getMusicSheetInfo) {
return {
sheetItem,
musicList: sheetItem?.musicList ?? [],
isEnd: true,
};
}
try {
const result = await this.plugin.instance?.getMusicSheetInfo?.(
resetMediaItem(sheetItem, undefined, true),
page,
);
if (!result) {
throw new Error();
}
result?.musicList?.forEach(_ => {
resetMediaItem(_, this.plugin.name);
});
if (page <= 1) {
return {
sheetItem: { ...sheetItem, ...(result?.sheetItem ?? {}) },
isEnd: result.isEnd === false ? false : true,
musicList: result.musicList,
};
} else {
return {
isEnd: result.isEnd === false ? false : true,
musicList: result.musicList,
};
}
} catch (e: any) {
trace("获取歌单信息失败", e, e?.message);
devLog("error", "获取歌单信息失败", e, e?.message);
return null;
}
}
async getArtistWorks<T extends IArtist.ArtistMediaType>(
artistItem: IArtist.IArtistItem,
page: number,
type: T,
): Promise<IPlugin.ISearchResult<T>> {
await this.ensurePluginIsMounted();
if (!this.plugin.instance.getArtistWorks) {
return {
isEnd: true,
data: [],
};
}
try {
const result = await this.plugin.instance.getArtistWorks(
artistItem,
page,
type,
);
if (!result.data) {
return {
isEnd: true,
data: [],
};
}
result.data?.forEach(_ => resetMediaItem(_, this.plugin.name));
return {
isEnd: result.isEnd ?? true,
data: result.data,
};
} catch (e: any) {
trace("查询作者信息失败", e?.message);
devLog("error", "查询作者信息失败", e, e?.message);
throw e;
}
}
async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> {
await this.ensurePluginIsMounted();
try {
const result =
(await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? [];
result.forEach(_ => resetMediaItem(_, this.plugin.name));
return result;
} catch (e: any) {
console.log(e);
devLog("error", "导入歌单失败", e, e?.message);
return [];
}
}
async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> {
await this.ensurePluginIsMounted();
try {
const result = await this.plugin.instance?.importMusicItem?.(
urlLike,
);
if (!result) {
throw new Error();
}
resetMediaItem(result, this.plugin.name);
return result;
} catch (e: any) {
devLog("error", "导入单曲失败", e, e?.message);
return null;
}
}
async getTopLists(): Promise<IMusic.IMusicSheetGroupItem[]> {
await this.ensurePluginIsMounted();
try {
const result = await this.plugin.instance?.getTopLists?.();
if (!result) {
throw new Error();
}
return result;
} catch (e: any) {
devLog("error", "获取榜单失败", e, e?.message);
return [];
}
}
async getTopListDetail(
topListItem: IMusic.IMusicSheetItemBase,
page: number,
): Promise<IPlugin.ITopListInfoResult> {
await this.ensurePluginIsMounted();
const result = await this.plugin.instance?.getTopListDetail?.(
topListItem,
page,
);
if (!result) {
throw new Error();
}
if (result.musicList) {
result.musicList.forEach(_ =>
resetMediaItem(_, this.plugin.name),
);
} else {
result.musicList = [];
}
if (result.isEnd !== false) {
result.isEnd = true;
}
return result;
}
async getRecommendSheetTags(): Promise<IPlugin.IGetRecommendSheetTagsResult> {
await this.ensurePluginIsMounted();
try {
const result =
await this.plugin.instance?.getRecommendSheetTags?.();
if (!result) {
throw new Error();
}
return result;
} catch (e: any) {
devLog("error", "获取推荐歌单失败", e, e?.message);
return {
data: [],
};
}
}
async getRecommendSheetsByTag(
tagItem: ICommon.IUnique,
page?: number,
): Promise<ICommon.PaginationResponse<IMusic.IMusicSheetItemBase>> {
await this.ensurePluginIsMounted();
try {
const result =
await this.plugin.instance?.getRecommendSheetsByTag?.(
tagItem,
page ?? 1,
);
if (!result) {
throw new Error();
}
if (result.isEnd !== false) {
result.isEnd = true;
}
if (!result.data) {
result.data = [];
}
result.data.forEach(item => resetMediaItem(item, this.plugin.name));
return result;
} catch (e: any) {
devLog("error", "获取推荐歌单详情失败", e, e?.message);
return {
isEnd: true,
data: [],
};
}
}
async getMusicComments(
musicItem: IMusic.IMusicItem,
page?: number
): Promise<ICommon.PaginationResponse<IMedia.IComment>> {
await this.ensurePluginIsMounted();
const result = await this.plugin.instance?.getMusicComments?.(
musicItem,
page ?? 1
);
if (!result) {
throw new Error();
}
if (result.isEnd !== false) {
result.isEnd = true;
}
if (!result.data) {
result.data = [];
}
return result;
}
}
export class Plugin {
public name: string = "";
public hash: string = "";
public state: PluginState = PluginState.Initializing;
public errorReason?: PluginErrorReason;
public instance: IPlugin.IPluginDefine = { platform: "" };
public path: string = "";
public methods!: IPlugin.IPluginInstanceMethods;
public supportedMethods: Set<keyof IPlugin.IPluginInstanceMethods> = new Set();
private lazyProps: ILazyProps | null = null;
static pluginManager: IPluginManager;
static injectDependencies(
pluginManager: IPluginManager,
) {
Plugin.pluginManager = pluginManager;
}
constructor(
funcCode: string | (() => IPlugin.IPluginDefine) | null,
pluginPath: string,
lazyProps: ILazyProps | null = null
) {
this.lazyProps = lazyProps;
if (!lazyProps) {
this.mountPlugin(funcCode!, pluginPath);
this.methods = new PluginMethodsWrapper(this, async () => {});
} else {
this.name = lazyProps.name;
this.hash = lazyProps.hash;
this.path = lazyProps.path;
this.instance = lazyProps.instance ?? {
platform: lazyProps.name,
};
this.supportedMethods = new Set((lazyProps.supportedMethods ?? []) as any);
this.methods = new PluginMethodsWrapper(this, this.ensureMounted.bind(this));
}
}
async ensureMounted() {
if ((this.state === PluginState.Initializing) && this.lazyProps) {
this.state = PluginState.Loading;
const loadFuncCode = this.lazyProps.loadFuncCode ?? (() => "");
try {
const funcCode = await loadFuncCode();
this.mountPlugin(funcCode, this.lazyProps.path);
} catch {
this.state = PluginState.Error;
this.errorReason = this.errorReason ?? PluginErrorReason.CannotParse;
}
}
}
private mountPlugin(
funcCode: string | (() => IPlugin.IPluginDefine),
pluginPath: string) {
this.state = PluginState.Loading;
let _instance: IPlugin.IPluginDefine;
const _module: any = { exports: {} };
try {
if (typeof funcCode === "string") {
const env = {
getUserVariables: () => {
return (
_internalPluginMeta.getUserVariables(this.name)
);
},
get userVariables() {
return this.getUserVariables() ?? {};
},
appVersion,
os: "android",
lang: "zh-CN",
};
const _process = {
platform: "android",
version: appVersion,
env,
};
_instance = Function(`
'use strict';
return function(require, __musicfree_require, module, exports, console, env, URL, process) {
${funcCode}
}
`)()(
_require,
_require,
_module,
_module.exports,
_console,
env,
URL,
_process
);
if (_module.exports.default) {
_instance = _module.exports
.default as IPlugin.IPluginInstance;
} else {
_instance = _module.exports as IPlugin.IPluginInstance;
}
} else {
_instance = funcCode();
}
if (Array.isArray(_instance.userVariables)) {
_instance.userVariables = _instance.userVariables.filter(
it => it?.key,
);
}
this.checkValid(_instance);
} catch (e: any) {
this.state = PluginState.Error;
this.errorReason = e?.errorReason ?? PluginErrorReason.CannotParse;
errorLog(`${pluginPath}插件无法解析 `, {
errorReason: this.errorReason,
message: e?.message,
stack: e?.stack,
});
_instance = e?.instance ?? {
platform: "",
appVersion: "",
async getMediaSource() {
return null;
},
async search() {
return {};
},
async getAlbumInfo() {
return null;
},
};
}
this.instance = _instance;
this.path = pluginPath;
this.name = _instance.platform;
this.supportedMethods = new Set(Object.keys(_instance).filter(
key => typeof (_instance[key]) === "function",
) as any);
if (
this.name === "" ||
!this.name
) {
this.hash = "";
this.state = PluginState.Error;
this.errorReason = this.errorReason ?? PluginErrorReason.CannotParse;
} else {
if (typeof funcCode === "string") {
this.hash = sha256(funcCode).toString();
} else {
this.hash = sha256(pluginPath + "@" + appVersion).toString();
}
}
if (this.state !== PluginState.Error) {
this.state = PluginState.Mounted;
}
}
private checkValid(_instance: IPlugin.IPluginDefine) {
if (
_instance.appVersion &&
!satisfies(DeviceInfo.getVersion(), _instance.appVersion)
) {
throw {
instance: _instance,
state: PluginState.Error,
errorReason: PluginErrorReason.VersionNotMatch,
};
}
return true;
}
}
const localFilePluginDefine: IPlugin.IPluginDefine = {
platform: localPluginPlatform,
async getMusicInfo(musicBase) {
const localPath = getLocalPath(musicBase);
if (localPath) {
const coverImg = await Mp3Util.getMediaCoverImg(localPath);
return {
artwork: coverImg,
};
}
return null;
},
async getLyric(musicBase) {
const localPath = getLocalPath(musicBase);
let rawLrc: string | null = null;
if (localPath) {
try {
rawLrc = await Mp3Util.getLyric(localPath);
} catch (e) {
console.log("读取内嵌歌词失败", e);
}
if (!rawLrc) {
const lastDot = localPath.lastIndexOf(".");
const lrcPath = localPath.slice(0, lastDot) + ".lrc";
try {
if (await exists(lrcPath)) {
rawLrc = await readFile(lrcPath, "utf8");
}
} catch { }
}
}
return rawLrc
? {
rawLrc,
}
: null;
},
async importMusicItem(urlLike) {
let meta: any = {};
let id: string;
try {
meta = await Mp3Util.getBasicMeta(urlLike);
const fileStat = await stat(urlLike);
id =
CryptoJs.MD5(fileStat.originalFilepath).toString(
CryptoJs.enc.Hex,
) || nanoid();
} catch {
id = nanoid();
}
return {
id: id,
platform: localPluginPlatform,
title: meta?.title ?? getFileName(urlLike),
artist: meta?.artist ?? "未知歌手",
duration: parseInt(meta?.duration ?? "0", 10) / 1000,
album: meta?.album ?? "未知专辑",
artwork: "",
[internalSerializeKey]: {
localPath: urlLike,
},
url: urlLike,
};
},
async getMediaSource(musicItem, quality) {
if (quality === "standard") {
return {
url: addFileScheme(musicItem.$?.localPath || musicItem.url),
};
}
return null;
},
};
export const localFilePlugin = new Plugin(function () {
return localFilePluginDefine;
}, "internal-plugin://local-file-plugin");