181 lines
5.5 KiB
JavaScript
181 lines
5.5 KiB
JavaScript
import kw from './kw/index.js'
|
||
import kg from './kg/index.js'
|
||
import tx from './tx/index.js'
|
||
import wy from './wy/index.js'
|
||
import mg from './mg/index.js'
|
||
import bd from './bd/index.js'
|
||
import xm from './xm.js'
|
||
import { supportQuality } from './api-source.js'
|
||
import {
|
||
sizeFormate,
|
||
formatPlayTime,
|
||
dateFormat,
|
||
dateFormat2,
|
||
formatPlayCount,
|
||
decodeName,
|
||
} from '../src/index.js'
|
||
|
||
// 通用工具函数(SDK 内 `import { ... } from '../index.js'` 的解析目标)
|
||
export { sizeFormate, formatPlayTime, dateFormat, dateFormat2, formatPlayCount, decodeName }
|
||
|
||
const SINGERS_SEPARATOR = /、|&|;|;|\/|,|,|\||\]|\[|!|!/g
|
||
const SUPPORTED_SOURCES = [
|
||
{ id: 'kw', name: '酷我音乐', module: kw },
|
||
{ id: 'kg', name: '酷狗音乐', module: kg },
|
||
{ id: 'tx', name: 'QQ音乐', module: tx },
|
||
{ id: 'wy', name: '网易音乐', module: wy },
|
||
{ id: 'mg', name: '咪咕音乐', module: mg },
|
||
{ id: 'xm', name: '虾米音乐', module: xm },
|
||
// { id: 'bd', name: '百度音乐', module: bd },
|
||
]
|
||
|
||
const SOURCE_MODULES = Object.fromEntries(SUPPORTED_SOURCES.map((s) => [s.id, s.module]))
|
||
|
||
export const sources = SUPPORTED_SOURCES.map(({ id, name }) => ({ id, name }))
|
||
|
||
export const init = () =>
|
||
Promise.all(SUPPORTED_SOURCES.map((s) => s.module.init?.()).filter(Boolean))
|
||
|
||
export { supportQuality }
|
||
|
||
const trimStr = (str) => (typeof str === 'string' ? str.trim() : str)
|
||
|
||
const filterStr = (str) =>
|
||
typeof str === 'string'
|
||
? str.replace(/\s|'|\.|,|,|&|"|、|\(|\)|(|)|`|~|-|<|>|\||\/|\]|\[|!|!/g, '')
|
||
: String(str ?? '')
|
||
|
||
const getIntervalSeconds = (interval) => {
|
||
if (!interval) return 0
|
||
const parts = String(interval).split(':')
|
||
let seconds = 0
|
||
let unit = 1
|
||
while (parts.length) {
|
||
seconds += parseInt(parts.pop()) * unit
|
||
unit *= 60
|
||
}
|
||
return seconds
|
||
}
|
||
|
||
const sortSingle = (singer) => {
|
||
if (!singer) return ''
|
||
return SINGERS_SEPARATOR.test(singer)
|
||
? singer.split(SINGERS_SEPARATOR).sort((a, b) => a.localeCompare(b)).join('、')
|
||
: singer
|
||
}
|
||
|
||
/**
|
||
* 多平台聚合搜索
|
||
* @param {object} params
|
||
* @param {string} params.name
|
||
* @param {string} [params.singer]
|
||
* @param {string} [params.source] 指定平台时,聚合结果会跳过该平台
|
||
* @param {number} [params.limit=25]
|
||
*/
|
||
export const searchMusic = async ({ name, singer, source: excludeSource, limit = 25 }) => {
|
||
const musicName = trimStr(name)
|
||
const tasks = []
|
||
for (const source of SUPPORTED_SOURCES) {
|
||
if (
|
||
source.id === 'xm' ||
|
||
source.id === excludeSource ||
|
||
!source.module.musicSearch
|
||
)
|
||
continue
|
||
tasks.push(
|
||
source.module.musicSearch
|
||
.search(`${musicName} ${singer || ''}`.trim(), 1, limit)
|
||
.catch(() => null)
|
||
)
|
||
}
|
||
return (await Promise.all(tasks)).filter(Boolean)
|
||
}
|
||
|
||
const pickMatching = (items, predicate) => {
|
||
const matched = []
|
||
const rest = []
|
||
for (const item of items) {
|
||
if (predicate(item)) matched.push(item)
|
||
else rest.push(item)
|
||
}
|
||
return { matched, rest }
|
||
}
|
||
|
||
/**
|
||
* 跨平台精确匹配歌曲(按歌手、歌名、专辑、时长综合排序)
|
||
*/
|
||
export const findMusic = async ({ name, singer, albumName, interval, source: s }) => {
|
||
const lists = await searchMusic({ name, singer, source: s, limit: 25 })
|
||
|
||
const fMusicName = filterStr(name).toLowerCase()
|
||
const fSinger = filterStr(sortSingle(singer)).toLowerCase()
|
||
const fAlbumName = filterStr(albumName).toLowerCase()
|
||
const fInterval = getIntervalSeconds(interval)
|
||
|
||
const isEqualsInterval = (intv) =>
|
||
Math.abs((fInterval || intv) - (intv || fInterval)) < 5
|
||
const isIncludesName = (n) => fMusicName.includes(n) || n.includes(fMusicName)
|
||
const isIncludesSinger = (sng) =>
|
||
fSinger ? fSinger.includes(sng) || sng.includes(fSinger) : true
|
||
const isEqualsAlbum = (alb) => (fAlbumName ? fAlbumName === alb : true)
|
||
|
||
let candidates = []
|
||
for (const source of lists) {
|
||
for (const item of source.list) {
|
||
item.name = trimStr(item.name)
|
||
item.singer = trimStr(item.singer)
|
||
item.fSinger = filterStr(sortSingle(item.singer).toLowerCase())
|
||
item.fMusicName = filterStr(String(item.name ?? '').toLowerCase())
|
||
item.fAlbumName = filterStr(String(item.albumName ?? '').toLowerCase())
|
||
item.fInterval = getIntervalSeconds(item.interval)
|
||
if (!isEqualsInterval(item.fInterval)) {
|
||
item.name = null
|
||
continue
|
||
}
|
||
candidates.push(item)
|
||
}
|
||
}
|
||
|
||
const result = []
|
||
const take = (predicate) => {
|
||
const { matched, rest } = pickMatching(candidates, predicate)
|
||
result.push(...matched)
|
||
candidates = rest
|
||
}
|
||
|
||
take((item) => item.fSinger === fSinger && item.fMusicName === fMusicName && item.interval === interval)
|
||
take((item) => item.fMusicName === fMusicName && item.fSinger === fSinger && item.fAlbumName === fAlbumName)
|
||
take((item) => item.fSinger === fSinger && item.fMusicName === fMusicName)
|
||
take((item) => item.fMusicName === fMusicName && item.interval === interval)
|
||
take((item) => item.fSinger === fSinger && item.interval === interval)
|
||
take((item) => item.interval === interval)
|
||
take((item) => item.fMusicName === fMusicName)
|
||
take((item) => item.fSinger === fSinger)
|
||
take((item) => item.fAlbumName === fAlbumName)
|
||
|
||
for (const item of candidates) {
|
||
delete item.fSinger
|
||
delete item.fMusicName
|
||
delete item.fAlbumName
|
||
delete item.fInterval
|
||
}
|
||
for (const item of result) {
|
||
delete item.fSinger
|
||
delete item.fMusicName
|
||
delete item.fAlbumName
|
||
delete item.fInterval
|
||
}
|
||
|
||
result.push(...candidates)
|
||
return result
|
||
}
|
||
|
||
export default {
|
||
sources,
|
||
...SOURCE_MODULES,
|
||
init,
|
||
supportQuality,
|
||
searchMusic,
|
||
findMusic,
|
||
}
|