v2.0.0
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
// 非业务工具方法
|
||||
|
||||
/**
|
||||
* 获取两个数之间的随机整数,大于等于min,小于max
|
||||
* @param {*} min
|
||||
* @param {*} max
|
||||
*/
|
||||
export const getRandom = (min: number, max: number): number => Math.floor(Math.random() * (max - min)) + min
|
||||
|
||||
|
||||
export const sizeFormate = (size: number): string => {
|
||||
// https://gist.github.com/thomseddon/3511330
|
||||
if (!size) return '0 B'
|
||||
let units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let number = Math.floor(Math.log(size) / Math.log(1024))
|
||||
return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}`
|
||||
}
|
||||
|
||||
const numFix = (n: number): string => n < 10 ? (`0${n}`) : n.toString()
|
||||
/**
|
||||
* 日期格式化
|
||||
* @param {*} date 时间
|
||||
* @param {String} format 时间格式,默认YYYY-MM-DD hh:mm:ss
|
||||
*/
|
||||
export const dateFormat = (date: string | number | Date, format = 'YYYY-MM-DD hh:mm:ss') => {
|
||||
if (typeof date != 'object') date = new Date(date)
|
||||
return format
|
||||
.replace('YYYY', date.getFullYear().toString())
|
||||
.replace('MM', numFix(date.getMonth() + 1).toString())
|
||||
.replace('DD', numFix(date.getDate()))
|
||||
.replace('hh', numFix(date.getHours()))
|
||||
.replace('mm', numFix(date.getMinutes()))
|
||||
.replace('ss', numFix(date.getSeconds()))
|
||||
}
|
||||
|
||||
|
||||
export const formatPlayTime = (time: number) => {
|
||||
let m = Math.trunc(time / 60)
|
||||
let s = Math.trunc(time % 60)
|
||||
return m == 0 && s == 0 ? '--/--' : numFix(m) + ':' + numFix(s)
|
||||
}
|
||||
|
||||
export const formatPlayTime2 = (time: number) => {
|
||||
let m = Math.trunc(time / 60)
|
||||
let s = Math.trunc(time % 60)
|
||||
return numFix(m) + ':' + numFix(s)
|
||||
}
|
||||
|
||||
|
||||
const encodeNames = {
|
||||
' ': ' ',
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
''': "'",
|
||||
} as const
|
||||
export const decodeName = (str: string | null = '') => {
|
||||
return str?.replace(/(?:&|<|>|"|'|'| )/gm, (s: string) => encodeNames[s as keyof typeof encodeNames]) ?? ''
|
||||
}
|
||||
|
||||
// 解析URL参数为对象
|
||||
export const parseUrlParams = (str: string): Record<string, string> => {
|
||||
const params: Record<string, string> = {}
|
||||
if (typeof str !== 'string') return params
|
||||
const paramsArr = str.split('&')
|
||||
for (const param of paramsArr) {
|
||||
let [key, value] = param.split('=')
|
||||
params[key] = value
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成节流函数
|
||||
* @param fn 回调
|
||||
* @param delay 延迟
|
||||
* @returns
|
||||
*/
|
||||
export function throttle<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {
|
||||
let timer: NodeJS.Timeout | null = null
|
||||
let _args: Args
|
||||
return (...args: Args) => {
|
||||
_args = args
|
||||
if (timer) return
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
void fn(..._args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成防抖函数
|
||||
* @param fn 回调
|
||||
* @param delay 延迟
|
||||
* @returns
|
||||
*/
|
||||
export function debounce<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {
|
||||
let timer: NodeJS.Timeout | null = null
|
||||
let _args: Args
|
||||
return (...args: Args) => {
|
||||
_args = args
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
void fn(..._args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
const fileNameRxp = /[\\/:*?#"<>|]/g
|
||||
export const filterFileName = (name: string): string => name.replace(fileNameRxp, '')
|
||||
|
||||
|
||||
// https://blog.csdn.net/xcxy2015/article/details/77164126#comments
|
||||
/**
|
||||
*
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
export const similar = (a: string, b: string) => {
|
||||
if (!a || !b) return 0
|
||||
if (a.length > b.length) { // 保证 a <= b
|
||||
let t = b
|
||||
b = a
|
||||
a = t
|
||||
}
|
||||
let al = a.length
|
||||
let bl = b.length
|
||||
let mp = [] // 一个表
|
||||
let i, j, ai, lt, tmp // ai:字符串a的第i个字符。 lt:左上角的值。 tmp:暂存新的值。
|
||||
for (i = 0; i <= bl; i++) mp[i] = i
|
||||
for (i = 1; i <= al; i++) {
|
||||
ai = a.charAt(i - 1)
|
||||
lt = mp[0]
|
||||
mp[0] = mp[0] + 1
|
||||
for (j = 1; j <= bl; j++) {
|
||||
tmp = Math.min(mp[j] + 1, mp[j - 1] + 1, lt + (ai == b.charAt(j - 1) ? 0 : 1))
|
||||
lt = mp[j]
|
||||
mp[j] = tmp
|
||||
}
|
||||
}
|
||||
return 1 - (mp[bl] / bl)
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序字符串
|
||||
* @param arr
|
||||
* @param data
|
||||
*/
|
||||
export const sortInsert = (arr: Array<{ num: number, data: any }>, data: { num: number, data: any }) => {
|
||||
let key = data.num
|
||||
let left = 0
|
||||
let right = arr.length - 1
|
||||
|
||||
while (left <= right) {
|
||||
let middle = Math.trunc((left + right) / 2)
|
||||
if (key == arr[middle].num) {
|
||||
left = middle
|
||||
break
|
||||
} else if (key < arr[middle].num) {
|
||||
right = middle - 1
|
||||
} else {
|
||||
left = middle + 1
|
||||
}
|
||||
}
|
||||
while (left > 0) {
|
||||
if (arr[left - 1].num != key) break
|
||||
left--
|
||||
}
|
||||
|
||||
arr.splice(left, 0, data)
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// https://github.com/tholman/cursor-effects/blob/master/src/bubbleCursor.js
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, fillStyle, strokeStyle) {
|
||||
const lifeSpan = Math.floor(Math.random() * 60 + 60)
|
||||
this.initialLifeSpan = lifeSpan //
|
||||
this.lifeSpan = lifeSpan // ms
|
||||
this.velocity = {
|
||||
x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 10),
|
||||
y: -0.4 + Math.random() * -1,
|
||||
}
|
||||
this.position = { x, y }
|
||||
this.fillStyle = fillStyle
|
||||
this.strokeStyle = strokeStyle
|
||||
|
||||
this.baseDimension = 4
|
||||
}
|
||||
|
||||
update(context, width) {
|
||||
this.position.x += this.velocity.x
|
||||
this.position.y += this.velocity.y
|
||||
|
||||
this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75
|
||||
this.velocity.y -= Math.random() / 600
|
||||
if (this.position.x >= width - 2 || this.position.x <= 2) this.lifeSpan = 0
|
||||
else if (this.position.y <= 5) this.lifeSpan = 0
|
||||
this.lifeSpan--
|
||||
|
||||
const scale =
|
||||
0.2 + (this.initialLifeSpan - this.lifeSpan) / this.initialLifeSpan
|
||||
|
||||
context.fillStyle = this.fillStyle
|
||||
context.strokeStyle = this.strokeStyle
|
||||
context.beginPath()
|
||||
context.arc(
|
||||
this.position.x - (this.baseDimension / 2) * scale,
|
||||
this.position.y - this.baseDimension / 2,
|
||||
this.baseDimension * scale,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
)
|
||||
|
||||
context.stroke()
|
||||
context.fill()
|
||||
|
||||
context.closePath()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class BubbleCursor {
|
||||
constructor({ element, fillStyle = 'rgba(77, 175, 124, 0.1)', strokeStyle = 'rgba(77, 175, 124, 0.3)' } = {}) {
|
||||
this.hasWrapperEl = element
|
||||
this.element = this.hasWrapperEl || document.body
|
||||
|
||||
this.width = window.innerWidth
|
||||
this.height = window.innerHeight
|
||||
this.cursor = { x: this.width / 2, y: this.width / 2 }
|
||||
this.particles = []
|
||||
this.canvas = null
|
||||
this.context = null
|
||||
this.fillStyle = fillStyle
|
||||
this.strokeStyle = strokeStyle
|
||||
|
||||
this.init()
|
||||
}
|
||||
|
||||
init() {
|
||||
this.canvas = document.createElement('canvas')
|
||||
this.context = this.canvas.getContext('2d')
|
||||
|
||||
this.canvas.style.top = '0px'
|
||||
this.canvas.style.left = '0px'
|
||||
this.canvas.style.pointerEvents = 'none'
|
||||
this.canvas.style.zIndex = 100
|
||||
|
||||
if (this.hasWrapperEl) {
|
||||
this.canvas.style.position = 'absolute'
|
||||
this.element.appendChild(this.canvas)
|
||||
this.canvas.width = this.element.clientWidth
|
||||
this.canvas.height = this.element.clientHeight
|
||||
} else {
|
||||
this.canvas.style.position = 'fixed'
|
||||
document.body.appendChild(this.canvas)
|
||||
this.canvas.width = this.width
|
||||
this.canvas.height = this.height
|
||||
}
|
||||
|
||||
this.bindEvents()
|
||||
this.loop()
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.element.addEventListener('mousemove', this.onMouseMove)
|
||||
this.element.addEventListener('touchmove', this.onTouchMove, { passive: true })
|
||||
this.element.addEventListener('touchstart', this.onTouchMove, { passive: true })
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
}
|
||||
|
||||
|
||||
onWindowResize = (e) => {
|
||||
this.width = window.innerWidth
|
||||
this.height = window.innerHeight
|
||||
|
||||
if (this.hasWrapperEl) {
|
||||
this.canvas.width = this.element.clientWidth
|
||||
this.canvas.height = this.element.clientHeight
|
||||
} else {
|
||||
this.canvas.width = this.width
|
||||
this.canvas.height = this.height
|
||||
}
|
||||
}
|
||||
|
||||
onTouchMove = (e) => {
|
||||
if (e.touches.length > 0) {
|
||||
for (let i = 0; i < e.touches.length; i++) {
|
||||
this.addParticle(
|
||||
e.touches[i].clientX,
|
||||
e.touches[i].clientY,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMouseMove = (e) => {
|
||||
if (this.hasWrapperEl) {
|
||||
const boundingRect = this.element.getBoundingClientRect()
|
||||
this.cursor.x = e.clientX - boundingRect.left
|
||||
this.cursor.y = e.clientY - boundingRect.top
|
||||
} else {
|
||||
this.cursor.x = e.clientX
|
||||
this.cursor.y = e.clientY
|
||||
}
|
||||
|
||||
this.addParticle(this.cursor.x, this.cursor.y)
|
||||
}
|
||||
|
||||
addParticle(x, y) {
|
||||
this.particles.push(new Particle(x, y, this.fillStyle, this.strokeStyle))
|
||||
}
|
||||
|
||||
updateParticles() {
|
||||
this.context.clearRect(0, 0, this.width, this.height)
|
||||
|
||||
// Update
|
||||
for (let i = 0; i < this.particles.length; i++) {
|
||||
this.particles[i].update(this.context, this.width)
|
||||
}
|
||||
|
||||
// Remove dead particles
|
||||
for (let i = this.particles.length - 1; i >= 0; i--) {
|
||||
if (this.particles[i].lifeSpan < 0) {
|
||||
this.particles.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop = () => {
|
||||
this.updateParticles()
|
||||
window.requestAnimationFrame(this.loop)
|
||||
}
|
||||
|
||||
setColor(fillStyle, strokeStyle) {
|
||||
this.fillStyle = fillStyle
|
||||
this.strokeStyle = strokeStyle
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.element.removeEventListener('mousemove', this.onMouseMove)
|
||||
this.element.removeEventListener('touchmove', this.onTouchMove, { passive: true })
|
||||
this.element.removeEventListener('touchstart', this.onTouchMove, { passive: true })
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
|
||||
if (this.hasWrapperEl) {
|
||||
this.element.removeChild(this.canvas)
|
||||
} else {
|
||||
document.body.removeChild(this.canvas)
|
||||
}
|
||||
|
||||
this.canvas = null
|
||||
this.context = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { EventEmitter } from 'events'
|
||||
import { performance } from 'perf_hooks'
|
||||
import { STATUS } from './util'
|
||||
import http from 'http'
|
||||
import { request, Options as RequestOptions } from './request'
|
||||
|
||||
export interface Options {
|
||||
forceResume: boolean
|
||||
requestOptions: RequestOptions
|
||||
}
|
||||
|
||||
const defaultChunkInfo = {
|
||||
path: '',
|
||||
startByte: '0',
|
||||
endByte: '',
|
||||
}
|
||||
|
||||
const defaultRequestOptions: Options['requestOptions'] = {
|
||||
method: 'get',
|
||||
headers: {},
|
||||
}
|
||||
const defaultOptions: Options = {
|
||||
forceResume: true,
|
||||
requestOptions: { ...defaultRequestOptions },
|
||||
}
|
||||
|
||||
class Task extends EventEmitter {
|
||||
resumeLastChunk: Buffer | null
|
||||
downloadUrl: string
|
||||
chunkInfo: { path: string, startByte: string, endByte: string }
|
||||
status: typeof STATUS[keyof typeof STATUS]
|
||||
options: Options
|
||||
requestOptions: Options['requestOptions']
|
||||
ws: fs.WriteStream | null = null
|
||||
progress = { total: 0, downloaded: 0, speed: 0, progress: 0 }
|
||||
statsEstimate = { time: 0, bytes: 0, prevBytes: 0 }
|
||||
requestInstance: http.ClientRequest | null = null
|
||||
|
||||
|
||||
constructor(url: string, savePath: string, filename: string, options: Partial<Options> = {}) {
|
||||
super()
|
||||
|
||||
this.resumeLastChunk = null
|
||||
this.downloadUrl = url
|
||||
this.chunkInfo = Object.assign({}, defaultChunkInfo, {
|
||||
path: path.join(savePath, filename),
|
||||
startByte: '0',
|
||||
})
|
||||
// if (!this.chunkInfo.endByte) this.chunkInfo.endByte = ''
|
||||
|
||||
this.options = Object.assign({}, defaultOptions, options)
|
||||
this.requestOptions = Object.assign({}, defaultRequestOptions, this.options.requestOptions || {})
|
||||
this.requestOptions.headers = this.requestOptions.headers ? { ...this.requestOptions.headers } : {}
|
||||
|
||||
this.status = STATUS.idle
|
||||
}
|
||||
|
||||
async __init() {
|
||||
const { path, startByte, endByte } = this.chunkInfo
|
||||
this.progress.downloaded = 0
|
||||
this.progress.progress = 0
|
||||
this.progress.speed = 0
|
||||
if (startByte) this.requestOptions.headers!.range = `bytes=${startByte}-${endByte}`
|
||||
|
||||
if (!path) return
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fs.stat(path, (errStat, stats) => {
|
||||
if (errStat) {
|
||||
// console.log(errStat.code)
|
||||
if (errStat.code !== 'ENOENT') {
|
||||
this.__handleError(errStat)
|
||||
reject(errStat)
|
||||
return
|
||||
}
|
||||
} else if (stats.size >= 10) {
|
||||
fs.open(path, 'r', (errOpen, fd) => {
|
||||
if (errOpen) {
|
||||
this.__handleError(errOpen)
|
||||
reject(errOpen)
|
||||
return
|
||||
}
|
||||
fs.read(fd, Buffer.alloc(10), 0, 10, stats.size - 10, (errRead, bytesRead, buffer) => {
|
||||
if (errRead) {
|
||||
this.__handleError(errRead)
|
||||
reject(errRead)
|
||||
return
|
||||
}
|
||||
fs.close(fd, errClose => {
|
||||
if (errClose) {
|
||||
this.__handleError(errClose)
|
||||
reject(errClose)
|
||||
return
|
||||
}
|
||||
|
||||
// resume download
|
||||
// console.log(buffer)
|
||||
this.resumeLastChunk = buffer
|
||||
this.progress.downloaded = stats.size
|
||||
this.requestOptions.headers!.range = `bytes=${stats.size - 10}-${endByte || ''}`
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
__httpFetch(url: string, options: Options['requestOptions']) {
|
||||
// console.log(options)
|
||||
this.requestInstance = request(url, options)
|
||||
.on('response', response => {
|
||||
if (response.statusCode !== 200 && response.statusCode !== 206) {
|
||||
if (response.statusCode == 416) {
|
||||
fs.unlink(this.chunkInfo.path, (err) => {
|
||||
this.__handleError(new Error(response.statusMessage))
|
||||
this.chunkInfo.startByte = '0'
|
||||
this.resumeLastChunk = null
|
||||
this.progress.downloaded = 0
|
||||
if (err) this.__handleError(err)
|
||||
})
|
||||
return
|
||||
}
|
||||
this.status = STATUS.failed
|
||||
this.emit('fail', response)
|
||||
this.__closeRequest()
|
||||
void this.__closeWriteStream()
|
||||
return
|
||||
}
|
||||
this.emit('response', response)
|
||||
try {
|
||||
this.__initDownload(response)
|
||||
} catch (error: any) {
|
||||
return this.__handleError(error)
|
||||
}
|
||||
this.status = STATUS.running
|
||||
response
|
||||
.on('data', this.__handleWriteData.bind(this))
|
||||
.on('error', err => this.__handleError(err))
|
||||
.on('end', () => {
|
||||
if (response.complete) {
|
||||
this.__handleComplete()
|
||||
} else {
|
||||
// this.__handleError(new Error('The connection was terminated while the message was still being sent'))
|
||||
void this.stop()
|
||||
}
|
||||
})
|
||||
})
|
||||
.on('error', err => this.__handleError(err))
|
||||
.on('close', () => {
|
||||
void this.__closeWriteStream()
|
||||
})
|
||||
.end()
|
||||
}
|
||||
|
||||
__initDownload(response: http.IncomingMessage) {
|
||||
this.progress.total = response.headers['content-length'] ? parseInt(response.headers['content-length']) : 0
|
||||
if (!this.progress.total) return this.__handleError(new Error('Content length is 0'))
|
||||
let options: any = {}
|
||||
let isResumable = this.options.forceResume ||
|
||||
response.headers['accept-ranges'] !== 'none' ||
|
||||
(typeof response.headers['accept-ranges'] == 'string' &&
|
||||
parseInt(response.headers['accept-ranges'].replace(/^bytes=(\d+)/, '$1')) > 0)
|
||||
|
||||
if (isResumable) {
|
||||
options.flags = 'a'
|
||||
if (this.progress.downloaded) this.progress.total -= 10
|
||||
} else {
|
||||
if (this.chunkInfo.startByte != '0') return this.__handleError(new Error('The resource cannot be resumed download.'))
|
||||
}
|
||||
this.progress.total += this.progress.downloaded
|
||||
this.statsEstimate.prevBytes = this.progress.downloaded
|
||||
if (!this.chunkInfo.path) return this.__handleError(new Error('Chunk save Path is not set.'))
|
||||
this.ws = fs.createWriteStream(this.chunkInfo.path, options)
|
||||
|
||||
this.ws.on('finish', () => {
|
||||
void this.__closeWriteStream()
|
||||
})
|
||||
this.ws.on('error', err => {
|
||||
fs.unlink(this.chunkInfo.path, (unlinkErr: any) => {
|
||||
this.__handleError(err)
|
||||
this.chunkInfo.startByte = '0'
|
||||
this.resumeLastChunk = null
|
||||
this.progress.downloaded = 0
|
||||
if (unlinkErr && unlinkErr.code !== 'ENOENT') this.__handleError(unlinkErr)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
__handleComplete() {
|
||||
if (this.status == STATUS.error) return
|
||||
void this.__closeWriteStream().then(() => {
|
||||
if (this.progress.downloaded == this.progress.total) {
|
||||
this.status = STATUS.completed
|
||||
this.emit('completed')
|
||||
} else {
|
||||
this.status = STATUS.stopped
|
||||
this.emit('stop')
|
||||
}
|
||||
})
|
||||
// console.log('end')
|
||||
}
|
||||
|
||||
__handleError(error: Error) {
|
||||
if (this.status == STATUS.error) return
|
||||
this.status = STATUS.error
|
||||
this.__closeRequest()
|
||||
void this.__closeWriteStream()
|
||||
if (error.message == 'aborted') return
|
||||
this.emit('error', error)
|
||||
}
|
||||
|
||||
async __closeWriteStream() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!this.ws) return resolve()
|
||||
// console.log('close write stream')
|
||||
this.ws.close(err => {
|
||||
if (err) {
|
||||
this.status = STATUS.error
|
||||
this.emit('error', err)
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
this.ws = null
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
__closeRequest() {
|
||||
if (!this.requestInstance || this.requestInstance.destroyed) return
|
||||
// console.log('close request')
|
||||
this.requestInstance.destroy()
|
||||
this.requestInstance = null
|
||||
}
|
||||
|
||||
__handleWriteData(chunk: Buffer) {
|
||||
if (this.resumeLastChunk) {
|
||||
const result = this.__handleDiffChunk(chunk)
|
||||
if (result) chunk = result
|
||||
else {
|
||||
this.__handleStop().finally(() => {
|
||||
// this.__handleError(new Error('Resume failed, response chunk does not match.'))
|
||||
// Resume failed, response chunk does not match, remove file and restart download
|
||||
console.log('Resume failed, response chunk does not match.')
|
||||
fs.unlink(this.chunkInfo.path, (unlinkErr: any) => {
|
||||
// this.__handleError(err)
|
||||
this.chunkInfo.startByte = '0'
|
||||
this.resumeLastChunk = null
|
||||
if (unlinkErr && unlinkErr.code !== 'ENOENT') return this.__handleError(unlinkErr)
|
||||
void this.start()
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
// console.log('data', chunk)
|
||||
if (this.status == STATUS.stopped || this.ws == null) return console.log('cancel write')
|
||||
this.__calculateProgress(chunk.length)
|
||||
this.ws.write(chunk, err => {
|
||||
if (!err) return
|
||||
console.log(err)
|
||||
this.__handleError(err)
|
||||
void this.stop()
|
||||
})
|
||||
}
|
||||
|
||||
__handleDiffChunk(chunk: Buffer): Buffer | null {
|
||||
// console.log('diff', chunk)
|
||||
let resumeLastChunkLen = this.resumeLastChunk!.length
|
||||
let chunkLen = chunk.length
|
||||
let isOk
|
||||
if (chunkLen >= resumeLastChunkLen) {
|
||||
isOk = chunk.slice(0, resumeLastChunkLen).toString('hex') === this.resumeLastChunk!.toString('hex')
|
||||
if (!isOk) return null
|
||||
|
||||
this.resumeLastChunk = null
|
||||
return chunk.slice(resumeLastChunkLen)
|
||||
} else {
|
||||
isOk = chunk.slice(0, chunkLen).toString('hex') === this.resumeLastChunk!.slice(0, chunkLen).toString('hex')
|
||||
if (!isOk) return null
|
||||
this.resumeLastChunk = this.resumeLastChunk!.slice(chunkLen)
|
||||
return chunk.slice(chunkLen)
|
||||
}
|
||||
}
|
||||
|
||||
async __handleStop() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.__closeRequest()
|
||||
if (this.ws) {
|
||||
this.ws.close(err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
this.emit('error', err)
|
||||
return
|
||||
}
|
||||
this.ws = null
|
||||
resolve()
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
__calculateProgress(receivedBytes: number) {
|
||||
const currentTime = performance.now()
|
||||
const elaspsedTime = currentTime - this.statsEstimate.time
|
||||
|
||||
const progress = this.progress
|
||||
progress.downloaded += receivedBytes
|
||||
progress.progress = progress.total ? (progress.downloaded / progress.total) * 100 : -1
|
||||
|
||||
|
||||
// emit the progress every second or if finished
|
||||
if (progress.downloaded === progress.total || elaspsedTime > 1000) {
|
||||
this.statsEstimate.time = currentTime
|
||||
this.statsEstimate.bytes = progress.downloaded - this.statsEstimate.prevBytes
|
||||
this.statsEstimate.prevBytes = progress.downloaded
|
||||
this.emit('progress', {
|
||||
total: progress.total,
|
||||
downloaded: progress.downloaded,
|
||||
progress: progress.progress,
|
||||
speed: this.statsEstimate.bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.status = STATUS.init
|
||||
await this.__init()
|
||||
if (this.status !== STATUS.init) return
|
||||
this.status = STATUS.running
|
||||
this.__httpFetch(this.downloadUrl, this.requestOptions)
|
||||
this.emit('start')
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.status == STATUS.stopped || this.status == STATUS.completed) return
|
||||
this.status = STATUS.stopped
|
||||
await this.__handleStop()
|
||||
this.emit('stop')
|
||||
}
|
||||
|
||||
refreshUrl(url: string) {
|
||||
this.downloadUrl = url
|
||||
}
|
||||
|
||||
updateSaveInfo(filePath: string, fileName: string) {
|
||||
this.chunkInfo.path = path.join(filePath, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
export default Task
|
||||
@@ -0,0 +1,100 @@
|
||||
import Downloader, { Options as DownloaderOptions } from './Downloader'
|
||||
import { getRequestAgent } from './util'
|
||||
import { sizeFormate } from '@common/utils'
|
||||
import http from 'http'
|
||||
|
||||
// these are the default options
|
||||
// const options = {
|
||||
// method: 'GET', // Request Method Verb
|
||||
// // Custom HTTP Header ex: Authorization, User-Agent
|
||||
// headers: {},
|
||||
// fileName: '', // Custom filename when saved
|
||||
// override: false, // if true it will override the file, otherwise will append '(number)' to the end of file
|
||||
// forceResume: false, // If the server does not return the "accept-ranges" header, can be force if it does support it
|
||||
// // httpRequestOptions: {}, // Override the http request options
|
||||
// // httpsRequestOptions: {}, // Override the https request options, ex: to add SSL Certs
|
||||
// }
|
||||
|
||||
export interface Options {
|
||||
url: string
|
||||
path: string
|
||||
fileName: string
|
||||
method?: DownloaderOptions['requestOptions']['method']
|
||||
headers?: DownloaderOptions['requestOptions']['headers']
|
||||
forceResume?: boolean
|
||||
proxy?: { host: string, port: number }
|
||||
onCompleted?: () => void
|
||||
onError?: (error: Error) => void
|
||||
onFail?: (response: http.IncomingMessage) => void
|
||||
onStart?: () => void
|
||||
onStop?: () => void
|
||||
onProgress?: (progress: LX.Download.ProgressInfo) => void
|
||||
}
|
||||
const noop = () => {}
|
||||
export const createDownload = ({
|
||||
url,
|
||||
path,
|
||||
fileName,
|
||||
method = 'get',
|
||||
forceResume,
|
||||
proxy,
|
||||
// resumeTime = 5000,
|
||||
onCompleted = noop,
|
||||
onError = noop,
|
||||
onFail = noop,
|
||||
onStart = noop,
|
||||
onStop = noop,
|
||||
onProgress = noop,
|
||||
}: Options) => {
|
||||
const dl = new Downloader(url, path, fileName, {
|
||||
requestOptions: {
|
||||
method,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
|
||||
},
|
||||
agent: getRequestAgent(url, proxy),
|
||||
timeout: 60 * 1000,
|
||||
},
|
||||
|
||||
forceResume,
|
||||
})
|
||||
|
||||
dl.on('completed', () => {
|
||||
onCompleted()
|
||||
}).on('error', (err: any) => {
|
||||
if (err.message === 'socket hang up') return
|
||||
onError(err)
|
||||
}).on('start', () => {
|
||||
onStart()
|
||||
// pauseResumeTimer(dl, resumeTime)
|
||||
}).on('progress', (stats) => {
|
||||
const speed = sizeFormate(stats.speed)
|
||||
onProgress({
|
||||
progress: parseInt(stats.progress.toFixed(2)),
|
||||
speed,
|
||||
downloaded: stats.downloaded,
|
||||
total: stats.total,
|
||||
})
|
||||
// if (debugDownload) {
|
||||
// const downloaded = sizeFormate(stats.downloaded)
|
||||
// const total = sizeFormate(stats.total)
|
||||
// console.log(`${speed}/s - ${progress}% [${downloaded}/${total}]`)
|
||||
// }
|
||||
}).on('stop', () => {
|
||||
onStop()
|
||||
// debugDownload && console.log('paused')
|
||||
}).on('fail', resp => {
|
||||
onFail(resp)
|
||||
// debugDownload && console.log('fail')
|
||||
})
|
||||
|
||||
// debugDownload && console.log('Downloading: ', url)
|
||||
|
||||
dl.start().catch(err => {
|
||||
onError(err)
|
||||
})
|
||||
|
||||
return dl
|
||||
}
|
||||
|
||||
export type DownloaderType = Downloader
|
||||
@@ -0,0 +1,73 @@
|
||||
import { URL } from 'url'
|
||||
import http from 'http'
|
||||
import https from 'https'
|
||||
|
||||
export interface Options {
|
||||
method: 'get' | 'head' | 'delete' | 'patch' | 'post' | 'put'
|
||||
params?: Record<string, string>
|
||||
// body?: Record<string, string>
|
||||
headers?: Record<string, string>
|
||||
timeout?: number
|
||||
agent?: http.Agent
|
||||
}
|
||||
|
||||
const defaultOptions: Options = {
|
||||
method: 'get',
|
||||
}
|
||||
|
||||
type HttpCallback = (res: http.IncomingMessage) => void
|
||||
|
||||
const sendRequest = (url: string, options: Options, callback?: HttpCallback) => {
|
||||
const urlParse = new URL(url)
|
||||
const httpOptions: http.RequestOptions | https.RequestOptions = {
|
||||
host: urlParse.hostname,
|
||||
port: urlParse.port,
|
||||
path: urlParse.pathname + urlParse.search,
|
||||
method: options.method,
|
||||
}
|
||||
|
||||
if (options.params) {
|
||||
(httpOptions.path as string) += `${urlParse.search ? '&' : '?'}${Object.entries(options.params)
|
||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
.join('&')}`
|
||||
}
|
||||
|
||||
if (options.headers) httpOptions.headers = { ...options.headers }
|
||||
|
||||
if (options.agent) httpOptions.agent = options.agent
|
||||
|
||||
return urlParse.protocol == 'https:'
|
||||
? https.request(httpOptions, callback)
|
||||
: http.request(httpOptions, callback)
|
||||
}
|
||||
|
||||
const applyTimeout = (request: http.ClientRequest, time: number) => {
|
||||
let timeout: NodeJS.Timeout | null = setTimeout(() => {
|
||||
timeout = null
|
||||
if (request.destroyed) return
|
||||
request.destroy(new Error('Request timeout'))
|
||||
}, time)
|
||||
request.on('response', () => {
|
||||
if (!timeout) return
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
})
|
||||
}
|
||||
|
||||
// const isRequireRedirect = (response: http.IncomingMessage) => {
|
||||
// return response.statusCode &&
|
||||
// response.statusCode > 300 &&
|
||||
// response.statusCode < 400 &&
|
||||
// Object.hasOwn(response.headers, 'location') &&
|
||||
// response.headers.location
|
||||
// }
|
||||
|
||||
// export function request(url: string, callback: HttpCallback)
|
||||
// export function request(url: string, options: Partial<Options>, callback: HttpCallback)
|
||||
export function request(url: string, _options: Partial<Options>, callback?: HttpCallback) {
|
||||
let options: Options = { ...defaultOptions, ..._options }
|
||||
const request = sendRequest(url, options, callback)
|
||||
if (options.timeout) applyTimeout(request, options.timeout)
|
||||
return request
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { httpOverHttp, httpsOverHttp } from 'tunnel'
|
||||
|
||||
export const STATUS = {
|
||||
idle: 'IDLE',
|
||||
init: 'INIT',
|
||||
running: 'RUNNING',
|
||||
paused: 'PAUSED',
|
||||
stopped: 'STOPPED',
|
||||
completed: 'COMPLETED',
|
||||
error: 'ERROR',
|
||||
failed: 'FAILED',
|
||||
} as const
|
||||
|
||||
const httpsRxp = /^https:/
|
||||
export const getRequestAgent = (url: string, proxy?: { host: string, port: number }) => {
|
||||
let options
|
||||
if (proxy) {
|
||||
options = {
|
||||
proxy: {
|
||||
host: proxy.host,
|
||||
port: proxy.port,
|
||||
},
|
||||
}
|
||||
}
|
||||
return options ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)(options) : undefined
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { shell, clipboard } from 'electron'
|
||||
|
||||
|
||||
/**
|
||||
* 在资源管理器中打开目录
|
||||
* @param {string} dir
|
||||
*/
|
||||
export const openDirInExplorer = (dir: string) => {
|
||||
shell.showItemInFolder(dir)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 在浏览器打开URL
|
||||
* @param {*} url
|
||||
*/
|
||||
export const openUrl = async(url: string) => {
|
||||
if (!/^https?:\/\//.test(url)) return
|
||||
await shell.openExternal(url)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
* @param str
|
||||
*/
|
||||
export const clipboardWriteText = (str: string) => {
|
||||
clipboard.writeText(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从剪贴板读取文本
|
||||
* @returns
|
||||
*/
|
||||
export const clipboardReadText = (): string => {
|
||||
return clipboard.readText()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import log from 'electron-log'
|
||||
|
||||
log.transports.file.level = 'info'
|
||||
|
||||
export const isLinux = process.platform == 'linux'
|
||||
export const isWin = process.platform == 'win32'
|
||||
export const isMac = process.platform == 'darwin'
|
||||
export const isProd = process.env.NODE_ENV == 'production'
|
||||
|
||||
|
||||
// https://stackoverflow.com/a/53387532
|
||||
export function compareVer(currentVer: string, targetVer: string): -1 | 0 | 1 {
|
||||
// treat non-numerical characters as lower version
|
||||
// replacing them with a negative number based on charcode of each character
|
||||
const fix = (s: string) => `.${s.toLowerCase().charCodeAt(0) - 2147483647}.`
|
||||
|
||||
const currentVerArr: Array<string | number> = ('' + currentVer).replace(/[^0-9.]/g, fix).split('.')
|
||||
const targetVerArr: Array<string | number> = ('' + targetVer).replace(/[^0-9.]/g, fix).split('.')
|
||||
let c = Math.max(currentVerArr.length, targetVerArr.length)
|
||||
for (let i = 0; i < c; i++) {
|
||||
// convert to integer the most efficient way
|
||||
currentVerArr[i] = ~~currentVerArr[i]
|
||||
targetVerArr[i] = ~~targetVerArr[i]
|
||||
if (currentVerArr[i] > targetVerArr[i]) return 1
|
||||
else if (currentVerArr[i] < targetVerArr[i]) return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export const encodePath = (path: string) => {
|
||||
// https://github.com/lyswhut/lx-music-desktop/issues/963
|
||||
return path.replaceAll('%', '%25')
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
log,
|
||||
}
|
||||
|
||||
export * from './common'
|
||||
@@ -0,0 +1,356 @@
|
||||
const { getNow, TimeoutTools } = require('./utils')
|
||||
|
||||
// const fontFormateRxp = /(?=<\d+,\d+>).*?/g
|
||||
const fontSplitRxp = /(?=<\d+,\d+>).*?/g
|
||||
const timeRxpAll = /<(\d+),(\d+)>/g
|
||||
const timeRxp = /<(\d+),(\d+)>/
|
||||
|
||||
|
||||
// Create animation
|
||||
const createAnimation = (dom, duration, isVertical) => new window.Animation(new window.KeyframeEffect(dom, isVertical
|
||||
? [
|
||||
{ backgroundSize: '100% 0' },
|
||||
{ backgroundSize: '100% 100%' },
|
||||
]
|
||||
: [
|
||||
{ backgroundSize: '0 100%' },
|
||||
{ backgroundSize: '100% 100%' },
|
||||
], {
|
||||
duration,
|
||||
easing: 'linear',
|
||||
},
|
||||
), document.timeline)
|
||||
|
||||
|
||||
// https://jsfiddle.net/ceqpnbky/
|
||||
// https://jsfiddle.net/ceqpnbky/1/
|
||||
|
||||
module.exports = class FontPlayer {
|
||||
constructor({
|
||||
time = 0,
|
||||
lyric = '',
|
||||
lineContentClassName = 'line-content',
|
||||
lineClassName = 'line',
|
||||
shadowClassName = 'shadow',
|
||||
fontModeClassName = 'font-mode',
|
||||
lineModeClassName = 'line-mode',
|
||||
fontLrcClassName = 'font-lrc',
|
||||
extendedLrcClassName = 'extended',
|
||||
shadowContent = false,
|
||||
extendedLyrics = [],
|
||||
isVertical = false,
|
||||
}) {
|
||||
this.time = time
|
||||
this.lyric = lyric
|
||||
|
||||
this.isVertical = isVertical
|
||||
|
||||
this.lineContentClassName = lineContentClassName
|
||||
this.lineClassName = lineClassName
|
||||
|
||||
this.shadowContent = shadowContent
|
||||
this.shadowClassName = shadowClassName
|
||||
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this.fontModeClassName = fontModeClassName
|
||||
this.fontLrcClassName = fontLrcClassName
|
||||
this.extendedLrcClassName = extendedLrcClassName
|
||||
this.lineModeClassName = lineModeClassName
|
||||
|
||||
|
||||
this.isPlay = false
|
||||
this.curFontNum = 0
|
||||
this.maxFontNum = 0
|
||||
this._performanceTime = 0
|
||||
this._startTime = 0
|
||||
|
||||
this.lineContent = null
|
||||
|
||||
this.timeoutTools = new TimeoutTools(80)
|
||||
this.waitPlayTimeout = new TimeoutTools(80)
|
||||
|
||||
this._init()
|
||||
}
|
||||
|
||||
_init() {
|
||||
if (this.lyric == null) this.lyric = ''
|
||||
|
||||
this.isLineMode = false
|
||||
|
||||
this.lineContent = document.createElement('div')
|
||||
this.lineContent.time = this.time
|
||||
this.lineContent.className = this.lineContentClassName
|
||||
|
||||
this.line = document.createElement('div')
|
||||
this.line.style = 'position:relative;display:inline-block;'
|
||||
this.line.className = this.lineClassName
|
||||
this.lineContent.appendChild(this.line)
|
||||
|
||||
this.lrcContent = document.createElement('div')
|
||||
this.lrcContent.className = this.fontLrcClassName
|
||||
// if (this.shadowContent) {
|
||||
// this.lrcShadowContent = document.createElement('div')
|
||||
// this.lrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
|
||||
// this.lrcShadowContent.className = this.shadowClassName
|
||||
// this.line.appendChild(this.lrcShadowContent)
|
||||
// }
|
||||
this.line.appendChild(this.lrcContent)
|
||||
|
||||
for (const lrc of this.extendedLyrics) {
|
||||
const extendedLrcContent = document.createElement('div')
|
||||
extendedLrcContent.style = 'position:relative;display:inline-block;'
|
||||
extendedLrcContent.className = this.extendedLrcClassName
|
||||
this.lineContent.appendChild(document.createElement('br'))
|
||||
this.lineContent.appendChild(extendedLrcContent)
|
||||
|
||||
|
||||
// if (this.shadowContent) {
|
||||
// const extendedLrcShadowContent = document.createElement('div')
|
||||
// extendedLrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
|
||||
// extendedLrcShadowContent.className = this.shadowClassName
|
||||
// extendedLrcShadowContent.textContent = lrc
|
||||
// extendedLrcContent.appendChild(extendedLrcShadowContent)
|
||||
// }
|
||||
|
||||
const lineContent = document.createElement('div')
|
||||
lineContent.className = this.fontLrcClassName
|
||||
lineContent.textContent = lrc.replace(timeRxpAll, '')
|
||||
extendedLrcContent.appendChild(lineContent)
|
||||
}
|
||||
this._parseLyric()
|
||||
}
|
||||
|
||||
_parseLyric() {
|
||||
const fonts = this.lyric.split(fontSplitRxp)
|
||||
// console.log(fonts)
|
||||
|
||||
this.maxFontNum = fonts.length - 1
|
||||
this.fonts = []
|
||||
let text
|
||||
// let lineText = ''
|
||||
let lrcShadowContent
|
||||
for (const font of fonts) {
|
||||
text = font.replace(timeRxp, '')
|
||||
if (RegExp.$2 == '') return this._handleLineParse()
|
||||
const time = parseInt(RegExp.$2)
|
||||
|
||||
const dom = document.createElement('span')
|
||||
dom.textContent = text
|
||||
const animation = createAnimation(dom, time, this.isVertical)
|
||||
this.lrcContent.appendChild(dom)
|
||||
// lineText += text
|
||||
|
||||
if (this.shadowContent) {
|
||||
if (!lrcShadowContent) lrcShadowContent = document.createElement('div')
|
||||
const shadowDom = document.createElement('span')
|
||||
shadowDom.textContent = text
|
||||
lrcShadowContent.appendChild(shadowDom)
|
||||
}
|
||||
// dom.style = shadowDom.style = this.fontStyle
|
||||
// dom.className = shadowDom.className = this.fontClassName
|
||||
|
||||
this.fonts.push({
|
||||
text,
|
||||
startTime: parseInt(RegExp.$1),
|
||||
time,
|
||||
dom,
|
||||
animation,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.shadowContent && lrcShadowContent) {
|
||||
lrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
|
||||
lrcShadowContent.className = this.shadowClassName
|
||||
this.line.appendChild(lrcShadowContent)
|
||||
}
|
||||
|
||||
this.line.appendChild(this.lrcContent)
|
||||
this.fonts.at(-1)?.animation.addEventListener('finish', () => {
|
||||
this.lineContent.classList.add('played')
|
||||
this.isPlay = false
|
||||
})
|
||||
this.lineContent.classList.add(this.fontModeClassName)
|
||||
// if (this.shadowContent) this.lrcShadowContent.textContent = lineText
|
||||
// console.log(this.fonts)
|
||||
}
|
||||
|
||||
_handleLineParse() {
|
||||
this.isLineMode = true
|
||||
this.lineContent.classList.add(this.lineModeClassName)
|
||||
this.lrcContent.textContent = this.lyric
|
||||
|
||||
// if (this.shadowContent) this.lrcShadowContent.textContent = this.lyric
|
||||
this.fonts.push({
|
||||
text: this.lyric,
|
||||
})
|
||||
}
|
||||
|
||||
_currentTime() {
|
||||
return getNow() - this._performanceTime + this._startTime
|
||||
}
|
||||
|
||||
_findcurFontNum(curTime, startIndex = 0) {
|
||||
const length = this.fonts.length
|
||||
for (let index = startIndex; index < length; index++) if (curTime < this.fonts[index].startTime) return index == 0 ? 0 : index - 1
|
||||
return length - 1
|
||||
}
|
||||
|
||||
_handlePlayMaxFontNum() {
|
||||
let curFont = this.fonts[this.curFontNum]
|
||||
// console.log(curFont.text)
|
||||
const currentTime = this._currentTime()
|
||||
const driftTime = currentTime - curFont.startTime
|
||||
if (currentTime > curFont.startTime + curFont.time) {
|
||||
this._handlePlayFont(curFont, driftTime, true)
|
||||
this.lineContent.classList.add('played')
|
||||
this.isPlay = false
|
||||
this.pause()
|
||||
} else {
|
||||
this._handlePlayFont(curFont, driftTime)
|
||||
}
|
||||
}
|
||||
|
||||
_handlePlayFont(font, currentTime, toFinishe) {
|
||||
switch (font.animation.playState) {
|
||||
case 'finished':
|
||||
break
|
||||
case 'idle':
|
||||
font.dom.style.backgroundSize = '100% 100%'
|
||||
if (!toFinishe) font.animation.play()
|
||||
break
|
||||
default:
|
||||
if (toFinishe) {
|
||||
font.animation.cancel()
|
||||
} else {
|
||||
font.animation.currentTime = currentTime
|
||||
font.animation.play()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_handlePlayLine(isPlayed) {
|
||||
this.isPlay = false
|
||||
if (isPlayed) {
|
||||
this.lineContent.classList.add('played')
|
||||
} else {
|
||||
this.lineContent.classList.remove('played')
|
||||
}
|
||||
// this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0'
|
||||
}
|
||||
|
||||
_handlePauseFont(font) {
|
||||
if (font.animation.playState == 'running') font.animation.pause()
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.curFontNum++
|
||||
// console.log('curFontNum time', this.fonts[this.curFontNum].time)
|
||||
if (this.curFontNum >= this.maxFontNum) return this._handlePlayMaxFontNum()
|
||||
let curFont = this.fonts[this.curFontNum]
|
||||
// console.log(curFont, nextFont, this.curFontNum, this.maxFontNum)
|
||||
const currentTime = this._currentTime()
|
||||
// console.log(curFont.text)
|
||||
const driftTime = currentTime - curFont.startTime
|
||||
|
||||
// console.log(currentTime, driftTime)
|
||||
|
||||
if (driftTime >= 0 || this.curFontNum == 0) {
|
||||
let nextFont = this.fonts[this.curFontNum + 1]
|
||||
this.delay = nextFont.startTime - curFont.startTime - driftTime
|
||||
if (this.delay > 0) {
|
||||
if (this.isPlay) {
|
||||
this.timeoutTools.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, this.delay)
|
||||
}
|
||||
this._handlePlayFont(curFont, driftTime)
|
||||
return
|
||||
} else {
|
||||
let newCurLineNum = this._findcurFontNum(currentTime, this.curFontNum + 1)
|
||||
if (newCurLineNum > this.curFontNum) this.curFontNum = newCurLineNum - 1
|
||||
for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
|
||||
this._refresh()
|
||||
return
|
||||
}
|
||||
} else if (this.curFontNum == 0) {
|
||||
this.curFontNum--
|
||||
if (this.isPlay) {
|
||||
this.waitPlayTimeout.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, -driftTime)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.curFontNum = this._findcurFontNum(currentTime, this.curFontNum) - 1
|
||||
for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
|
||||
// this.curFontNum--
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
play(curTime = 0) {
|
||||
// console.log('play', curTime)
|
||||
if (!this.fonts.length) return
|
||||
this.pause()
|
||||
|
||||
if (this.isLineMode) return this._handlePlayLine(true)
|
||||
this.lineContent.classList.remove('played')
|
||||
this.isPlay = true
|
||||
this._performanceTime = getNow()
|
||||
this._startTime = curTime
|
||||
|
||||
this.curFontNum = this._findcurFontNum(curTime)
|
||||
|
||||
for (let i = this.curFontNum; i > -1; i--) {
|
||||
this._handlePlayFont(this.fonts[i], 0, true)
|
||||
}
|
||||
for (let i = this.curFontNum, len = this.fonts.length; i < len; i++) {
|
||||
let font = this.fonts[i]
|
||||
font.animation.cancel()
|
||||
font.dom.style.backgroundSize = '0 100%'
|
||||
}
|
||||
|
||||
this.curFontNum--
|
||||
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.isPlay) return
|
||||
this.isPlay = false
|
||||
this.timeoutTools.clear()
|
||||
this.waitPlayTimeout.clear()
|
||||
this._handlePauseFont(this.fonts[this.curFontNum])
|
||||
if (this.curFontNum === this.maxLine) return
|
||||
const curFontNum = this._findcurFontNum(this._currentTime())
|
||||
if (this.curFontNum === curFontNum) return
|
||||
for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.pause()
|
||||
if (this.isLineMode) return this._handlePlayLine(true)
|
||||
this.lineContent.classList.add('played')
|
||||
|
||||
for (const font of this.fonts) {
|
||||
font.animation.cancel()
|
||||
font.dom.style.backgroundSize = '100% 100%'
|
||||
}
|
||||
this.curFontNum = this.maxFontNum
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.pause()
|
||||
if (this.isLineMode) return this._handlePlayLine(false)
|
||||
this.lineContent.classList.remove('played')
|
||||
for (const font of this.fonts) {
|
||||
font.animation.cancel()
|
||||
font.dom.style.backgroundSize = '0 100%'
|
||||
}
|
||||
this.curFontNum = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
const LinePlayer = require('./line-player')
|
||||
const FontPlayer = require('./font-player')
|
||||
|
||||
const fontTimeExp = /<(\d+),(\d+)>/g
|
||||
|
||||
module.exports = class Lyric {
|
||||
constructor({
|
||||
lyric = '',
|
||||
extendedLyrics = [],
|
||||
offset = 0,
|
||||
lineContentClassName = 'line-content',
|
||||
lineClassName = 'line',
|
||||
shadowClassName = 'shadow',
|
||||
fontModeClassName = 'font-mode',
|
||||
lineModeClassName = 'line-mode',
|
||||
fontLrcClassName = 'font-lrc',
|
||||
extendedLrcClassName = 'extended',
|
||||
activeLineClassName = 'active',
|
||||
shadowContent = false,
|
||||
isVertical = false,
|
||||
onPlay = function(line, text) { },
|
||||
onSetLyric = function(lines, offset) { },
|
||||
onUpdateLyric = function(lines) { },
|
||||
}) {
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this.offset = offset
|
||||
this.onPlay = onPlay
|
||||
this.onSetLyric = onSetLyric
|
||||
this.onUpdateLyric = onUpdateLyric
|
||||
|
||||
this.lineContentClassName = lineContentClassName
|
||||
this.lineClassName = lineClassName
|
||||
this.shadowClassName = shadowClassName
|
||||
this.fontModeClassName = fontModeClassName
|
||||
this.lineModeClassName = lineModeClassName
|
||||
this.fontLrcClassName = fontLrcClassName
|
||||
this.extendedLrcClassName = extendedLrcClassName
|
||||
this.activeLineClassName = activeLineClassName
|
||||
this.shadowContent = shadowContent
|
||||
|
||||
this.isVertical = isVertical
|
||||
this.playingLineNum = -1
|
||||
this.isLineMode = false
|
||||
|
||||
this.initInfo = {
|
||||
lines: [],
|
||||
offset: 0,
|
||||
}
|
||||
|
||||
this.linePlayer = new LinePlayer({
|
||||
offset: this.offset,
|
||||
onPlay: this._handleLinePlayerOnPlay,
|
||||
onSetLyric: this._handleLinePlayerOnSetLyric,
|
||||
})
|
||||
}
|
||||
|
||||
_init() {
|
||||
this.playingLineNum = -1
|
||||
this.isLineMode = false
|
||||
|
||||
this.linePlayer.setLyric(this.lyric, this.extendedLyrics)
|
||||
}
|
||||
|
||||
_handleLinePlayerOnPlay = (num, text, curTime) => {
|
||||
if (this.isLineMode) {
|
||||
if (num < this.playingLineNum + 1) {
|
||||
for (let i = this.playingLineNum; i > num - 1; i--) {
|
||||
const font = this._lineFonts[i]
|
||||
font.reset()
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
} else if (num > this.playingLineNum) {
|
||||
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
|
||||
const font = this._lineFonts[i]
|
||||
font.reset()
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
} else if (this.playingLineNum > -1) {
|
||||
const font = this._lineFonts[this.playingLineNum]
|
||||
font.reset()
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
} else {
|
||||
if (num < this.playingLineNum + 1) {
|
||||
for (let i = this.playingLineNum; i > num - 1; i--) {
|
||||
const font = this._lineFonts[i]
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
font.reset()
|
||||
}
|
||||
} else if (num > this.playingLineNum) {
|
||||
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
|
||||
const font = this._lineFonts[i]
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
font.finish()
|
||||
}
|
||||
} else if (this.playingLineNum > -1) {
|
||||
const font = this._lineFonts[this.playingLineNum]
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
}
|
||||
this.playingLineNum = num
|
||||
const font = this._lineFonts[num]
|
||||
font.lineContent.classList.add(this.activeLineClassName)
|
||||
font.play(curTime - this._lines[num].time)
|
||||
this.onPlay(num, this._lines[num].text)
|
||||
}
|
||||
|
||||
_initLines = (lyricLines, offset, isUpdate) => {
|
||||
// console.log(lyricLines)
|
||||
// this._lines = lyricsLines
|
||||
this.isLineMode = lyricLines.length && !/^<\d+,\d+>/.test(lyricLines[0].text)
|
||||
|
||||
this._lineFonts = []
|
||||
if (this.isLineMode) {
|
||||
this._lines = lyricLines.map(line => {
|
||||
const fontPlayer = new FontPlayer({
|
||||
time: line.time,
|
||||
lyric: line.text,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
lineContentClassName: this.lineContentClassName,
|
||||
lineClassName: this.lineClassName,
|
||||
shadowClassName: this.shadowClassName,
|
||||
fontModeClassName: this.fontModeClassName,
|
||||
lineModeClassName: this.lineModeClassName,
|
||||
fontLrcClassName: this.fontLrcClassName,
|
||||
extendedLrcClassName: this.extendedLrcClassName,
|
||||
shadowContent: this.shadowContent,
|
||||
isVertical: this.isVertical,
|
||||
})
|
||||
|
||||
this._lineFonts.push(fontPlayer)
|
||||
return {
|
||||
text: line.text,
|
||||
time: line.time,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
dom_line: fontPlayer.lineContent,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this._lines = lyricLines.map(line => {
|
||||
const fontPlayer = new FontPlayer({
|
||||
time: line.time,
|
||||
lyric: line.text,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
lineContentClassName: this.lineContentClassName,
|
||||
lineClassName: this.lineClassName,
|
||||
shadowClassName: this.shadowClassName,
|
||||
fontModeClassName: this.fontModeClassName,
|
||||
lineModeClassName: this.lineModeClassName,
|
||||
fontLrcClassName: this.fontLrcClassName,
|
||||
extendedLrcClassName: this.extendedLrcClassName,
|
||||
shadowContent: this.shadowContent,
|
||||
isVertical: this.isVertical,
|
||||
})
|
||||
|
||||
this._lineFonts.push(fontPlayer)
|
||||
return {
|
||||
text: line.text.replace(fontTimeExp, ''),
|
||||
time: line.time,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
dom_line: fontPlayer.lineContent,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 如果是逐行歌词,则添加 60ms 的偏移
|
||||
let newOffset = this.isLineMode ? this.offset + 60 : this.offset
|
||||
offset = offset - this.linePlayer.offset + newOffset
|
||||
this.linePlayer.offset = newOffset
|
||||
if (isUpdate) this.onUpdateLyric(this._lines)
|
||||
else this.onSetLyric(this._lines, offset)
|
||||
}
|
||||
|
||||
_handleLinePlayerOnSetLyric = (lyricLines, offset) => {
|
||||
this._initLines(lyricLines, offset, false)
|
||||
this.playingLineNum = 0
|
||||
this.initInfo.lines = lyricLines
|
||||
this.initInfo.offset = offset
|
||||
}
|
||||
|
||||
play(curTime) {
|
||||
if (!this.linePlayer) return
|
||||
this.linePlayer.play(curTime)
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.linePlayer) return
|
||||
this.linePlayer.pause()
|
||||
if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum]?.pause()
|
||||
}
|
||||
|
||||
setOffset(offset) {
|
||||
this.linePlayer.offset = offset
|
||||
}
|
||||
|
||||
setLyric(lyric, extendedLyrics) {
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this._init()
|
||||
}
|
||||
|
||||
setVertical(isVertical) {
|
||||
this.isVertical = isVertical
|
||||
this._initLines(this.initInfo.lines, this.initInfo.offset, true)
|
||||
if (this.linePlayer.isPlay) {
|
||||
const num = this.playingLineNum
|
||||
this.playingLineNum = 0
|
||||
this._handleLinePlayerOnPlay(num, '', this.linePlayer._currentTime())
|
||||
} else this.playingLineNum = 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
const { getNow, TimeoutTools } = require('./utils')
|
||||
|
||||
const timeFieldExp = /^(?:\[[\d:.]+\])+/g
|
||||
const timeExp = /[\d:.]+/g
|
||||
const tagRegMap = {
|
||||
title: 'ti',
|
||||
artist: 'ar',
|
||||
album: 'al',
|
||||
offset: 'offset',
|
||||
by: 'by',
|
||||
}
|
||||
|
||||
const timeoutTools = new TimeoutTools()
|
||||
|
||||
const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
|
||||
const extendedLines = extendedLyric.split(/\r\n|\n|\r/)
|
||||
for (let i = 0; i < extendedLines.length; i++) {
|
||||
const line = extendedLines[i].trim()
|
||||
let result = timeFieldExp.exec(line)
|
||||
if (result) {
|
||||
const timeField = result[0]
|
||||
const text = line.replace(timeFieldExp, '').trim()
|
||||
if (text) {
|
||||
const times = timeField.match(timeExp)
|
||||
if (times == null) continue
|
||||
for (const time of times) {
|
||||
const timeStr = time.replace(/(\.\d\d)0$/, '$1')
|
||||
const targetLine = lrcLinesMap[timeStr]
|
||||
if (targetLine) targetLine.extendedLyrics.push(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = class LinePlayer {
|
||||
constructor({ offset = 0, onPlay = function() { }, onSetLyric = function() { } } = {}) {
|
||||
this.tags = {}
|
||||
this.lines = null
|
||||
this.onPlay = onPlay
|
||||
this.onSetLyric = onSetLyric
|
||||
this.isPlay = false
|
||||
this.curLineNum = 0
|
||||
this.maxLine = 0
|
||||
this.offset = offset
|
||||
this._performanceTime = 0
|
||||
this._startTime = 0
|
||||
}
|
||||
|
||||
_init() {
|
||||
if (this.lyric == null) this.lyric = ''
|
||||
if (this.extendedLyrics == null) this.extendedLyrics = []
|
||||
this._initTag()
|
||||
this._initLines()
|
||||
this.onSetLyric(this.lines, this.tags.offset + this.offset)
|
||||
}
|
||||
|
||||
_initTag() {
|
||||
this.tags = {}
|
||||
for (let tag in tagRegMap) {
|
||||
const matches = this.lyric.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i'))
|
||||
this.tags[tag] = (matches && matches[1]) || ''
|
||||
}
|
||||
if (this.tags.offset) {
|
||||
let offset = parseInt(this.tags.offset)
|
||||
this.tags.offset = Number.isNaN(offset) ? 0 : offset
|
||||
} else {
|
||||
this.tags.offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
_initLines() {
|
||||
this.lines = []
|
||||
const lines = this.lyric.split(/\r\n|\r|\n/)
|
||||
const linesMap = {}
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
let result = timeFieldExp.exec(line)
|
||||
if (result) {
|
||||
const timeField = result[0]
|
||||
const text = line.replace(timeFieldExp, '').trim()
|
||||
if (text) {
|
||||
const times = timeField.match(timeExp)
|
||||
if (times == null) continue
|
||||
for (const time of times) {
|
||||
const timeStr = time.replace(/(\.\d\d)0$/, '$1')
|
||||
if (linesMap[timeStr]) {
|
||||
linesMap[timeStr].extendedLyrics.push(text)
|
||||
continue
|
||||
}
|
||||
const timeArr = timeStr.split(':')
|
||||
if (timeArr.length < 3) timeArr.unshift(0)
|
||||
if (timeArr[2].indexOf('.') > -1) {
|
||||
timeArr.push(...timeArr[2].split('.'))
|
||||
timeArr.splice(2, 1)
|
||||
}
|
||||
linesMap[timeStr] = {
|
||||
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
|
||||
text,
|
||||
extendedLyrics: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const lrc of this.extendedLyrics) parseExtendedLyric(linesMap, lrc)
|
||||
this.lines = Object.values(linesMap)
|
||||
this.lines.sort((a, b) => {
|
||||
return a.time - b.time
|
||||
})
|
||||
this.maxLine = this.lines.length - 1
|
||||
}
|
||||
|
||||
_currentTime() {
|
||||
return getNow() - this._performanceTime + this._startTime
|
||||
}
|
||||
|
||||
_findCurLineNum(curTime, startIndex = 0) {
|
||||
if (curTime <= 0) return 0
|
||||
const length = this.lines.length
|
||||
for (let index = startIndex; index < length; index++) if (curTime <= this.lines[index].time) return index === 0 ? 0 : index - 1
|
||||
return length - 1
|
||||
}
|
||||
|
||||
_handleMaxLine() {
|
||||
this.onPlay(this.curLineNum, this.lines[this.curLineNum].text, this._currentTime())
|
||||
this.pause()
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.curLineNum++
|
||||
// console.log('curLineNum time', this.lines[this.curLineNum].time)
|
||||
if (this.curLineNum >= this.maxLine) return this._handleMaxLine()
|
||||
|
||||
let curLine = this.lines[this.curLineNum]
|
||||
|
||||
const currentTime = this._currentTime()
|
||||
const driftTime = currentTime - curLine.time
|
||||
|
||||
if (driftTime >= 0 || this.curLineNum === 0) {
|
||||
let nextLine = this.lines[this.curLineNum + 1]
|
||||
this.delay = nextLine.time - curLine.time - driftTime
|
||||
|
||||
if (this.delay > 0) {
|
||||
if (this.isPlay) {
|
||||
timeoutTools.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, this.delay)
|
||||
}
|
||||
this.onPlay(this.curLineNum, curLine.text, currentTime)
|
||||
return
|
||||
} else {
|
||||
let newCurLineNum = this._findCurLineNum(currentTime, this.curLineNum + 1)
|
||||
if (newCurLineNum > this.curLineNum) this.curLineNum = newCurLineNum - 1
|
||||
this._refresh()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.curLineNum = this._findCurLineNum(currentTime, this.curLineNum) - 1
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
play(curTime = 0) {
|
||||
if (!this.lines.length) return
|
||||
this.pause()
|
||||
this.isPlay = true
|
||||
|
||||
this._performanceTime = getNow() - parseInt(this.tags.offset + this.offset)
|
||||
this._startTime = curTime
|
||||
|
||||
this.curLineNum = this._findCurLineNum(this._currentTime()) - 1
|
||||
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.isPlay) return
|
||||
this.isPlay = false
|
||||
timeoutTools.clear()
|
||||
if (this.curLineNum === this.maxLine) return
|
||||
const currentTime = this._currentTime()
|
||||
const curLineNum = this._findCurLineNum(currentTime)
|
||||
if (this.curLineNum !== curLineNum) {
|
||||
this.curLineNum = curLineNum
|
||||
this.onPlay(curLineNum, this.lines[curLineNum].text, currentTime)
|
||||
}
|
||||
}
|
||||
|
||||
setLyric(lyric, extendedLyrics) {
|
||||
// console.log(extendedLyrics)
|
||||
if (this.isPlay) this.pause()
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this._init()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
const getNow = exports.getNow = typeof performance == 'object' && window.performance.now ? window.performance.now.bind(window.performance) : Date.now.bind(Date)
|
||||
|
||||
exports.TimeoutTools = class TimeoutTools {
|
||||
constructor(thresholdTime = 200) {
|
||||
this.invokeTime = 0
|
||||
this.animationFrameId = null
|
||||
this.timeoutId = null
|
||||
this.callback = null
|
||||
this.thresholdTime = thresholdTime
|
||||
}
|
||||
|
||||
run() {
|
||||
this.animationFrameId = window.requestAnimationFrame(() => {
|
||||
this.animationFrameId = null
|
||||
let diff = this.invokeTime - getNow()
|
||||
// console.log('diff', diff)
|
||||
if (diff > 0) {
|
||||
if (diff < this.thresholdTime) return this.run()
|
||||
return this.timeoutId = window.setTimeout(() => {
|
||||
this.timeoutId = null
|
||||
this.run()
|
||||
}, diff - this.thresholdTime)
|
||||
}
|
||||
|
||||
// console.log('diff', diff)
|
||||
this.callback(diff)
|
||||
})
|
||||
}
|
||||
|
||||
start(callback = () => {}, timeout = 0) {
|
||||
// console.log(timeout)
|
||||
this.callback = callback
|
||||
this.invokeTime = getNow() + timeout
|
||||
|
||||
this.run()
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.animationFrameId) {
|
||||
window.cancelAnimationFrame(this.animationFrameId)
|
||||
this.animationFrameId = null
|
||||
}
|
||||
if (this.timeoutId) {
|
||||
window.clearTimeout(this.timeoutId)
|
||||
this.timeoutId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { compareVer } from './index'
|
||||
|
||||
const oldThemeMap = {
|
||||
0: 'green',
|
||||
1: 'blue',
|
||||
2: 'yellow',
|
||||
3: 'orange',
|
||||
4: 'red',
|
||||
10: 'pink',
|
||||
5: 'purple',
|
||||
6: 'grey',
|
||||
11: 'ming',
|
||||
12: 'blue2',
|
||||
13: 'black',
|
||||
7: 'mid_autumn',
|
||||
8: 'naruto',
|
||||
9: 'happy_new_year',
|
||||
} as const
|
||||
|
||||
export default (setting: any): Partial<LX.AppSetting> => {
|
||||
setting = { ...setting }
|
||||
|
||||
// 迁移 v2 之前的配置
|
||||
if (compareVer(setting.version, '2.0.0') < 0) {
|
||||
// 迁移列表滚动位置设置 ~0.18.3
|
||||
if (setting.list.scroll) {
|
||||
let scroll = setting.list.scroll
|
||||
if (setting.list?.isSaveScrollLocation) setting.list.isSaveScrollLocation = scroll.enable
|
||||
delete setting.list.scroll
|
||||
}
|
||||
|
||||
// 修正拼写问题 v1.8.2 及以前
|
||||
if (setting.player.isShowLyricTransition != null) {
|
||||
setting.player.isShowLyricTranslation = setting.player.isShowLyricTransition
|
||||
delete setting.player.isShowLyricTransition
|
||||
}
|
||||
|
||||
// 迁移v1.19.0之前的主题设置
|
||||
if (setting.themeId != null) {
|
||||
setting.theme = {
|
||||
id: setting.themeId,
|
||||
}
|
||||
delete setting.themeId
|
||||
}
|
||||
|
||||
setting.tray.enable = setting.tray.isShow
|
||||
|
||||
setting['common.windowSizeId'] = setting.windowSizeId
|
||||
setting['common.startInFullscreen'] = setting.startInFullscreen
|
||||
setting['common.langId'] = setting.langId
|
||||
setting['common.apiSource'] = setting.apiSource
|
||||
setting['common.sourceNameType'] = setting.sourceNameType
|
||||
setting['common.font'] = setting.font
|
||||
setting['common.isShowAnimation'] = setting.isShowAnimation
|
||||
setting['common.randomAnimate'] = setting.randomAnimate
|
||||
setting['common.isAgreePact'] = setting.isAgreePact
|
||||
setting['common.controlBtnPosition'] = setting.controlBtnPosition
|
||||
|
||||
setting['player.togglePlayMethod'] = setting.player.togglePlayMethod
|
||||
setting['player.highQuality'] = setting.player.highQuality
|
||||
setting['player.isShowTaskProgess'] = setting.player.isShowTaskProgess
|
||||
setting['player.volume'] = setting.player.volume
|
||||
setting['player.isMute'] = setting.player.isMute
|
||||
setting['player.mediaDeviceId'] = setting.player.mediaDeviceId
|
||||
setting['player.isMediaDeviceRemovedStopPlay'] = setting.player.isMediaDeviceRemovedStopPlay
|
||||
setting['player.isShowLyricTranslation'] = setting.player.isShowLyricTranslation
|
||||
setting['player.isShowLyricRoma'] = setting.player.isShowLyricRoma
|
||||
setting['player.isS2t'] = setting.player.isS2t
|
||||
setting['player.isPlayLxlrc'] = setting.player.isPlayLxlrc
|
||||
setting['player.isSavePlayTime'] = setting.player.isSavePlayTime
|
||||
setting['player.audioVisualization'] = setting.player.audioVisualization
|
||||
setting['player.waitPlayEndStop'] = setting.player.waitPlayEndStop
|
||||
setting['player.waitPlayEndStopTime'] = setting.player.waitPlayEndStopTime
|
||||
setting['player.autoSkipOnError'] = setting.player.autoSkipOnError
|
||||
|
||||
setting['playDetail.isZoomActiveLrc'] = setting.playDetail.isZoomActiveLrc
|
||||
setting['playDetail.isShowLyricProgressSetting'] = setting.playDetail.isShowLyricProgressSetting
|
||||
setting['playDetail.style.fontSize'] = setting.playDetail.style.fontSize
|
||||
setting['playDetail.style.align'] = setting.playDetail.style.align
|
||||
|
||||
setting['desktopLyric.enable'] = setting.desktopLyric.enable
|
||||
setting['desktopLyric.isLock'] = setting.desktopLyric.isLock
|
||||
setting['desktopLyric.isAlwaysOnTop'] = setting.desktopLyric.isAlwaysOnTop
|
||||
setting['desktopLyric.isAlwaysOnTopLoop'] = setting.desktopLyric.isAlwaysOnTopLoop
|
||||
setting['desktopLyric.width'] = setting.desktopLyric.width
|
||||
setting['desktopLyric.height'] = setting.desktopLyric.height
|
||||
setting['desktopLyric.x'] = setting.desktopLyric.x
|
||||
setting['desktopLyric.y'] = setting.desktopLyric.y
|
||||
setting['desktopLyric.isLockScreen'] = setting.desktopLyric.isLockScreen
|
||||
setting['desktopLyric.isDelayScroll'] = setting.desktopLyric.isDelayScroll
|
||||
setting['desktopLyric.isHoverHide'] = setting.desktopLyric.isHoverHide
|
||||
setting['desktopLyric.style.font'] = setting.desktopLyric.style.font
|
||||
setting['desktopLyric.style.fontSize'] = setting.desktopLyric.style.fontSize / 100 * 16
|
||||
setting['desktopLyric.style.opacity'] = setting.desktopLyric.style.opacity
|
||||
setting['desktopLyric.style.isZoomActiveLrc'] = setting.desktopLyric.style.isZoomActiveLrc
|
||||
|
||||
setting['list.isClickPlayList'] = setting.list.isClickPlayList
|
||||
setting['list.isShowAlbumName'] = setting.list.isShowAlbumName
|
||||
setting['list.isShowSource'] = setting.list.isShowSource
|
||||
setting['list.isSaveScrollLocation'] = setting.list.isSaveScrollLocation
|
||||
setting['list.addMusicLocationType'] = setting.list.addMusicLocationType
|
||||
|
||||
setting['download.enable'] = setting.download.enable
|
||||
setting['download.savePath'] = setting.download.savePath
|
||||
setting['download.fileName'] = setting.download.fileName
|
||||
setting['download.maxDownloadNum'] = setting.download.maxDownloadNum
|
||||
setting['download.isDownloadLrc'] = setting.download.isDownloadLrc
|
||||
setting['download.lrcFormat'] = setting.download.lrcFormat
|
||||
setting['download.isEmbedPic'] = setting.download.isEmbedPic
|
||||
setting['download.isEmbedLyric'] = setting.download.isEmbedLyric
|
||||
setting['download.isUseOtherSource'] = setting.download.isUseOtherSource
|
||||
|
||||
setting['search.isShowHotSearch'] = setting.search.isShowHotSearch
|
||||
setting['search.isShowHistorySearch'] = setting.search.isShowHistorySearch
|
||||
setting['search.isFocusSearchBox'] = setting.search.isFocusSearchBox
|
||||
|
||||
setting['network.proxy.enable'] = setting.network.proxy.enable
|
||||
setting['network.proxy.host'] = setting.network.proxy.host
|
||||
setting['network.proxy.port'] = setting.network.proxy.port
|
||||
setting['network.proxy.username'] = setting.network.proxy.username
|
||||
setting['network.proxy.password'] = setting.network.proxy.password
|
||||
|
||||
setting['tray.enable'] = setting.tray.enable
|
||||
setting['tray.themeId'] = setting.tray.themeId
|
||||
|
||||
|
||||
setting['sync.enable'] = setting.sync.enable
|
||||
setting['sync.port'] = setting.sync.port
|
||||
|
||||
setting['theme.id'] = oldThemeMap[setting.theme.id as keyof typeof oldThemeMap]
|
||||
setting['theme.lightId'] = oldThemeMap[setting.theme.lightId as keyof typeof oldThemeMap]
|
||||
setting['theme.darkId'] = oldThemeMap[setting.theme.darkId as keyof typeof oldThemeMap]
|
||||
|
||||
setting['odc.isAutoClearSearchInput'] = setting.odc.isAutoClearSearchInput
|
||||
setting['odc.isAutoClearSearchList'] = setting.odc.isAutoClearSearchList
|
||||
}
|
||||
|
||||
return setting
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
const http = require('http')
|
||||
const https = require('https')
|
||||
const fs = require('fs')
|
||||
|
||||
const sendRequest = (url) => {
|
||||
const urlParse = new URL(url)
|
||||
const httpOptions = {
|
||||
method: 'get',
|
||||
host: urlParse.hostname,
|
||||
port: urlParse.port,
|
||||
path: urlParse.pathname + urlParse.search,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
|
||||
},
|
||||
}
|
||||
|
||||
// console.log(httpOptions)
|
||||
return url.protocol === 'https:'
|
||||
? https.request(httpOptions)
|
||||
: http.request(httpOptions)
|
||||
}
|
||||
|
||||
module.exports = (url, filePath) => {
|
||||
return new Promise((resolve) => {
|
||||
sendRequest(url)
|
||||
.on('response', response => {
|
||||
// console.log(response.statusCode)
|
||||
if (response.statusCode !== 200 && response.statusCode != 206) {
|
||||
response.destroy(new Error('failed'))
|
||||
return
|
||||
}
|
||||
response
|
||||
.pipe(fs.createWriteStream(filePath))
|
||||
.on('finish', () => {
|
||||
// console.log('finish')
|
||||
if (response.complete) {
|
||||
// console.log('complete')
|
||||
// meta.APIC = picPath
|
||||
// handleWriteMeta(meta, filePath)
|
||||
resolve(true)
|
||||
} else {
|
||||
resolve(false)
|
||||
fs.unlink(filePath, err => {
|
||||
if (err) console.log(err.message)
|
||||
})
|
||||
}
|
||||
}).on('error', err => {
|
||||
// console.log('response error')
|
||||
if (err) console.log(err.message)
|
||||
fs.unlink(filePath, err => {
|
||||
if (err) console.log(err.message)
|
||||
})
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
.on('error', err => {
|
||||
if (err) console.log(err.message)
|
||||
// delete meta.APIC
|
||||
// handleWriteMeta(meta, filePath)
|
||||
resolve(false)
|
||||
})
|
||||
.end()
|
||||
})
|
||||
}
|
||||
|
||||
// const url = 'https://y.gtimg.cn/music/photo_new/T002R500x500M000000nfgwP0D6qxd.jpg'
|
||||
// // const url = 'http://p4.music.126.net/-U2K8GKlASCSXK0cRre1gA==/109951163188718762.jpg'
|
||||
// const picPath = require('path').join(__dirname, 'test.jpg')
|
||||
// module.exports(url, picPath).then((sucee) => {
|
||||
// console.log(sucee)
|
||||
// })
|
||||
@@ -0,0 +1,281 @@
|
||||
// Fork from https://github.com/claus/flac-metadata
|
||||
// 在 flac-metadata 的基础上修复与改进标签与封面写入逻辑
|
||||
|
||||
|
||||
const Transform = require('stream').Transform
|
||||
|
||||
const MetaDataBlock = require('./lib/MetaDataBlock')
|
||||
const MetaDataBlockStreamInfo = require('./lib/MetaDataBlockStreamInfo')
|
||||
const MetaDataBlockVorbisComment = require('./lib/MetaDataBlockVorbisComment')
|
||||
const MetaDataBlockPicture = require('./lib/MetaDataBlockPicture')
|
||||
|
||||
const STATE_IDLE = 0
|
||||
const STATE_MARKER = 1
|
||||
const STATE_MDB_HEADER = 2
|
||||
const STATE_MDB = 3
|
||||
const STATE_PASS_THROUGH = 4
|
||||
|
||||
class Processor extends Transform {
|
||||
constructor(options) {
|
||||
super(options)
|
||||
// MDB types
|
||||
this.MDB_TYPE_STREAMINFO = 0
|
||||
this.MDB_TYPE_PADDING = 1
|
||||
this.MDB_TYPE_APPLICATION = 2
|
||||
this.MDB_TYPE_SEEKTABLE = 3
|
||||
this.MDB_TYPE_VORBIS_COMMENT = 4
|
||||
this.MDB_TYPE_CUESHEET = 5
|
||||
this.MDB_TYPE_PICTURE = 6
|
||||
this.MDB_TYPE_INVALID = 127
|
||||
|
||||
this.state = STATE_IDLE
|
||||
|
||||
this.isFlac = false
|
||||
|
||||
this.buf = null
|
||||
this.bufPos = 0
|
||||
|
||||
this.mdb = null
|
||||
this.mdbLen = 0
|
||||
this.mdbLast = false
|
||||
this.mdbPush = false
|
||||
this.mdbLastWritten = false
|
||||
|
||||
this.parseMetaDataBlocks = false
|
||||
|
||||
this.waitWriteVorbis = null
|
||||
this.waitWritePicture = null
|
||||
this.tasks = 0
|
||||
|
||||
if (!(this instanceof Processor)) return new Processor(options)
|
||||
if (options && !!options.parseMetaDataBlocks) { this.parseMetaDataBlocks = true }
|
||||
}
|
||||
|
||||
writeMeta({ vorbis, picture }) {
|
||||
if (vorbis != null) {
|
||||
this.waitWriteVorbis = vorbis
|
||||
this.tasks++
|
||||
}
|
||||
if (picture != null) {
|
||||
this.waitWritePicture = picture
|
||||
this.tasks++
|
||||
}
|
||||
}
|
||||
|
||||
// clearMeta() {
|
||||
// this.mdbLastWritten = true
|
||||
// }
|
||||
|
||||
readMeta(callback) {
|
||||
this.parseMetaDataBlocks = true
|
||||
this.readCallBack = callback
|
||||
}
|
||||
|
||||
_transform(chunk, enc, done) {
|
||||
let chunkPos = 0
|
||||
let chunkLen = chunk.length
|
||||
let isChunkProcessed = false
|
||||
let _this = this
|
||||
|
||||
function _safePush(minCapacity, persist, validate) {
|
||||
let slice
|
||||
let chunkAvailable = chunkLen - chunkPos
|
||||
let isDone = (chunkAvailable + this.bufPos >= minCapacity)
|
||||
validate = (typeof validate === 'function') ? validate : function() { return true }
|
||||
if (isDone) {
|
||||
// Enough data available
|
||||
if (persist) {
|
||||
// Persist the entire block so it can be parsed
|
||||
if (this.bufPos > 0) {
|
||||
// Part of this block's data is in backup buffer, copy rest over
|
||||
chunk.copy(this.buf, this.bufPos, chunkPos, chunkPos + minCapacity - this.bufPos)
|
||||
slice = this.buf.slice(0, minCapacity)
|
||||
} else {
|
||||
// Entire block fits in current chunk
|
||||
slice = chunk.slice(chunkPos, chunkPos + minCapacity)
|
||||
}
|
||||
} else {
|
||||
slice = chunk.slice(chunkPos, chunkPos + minCapacity - this.bufPos)
|
||||
}
|
||||
// Push block after validation
|
||||
validate(slice, isDone) && _this.push(slice)
|
||||
chunkPos += minCapacity - this.bufPos
|
||||
this.bufPos = 0
|
||||
this.buf = null
|
||||
} else {
|
||||
// Not enough data available
|
||||
if (persist) {
|
||||
// Copy/append incomplete block to backup buffer
|
||||
this.buf = this.buf || Buffer.alloc(minCapacity)
|
||||
chunk.copy(this.buf, this.bufPos, chunkPos, chunkLen)
|
||||
} else {
|
||||
// Push incomplete block after validation
|
||||
slice = chunk.slice(chunkPos, chunkLen)
|
||||
validate(slice, isDone) && _this.push(slice)
|
||||
}
|
||||
this.bufPos += chunkLen - chunkPos
|
||||
}
|
||||
return isDone
|
||||
};
|
||||
let safePush = _safePush.bind(this)
|
||||
|
||||
while (!isChunkProcessed) {
|
||||
switch (this.state) {
|
||||
case STATE_IDLE:
|
||||
this.state = STATE_MARKER
|
||||
break
|
||||
case STATE_MARKER:
|
||||
if (safePush(4, true, this._validateMarker.bind(this))) {
|
||||
this.state = this.isFlac ? STATE_MDB_HEADER : STATE_PASS_THROUGH
|
||||
} else {
|
||||
isChunkProcessed = true
|
||||
}
|
||||
break
|
||||
case STATE_MDB_HEADER:
|
||||
if (safePush(4, true, this._validateMDBHeader.bind(this))) {
|
||||
this.state = STATE_MDB
|
||||
} else {
|
||||
isChunkProcessed = true
|
||||
}
|
||||
break
|
||||
case STATE_MDB:
|
||||
if (safePush(this.mdbLen, this.parseMetaDataBlocks, this._validateMDB.bind(this))) {
|
||||
if (this.mdb.isLast) {
|
||||
// This MDB has the isLast flag set to true.
|
||||
// Ignore all following MDBs.
|
||||
this.mdbLastWritten = true
|
||||
}
|
||||
this.readCallBack && this.readCallBack(this.mdb)
|
||||
if (this.mdbLast) {
|
||||
this._writeVorbisComment()
|
||||
this._writePicture()
|
||||
this.state = STATE_PASS_THROUGH
|
||||
} else {
|
||||
this.state = STATE_MDB_HEADER
|
||||
}
|
||||
} else {
|
||||
isChunkProcessed = true
|
||||
}
|
||||
break
|
||||
case STATE_PASS_THROUGH:
|
||||
safePush(chunkLen - chunkPos, false)
|
||||
isChunkProcessed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
done()
|
||||
}
|
||||
|
||||
_validateMarker(slice, isDone) {
|
||||
this.isFlac = (slice.toString('utf8', 0) === 'fLaC')
|
||||
// TODO: completely bail out if file is not a FLAC?
|
||||
return true
|
||||
}
|
||||
|
||||
_validateMDBHeader(slice, isDone) {
|
||||
// Parse MDB header
|
||||
let header = slice.readUInt32BE(0)
|
||||
let type = (header >>> 24) & 0x7f
|
||||
this.mdbLast = (((header >>> 24) & 0x80) !== 0)
|
||||
this.mdbLen = header & 0xffffff
|
||||
// Create appropriate MDB object
|
||||
// (data is injected later in _validateMDB, if parseMetaDataBlocks option is set to true)
|
||||
switch (type) {
|
||||
case this.MDB_TYPE_STREAMINFO:
|
||||
this.mdb = new MetaDataBlockStreamInfo(this.mdbLast)
|
||||
break
|
||||
case this.MDB_TYPE_VORBIS_COMMENT:
|
||||
if (this.waitWriteVorbis) {
|
||||
this._writeVorbisComment(slice, header)
|
||||
this.mdbPush = false
|
||||
return this.mdbPush
|
||||
} else {
|
||||
this.mdb = new MetaDataBlockVorbisComment(this.mdbLast)
|
||||
this.readCallback && this.readCallback(this.mdb)
|
||||
}
|
||||
break
|
||||
case this.MDB_TYPE_PICTURE:
|
||||
if (this.waitWritePicture) {
|
||||
this._writePicture(slice, header)
|
||||
this.mdbPush = false
|
||||
return this.mdbPush
|
||||
} else {
|
||||
this.mdb = new MetaDataBlockPicture(this.mdbLast)
|
||||
this.readCallback && this.readCallback(this.mdb)
|
||||
}
|
||||
break
|
||||
case this.MDB_TYPE_PADDING:
|
||||
case this.MDB_TYPE_APPLICATION:
|
||||
case this.MDB_TYPE_SEEKTABLE:
|
||||
case this.MDB_TYPE_CUESHEET:
|
||||
case this.MDB_TYPE_INVALID:
|
||||
default:
|
||||
this.mdb = new MetaDataBlock(this.mdbLast, type)
|
||||
break
|
||||
}
|
||||
|
||||
// this.emit('preprocess', this.mdb)
|
||||
|
||||
if (this.mdbLastWritten) {
|
||||
// A previous MDB had the isLast flag set to true.
|
||||
// Ignore all following MDBs.
|
||||
this.mdb.remove()
|
||||
} else {
|
||||
if (this.mdbLast && this.tasks > 0) {
|
||||
header &= 0x7fffffff
|
||||
slice.writeUInt32BE(header >>> 0, 0)
|
||||
}
|
||||
// The consumer may change the MDB's isLast flag in the preprocess handler.
|
||||
// Here that flag is updated in the MDB header.
|
||||
}
|
||||
this.mdbPush = !this.mdb.removed
|
||||
return this.mdbPush
|
||||
}
|
||||
|
||||
_validateMDB(slice, isDone) {
|
||||
// Parse the MDB if parseMetaDataBlocks option is set to true
|
||||
if (this.parseMetaDataBlocks && isDone) {
|
||||
this.mdb.parse(slice)
|
||||
}
|
||||
return this.mdbPush
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
// All chunks have been processed
|
||||
// Clean up
|
||||
this.state = STATE_IDLE
|
||||
this.mdbLastWritten = false
|
||||
this.isFlac = false
|
||||
this.bufPos = 0
|
||||
this.buf = null
|
||||
this.mdb = null
|
||||
done()
|
||||
}
|
||||
|
||||
_writeVorbisComment() {
|
||||
if (this.waitWriteVorbis == null) return
|
||||
let isLast = this.mdbLast && this.tasks === 1
|
||||
this.tasks--
|
||||
this.push(MetaDataBlockVorbisComment.create(isLast, this.waitWriteVorbis.vendor, this.waitWriteVorbis.comments).publish())
|
||||
this.waitWriteVorbis = null
|
||||
}
|
||||
|
||||
_writePicture() {
|
||||
if (this.waitWritePicture == null) return
|
||||
let isLast = this.mdbLast && this.tasks === 1
|
||||
this.tasks--
|
||||
this.push(
|
||||
MetaDataBlockPicture.create(
|
||||
isLast, this.waitWritePicture.pictureType,
|
||||
this.waitWritePicture.mimeType, this.waitWritePicture.description,
|
||||
this.waitWritePicture.width, this.waitWritePicture.height,
|
||||
this.waitWritePicture.bitsPerPixel, this.waitWritePicture.colors,
|
||||
this.waitWritePicture.pictureData,
|
||||
).publish(),
|
||||
)
|
||||
this.waitWritePicture = null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Processor
|
||||
@@ -0,0 +1,25 @@
|
||||
class MetaDataBlock {
|
||||
constructor(isLast, type) {
|
||||
this.isLast = isLast
|
||||
this.type = type
|
||||
this.error = null
|
||||
this.hasData = false
|
||||
this.removed = false
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.removed = true
|
||||
}
|
||||
|
||||
parse(buffer) {
|
||||
}
|
||||
|
||||
toString() {
|
||||
let str = '[MetaDataBlock]'
|
||||
str += ' type: ' + this.type
|
||||
str += ', isLast: ' + this.isLast
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MetaDataBlock
|
||||
@@ -0,0 +1,130 @@
|
||||
const MetaDataBlock = require('./MetaDataBlock')
|
||||
|
||||
class MetaDataBlockPicture extends MetaDataBlock {
|
||||
constructor(isLast) {
|
||||
super(isLast, 6)
|
||||
|
||||
this.pictureType = 0
|
||||
this.mimeType = ''
|
||||
this.description = ''
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.bitsPerPixel = 0
|
||||
this.colors = 0
|
||||
this.pictureData = null
|
||||
}
|
||||
|
||||
static create(isLast, pictureType, mimeType, description, width, height, bitsPerPixel, colors, pictureData) {
|
||||
let mdb = new MetaDataBlockPicture(isLast)
|
||||
mdb.pictureType = pictureType
|
||||
mdb.mimeType = mimeType
|
||||
mdb.description = description
|
||||
mdb.width = width
|
||||
mdb.height = height
|
||||
mdb.bitsPerPixel = bitsPerPixel
|
||||
mdb.colors = colors
|
||||
mdb.pictureData = pictureData
|
||||
mdb.hasData = true
|
||||
return mdb
|
||||
}
|
||||
|
||||
|
||||
parse(buffer) {
|
||||
try {
|
||||
let pos = 0
|
||||
|
||||
this.pictureType = buffer.readUInt32BE(pos)
|
||||
pos += 4
|
||||
|
||||
let mimeTypeLength = buffer.readUInt32BE(pos)
|
||||
this.mimeType = buffer.toString('utf8', pos + 4, pos + 4 + mimeTypeLength)
|
||||
pos += 4 + mimeTypeLength
|
||||
|
||||
let descriptionLength = buffer.readUInt32BE(pos)
|
||||
this.description = buffer.toString('utf8', pos + 4, pos + 4 + descriptionLength)
|
||||
pos += 4 + descriptionLength
|
||||
|
||||
this.width = buffer.readUInt32BE(pos)
|
||||
this.height = buffer.readUInt32BE(pos + 4)
|
||||
this.bitsPerPixel = buffer.readUInt32BE(pos + 8)
|
||||
this.colors = buffer.readUInt32BE(pos + 12)
|
||||
pos += 16
|
||||
|
||||
let pictureDataLength = buffer.readUInt32BE(pos)
|
||||
this.pictureData = Buffer.alloc(pictureDataLength)
|
||||
buffer.copy(this.pictureData, 0, pos + 4, pictureDataLength)
|
||||
|
||||
this.hasData = true
|
||||
} catch (e) {
|
||||
this.error = e
|
||||
this.hasData = false
|
||||
}
|
||||
}
|
||||
|
||||
publish() {
|
||||
let pos = 0
|
||||
let size = this.getSize()
|
||||
let buffer = Buffer.alloc(4 + size)
|
||||
|
||||
let header = size
|
||||
header |= (this.type << 24)
|
||||
header |= (this.isLast ? 0x80000000 : 0)
|
||||
buffer.writeUInt32BE(header >>> 0, pos)
|
||||
pos += 4
|
||||
|
||||
buffer.writeUInt32BE(this.pictureType, pos)
|
||||
pos += 4
|
||||
|
||||
let mimeTypeLen = Buffer.byteLength(this.mimeType)
|
||||
buffer.writeUInt32BE(mimeTypeLen, pos)
|
||||
buffer.write(this.mimeType, pos + 4)
|
||||
pos += 4 + mimeTypeLen
|
||||
|
||||
let descriptionLen = Buffer.byteLength(this.description)
|
||||
buffer.writeUInt32BE(descriptionLen, pos)
|
||||
buffer.write(this.description, pos + 4)
|
||||
pos += 4 + descriptionLen
|
||||
|
||||
buffer.writeUInt32BE(this.width, pos)
|
||||
buffer.writeUInt32BE(this.height, pos + 4)
|
||||
buffer.writeUInt32BE(this.bitsPerPixel, pos + 8)
|
||||
buffer.writeUInt32BE(this.colors, pos + 12)
|
||||
pos += 16
|
||||
|
||||
buffer.writeUInt32BE(this.pictureData.length, pos)
|
||||
this.pictureData.copy(buffer, pos + 4)
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
getSize() {
|
||||
let size = 4
|
||||
size += 4 + Buffer.byteLength(this.mimeType)
|
||||
size += 4 + Buffer.byteLength(this.description)
|
||||
size += 16
|
||||
size += 4 + this.pictureData.length
|
||||
return size
|
||||
}
|
||||
|
||||
toString() {
|
||||
let str = '[MetaDataBlockPicture]'
|
||||
str += ' type: ' + this.type
|
||||
str += ', isLast: ' + this.isLast
|
||||
if (this.error) {
|
||||
str += '\n ERROR: ' + this.error
|
||||
}
|
||||
if (this.hasData) {
|
||||
str += '\n pictureType: ' + this.pictureType
|
||||
str += '\n mimeType: ' + this.mimeType
|
||||
str += '\n description: ' + this.description
|
||||
str += '\n width: ' + this.width
|
||||
str += '\n height: ' + this.height
|
||||
str += '\n bitsPerPixel: ' + this.bitsPerPixel
|
||||
str += '\n colors: ' + this.colors
|
||||
str += '\n pictureData: ' + (this.pictureData ? this.pictureData.length : '<null>')
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MetaDataBlockPicture
|
||||
@@ -0,0 +1,84 @@
|
||||
const MetaDataBlock = require('./MetaDataBlock')
|
||||
|
||||
function pad(n, width) {
|
||||
n = '' + n
|
||||
return (n.length >= width) ? n : new Array(width - n.length + 1).join('0') + n
|
||||
}
|
||||
|
||||
class MetaDataBlockStreamInfo extends MetaDataBlock {
|
||||
constructor(isLast) {
|
||||
super(isLast, 0)
|
||||
|
||||
this.minBlockSize = 0
|
||||
this.maxBlockSize = 0
|
||||
this.minFrameSize = 0
|
||||
this.maxFrameSize = 0
|
||||
this.sampleRate = 0
|
||||
this.channels = 0
|
||||
this.bitsPerSample = 0
|
||||
this.samples = 0
|
||||
this.checksum = null
|
||||
this.duration = 0
|
||||
this.durationStr = '0:00.000'
|
||||
}
|
||||
|
||||
remove() {
|
||||
console.error("WARNING: Can't remove StreamInfo block!")
|
||||
}
|
||||
|
||||
parse(buffer) {
|
||||
try {
|
||||
let pos = 0
|
||||
|
||||
this.minBlockSize = buffer.readUInt16BE(pos)
|
||||
this.maxBlockSize = buffer.readUInt16BE(pos + 2)
|
||||
this.minFrameSize = (buffer.readUInt8(pos + 4) << 16) | buffer.readUInt16BE(pos + 5)
|
||||
this.maxFrameSize = (buffer.readUInt8(pos + 7) << 16) | buffer.readUInt16BE(pos + 8)
|
||||
|
||||
let tmp = buffer.readUInt32BE(pos + 10)
|
||||
this.sampleRate = tmp >>> 12
|
||||
this.channels = (tmp >>> 9) & 0x07
|
||||
this.bitsPerSample = (tmp >>> 4) & 0x1f
|
||||
this.samples = +((tmp & 0x0f) << 4) + buffer.readUInt32BE(pos + 14)
|
||||
|
||||
this.checksum = Buffer.alloc(16)
|
||||
buffer.copy(this.checksum, 0, 18, 34)
|
||||
|
||||
this.duration = this.samples / this.sampleRate
|
||||
|
||||
let minutes = '' + Math.floor(this.duration / 60)
|
||||
let seconds = pad(Math.floor(this.duration % 60), 2)
|
||||
let milliseconds = pad(Math.round(((this.duration % 60) - Math.floor(this.duration % 60)) * 1000), 3)
|
||||
this.durationStr = minutes + ':' + seconds + '.' + milliseconds
|
||||
|
||||
this.hasData = true
|
||||
} catch (e) {
|
||||
this.error = e
|
||||
this.hasData = false
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
let str = '[MetaDataBlockStreamInfo]'
|
||||
str += ' type: ' + this.type
|
||||
str += ', isLast: ' + this.isLast
|
||||
if (this.error) {
|
||||
str += '\n ERROR: ' + this.error
|
||||
}
|
||||
if (this.hasData) {
|
||||
str += '\n minBlockSize: ' + this.minBlockSize
|
||||
str += '\n maxBlockSize: ' + this.maxBlockSize
|
||||
str += '\n minFrameSize: ' + this.minFrameSize
|
||||
str += '\n maxFrameSize: ' + this.maxFrameSize
|
||||
str += '\n samples: ' + this.samples
|
||||
str += '\n sampleRate: ' + this.sampleRate
|
||||
str += '\n channels: ' + (this.channels + 1)
|
||||
str += '\n bitsPerSample: ' + (this.bitsPerSample + 1)
|
||||
str += '\n duration: ' + this.durationStr
|
||||
str += '\n checksum: ' + (this.checksum ? this.checksum.toString('hex') : '<null>')
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MetaDataBlockStreamInfo
|
||||
@@ -0,0 +1,105 @@
|
||||
const MetaDataBlock = require('./MetaDataBlock')
|
||||
|
||||
class MetaDataBlockVorbisComment extends MetaDataBlock {
|
||||
constructor(isLast) {
|
||||
super(isLast, 4)
|
||||
|
||||
this.vendor = ''
|
||||
this.comments = []
|
||||
}
|
||||
|
||||
static create(isLast, vendor, comments) {
|
||||
let mdb = new MetaDataBlockVorbisComment(isLast)
|
||||
mdb.vendor = vendor
|
||||
mdb.comments = comments
|
||||
mdb.hasData = true
|
||||
return mdb
|
||||
}
|
||||
|
||||
parse(buffer) {
|
||||
try {
|
||||
let pos = 0
|
||||
|
||||
let vendorLen = buffer.readUInt32LE(pos)
|
||||
let vendor = buffer.toString('utf8', pos + 4, pos + 4 + vendorLen)
|
||||
this.vendor = vendor
|
||||
pos += 4 + vendorLen
|
||||
|
||||
let commentCount = buffer.readUInt32LE(pos)
|
||||
pos += 4
|
||||
|
||||
while (commentCount-- > 0) {
|
||||
let commentLen = buffer.readUInt32LE(pos)
|
||||
let comment = buffer.toString('utf8', pos + 4, pos + 4 + commentLen)
|
||||
this.comments.push(comment)
|
||||
pos += 4 + commentLen
|
||||
}
|
||||
|
||||
this.hasData = true
|
||||
} catch (e) {
|
||||
this.error = e
|
||||
this.hasData = false
|
||||
}
|
||||
}
|
||||
|
||||
publish() {
|
||||
let pos = 0
|
||||
let size = this.getSize()
|
||||
let buffer = Buffer.alloc(4 + size)
|
||||
|
||||
let header = size
|
||||
header |= (this.type << 24)
|
||||
header |= (this.isLast ? 0x80000000 : 0)
|
||||
buffer.writeUInt32BE(header >>> 0, pos)
|
||||
pos += 4
|
||||
let vendorLen = Buffer.byteLength(this.vendor)
|
||||
buffer.writeUInt32LE(vendorLen, pos)
|
||||
buffer.write(this.vendor, pos + 4)
|
||||
pos += 4 + vendorLen
|
||||
|
||||
let commentCount = this.comments.length
|
||||
buffer.writeUInt32LE(commentCount, pos)
|
||||
pos += 4
|
||||
|
||||
for (let i = 0; i < commentCount; i++) {
|
||||
let comment = this.comments[i]
|
||||
let commentLen = Buffer.byteLength(comment)
|
||||
buffer.writeUInt32LE(commentLen, pos)
|
||||
buffer.write(comment, pos + 4)
|
||||
pos += 4 + commentLen
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
getSize() {
|
||||
let size = 8 + Buffer.byteLength(this.vendor)
|
||||
for (let i = 0; i < this.comments.length; i++) {
|
||||
size += 4 + Buffer.byteLength(this.comments[i])
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
toString() {
|
||||
let str = '[MetaDataBlockVorbisComment]'
|
||||
str += ' type: ' + this.type
|
||||
str += ', isLast: ' + this.isLast
|
||||
if (this.error) {
|
||||
str += '\n ERROR: ' + this.error
|
||||
}
|
||||
if (this.hasData) {
|
||||
str += '\n vendor: ' + this.vendor
|
||||
if (this.comments.length) {
|
||||
str += '\n comments:'
|
||||
for (let i = 0; i < this.comments.length; i++) {
|
||||
str += '\n ' + this.comments[i].split('=').join(': ')
|
||||
}
|
||||
} else {
|
||||
str += '\n comments: none'
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MetaDataBlockVorbisComment
|
||||
@@ -0,0 +1,80 @@
|
||||
const fs = require('fs')
|
||||
const fsPromises = fs.promises
|
||||
const path = require('path')
|
||||
const getImgSize = require('image-size')
|
||||
const download = require('./downloader')
|
||||
|
||||
const FlacProcessor = require('./flac-metadata/index')
|
||||
|
||||
const extReg = /^(\.(?:jpe?g|png)).*$/
|
||||
const vendor = 'reference libFLAC 1.2.1 20070917'
|
||||
|
||||
const writeMeta = async(filePath, meta, picPath) => {
|
||||
const comments = Object.keys(meta).map(key => `${key.toUpperCase()}=${meta[key] || ''}`)
|
||||
const data = {
|
||||
vorbis: {
|
||||
vendor,
|
||||
comments,
|
||||
},
|
||||
}
|
||||
if (picPath) {
|
||||
const apicData = await fsPromises.readFile(picPath)
|
||||
let imgSize = getImgSize(apicData)
|
||||
let mime_type
|
||||
let bitsPerPixel
|
||||
if (apicData[0] == 0xff && apicData[1] == 0xd8 && apicData[2] == 0xff) {
|
||||
mime_type = 'image/jpeg'
|
||||
bitsPerPixel = 24
|
||||
} else {
|
||||
mime_type = 'image/png'
|
||||
bitsPerPixel = 32
|
||||
}
|
||||
data.picture = {
|
||||
pictureType: 3,
|
||||
mimeType: mime_type,
|
||||
description: '',
|
||||
width: imgSize.width,
|
||||
height: imgSize.height,
|
||||
bitsPerPixel,
|
||||
colors: 0,
|
||||
pictureData: apicData,
|
||||
}
|
||||
}
|
||||
|
||||
const reader = fs.createReadStream(filePath)
|
||||
const tempPath = filePath + '.lxmtemp'
|
||||
const writer = fs.createWriteStream(tempPath)
|
||||
const flacProcessor = new FlacProcessor()
|
||||
flacProcessor.writeMeta(data)
|
||||
|
||||
reader.pipe(flacProcessor).pipe(writer).on('finish', () => {
|
||||
fs.unlink(filePath, err => {
|
||||
if (err) return console.log(err.message)
|
||||
fs.rename(tempPath, filePath, err => {
|
||||
if (err) console.log(err.message)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = (filePath, meta) => {
|
||||
if (!meta.APIC) return writeMeta(filePath, meta)
|
||||
const picUrl = meta.APIC
|
||||
delete meta.APIC
|
||||
if (!/^http/.test(picUrl)) {
|
||||
return writeMeta(filePath, meta)
|
||||
}
|
||||
let ext = path.extname(picUrl)
|
||||
let picPath = filePath.replace(/\.flac$/, '') + (ext ? ext.replace(extReg, '$1') : '.jpg')
|
||||
|
||||
download(picUrl, picPath).then(success => {
|
||||
if (success) {
|
||||
writeMeta(filePath, meta, picPath).finally(() => {
|
||||
fs.unlink(picPath, err => {
|
||||
if (err) console.log(err.message)
|
||||
})
|
||||
})
|
||||
} else writeMeta(filePath, meta)
|
||||
})
|
||||
}
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export interface MusicMeta {
|
||||
title: string
|
||||
artist: string | null
|
||||
album: string | null
|
||||
APIC: string | null
|
||||
lyrics: string | null
|
||||
}
|
||||
export function setMeta(filePath: string, meta: MusicMeta): void
|
||||
@@ -0,0 +1,14 @@
|
||||
const path = require('path')
|
||||
const mp3Meta = require('./mp3Meta')
|
||||
const flacMeta = require('./flacMeta')
|
||||
|
||||
exports.setMeta = (filePath, meta) => {
|
||||
switch (path.extname(filePath)) {
|
||||
case '.mp3':
|
||||
mp3Meta(filePath, meta)
|
||||
break
|
||||
case '.flac':
|
||||
flacMeta(filePath, meta)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
const NodeID3 = require('node-id3')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const download = require('./downloader')
|
||||
const extReg = /^(\.(?:jpe?g|png)).*$/
|
||||
|
||||
const handleWriteMeta = (meta, filePath) => {
|
||||
if (meta.lyrics) {
|
||||
meta.unsynchronisedLyrics = {
|
||||
language: 'zho',
|
||||
text: meta.lyrics,
|
||||
}
|
||||
delete meta.lyrics
|
||||
}
|
||||
NodeID3.write(meta, filePath)
|
||||
}
|
||||
|
||||
module.exports = (filePath, meta) => {
|
||||
if (!meta.APIC) return handleWriteMeta(meta, filePath)
|
||||
if (!/^http/.test(meta.APIC)) {
|
||||
delete meta.APIC
|
||||
return handleWriteMeta(meta, filePath)
|
||||
}
|
||||
let ext = path.extname(meta.APIC)
|
||||
let picPath = filePath.replace(/\.mp3$/, '') + (ext ? ext.replace(extReg, '$1') : '.jpg')
|
||||
download(meta.APIC, picPath).then(success => {
|
||||
if (success) {
|
||||
meta.APIC = picPath
|
||||
handleWriteMeta(meta, filePath)
|
||||
fs.unlink(picPath, err => {
|
||||
if (err) console.log(err.message)
|
||||
})
|
||||
} else {
|
||||
delete meta.APIC
|
||||
handleWriteMeta(meta, filePath)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto'
|
||||
import { gzip, gunzip } from 'zlib'
|
||||
import { log } from '@common/utils'
|
||||
import path from 'path'
|
||||
|
||||
export const joinPath = (...paths: string[]): string => path.join(...paths)
|
||||
|
||||
export const extname = (p: string): string => path.extname(p)
|
||||
export const basename = (p: string, ext?: string): string => path.basename(p, ext)
|
||||
export const dirname = (p: string): string => path.dirname(p)
|
||||
|
||||
/**
|
||||
* 检查路径是否存在
|
||||
* @param {*} path 路径
|
||||
*/
|
||||
export const checkPath = async(path: string): Promise<boolean> => {
|
||||
return await new Promise(resolve => {
|
||||
fs.access(path, fs.constants.F_OK, err => {
|
||||
if (err) return resolve(false)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const getFileStats = async(path: string): Promise<fs.Stats | null> => {
|
||||
return await new Promise(resolve => {
|
||||
fs.stat(path, (err, stats) => {
|
||||
if (err) return resolve(null)
|
||||
resolve(stats)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径并创建目录
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
export const createDir = async(path: string): Promise<void> => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
fs.access(path, fs.constants.F_OK | fs.constants.W_OK, err => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
fs.mkdir(path, { recursive: true }, err => {
|
||||
if (err) return reject(err)
|
||||
resolve()
|
||||
})
|
||||
return
|
||||
}
|
||||
return reject(err)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const removeFile = async(path: string) => new Promise<void>((resolve, reject) => {
|
||||
fs.access(path, fs.constants.F_OK, err => {
|
||||
if (err) return err.code == 'ENOENT' ? resolve() : reject(err)
|
||||
fs.unlink(path, err => {
|
||||
if (err) return reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
export const readFile = async(path: string) => fs.promises.readFile(path)
|
||||
|
||||
|
||||
/**
|
||||
* 创建 MD5 hash
|
||||
* @param {*} str
|
||||
*/
|
||||
export const toMD5 = (str: string) => crypto.createHash('md5').update(str).digest('hex')
|
||||
|
||||
export const gzipData = async(str: string): Promise<Buffer> => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
gzip(str, (err, result) => {
|
||||
if (err) return reject(err)
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const gunzipData = async(buf: Buffer): Promise<string> => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
gunzip(buf, (err, result) => {
|
||||
if (err) return reject(err)
|
||||
resolve(result.toString())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存lx配置文件
|
||||
* @param path 保存路径
|
||||
* @param data 数据
|
||||
*/
|
||||
export const saveLxConfigFile = async(path: string, data: any) => {
|
||||
if (!path.endsWith('.lxmc')) path += '.lxmc'
|
||||
fs.writeFile(path, await gzipData(JSON.stringify(data)), 'binary', err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取lx配置文件
|
||||
* @param path 文件路径
|
||||
* @returns 数据
|
||||
*/
|
||||
export const readLxConfigFile = async(path: string): Promise<any> => {
|
||||
let isJSON = path.endsWith('.json')
|
||||
let data: string | Buffer = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary')
|
||||
if (!data || isJSON) return data
|
||||
data = await gunzipData(Buffer.from(data, 'binary'))
|
||||
data = JSON.parse(data)
|
||||
|
||||
// 修复v1.14.0出现的导出数据被序列化两次的问题
|
||||
if (typeof data != 'object') {
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch (err) {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export const saveStrToFile = async(path: string, str: string | Buffer): Promise<void> => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
fs.writeFile(path, str, err => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const b64DecodeUnicode = (str: string): string => {
|
||||
// Going backwards: from bytestream, to percent-encoding, to original string.
|
||||
// return decodeURIComponent(window.atob(str).split('').map(function(c) {
|
||||
// return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
||||
// }).join(''))
|
||||
|
||||
return Buffer.from(str, 'base64').toString()
|
||||
}
|
||||
|
||||
export const copyFile = async(sourcePath: string, distPath: string) => {
|
||||
return fs.promises.copyFile(sourcePath, distPath)
|
||||
}
|
||||
|
||||
export const moveFile = async(sourcePath: string, distPath: string) => {
|
||||
return fs.promises.rename(sourcePath, distPath)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
const fs = require('fs').promises
|
||||
const path = require('path')
|
||||
|
||||
const sourceFilePath = path.join(__dirname, './kMandarin_8105.txt')
|
||||
const distFilePath = path.join(__dirname, './pinyin.txt')
|
||||
|
||||
const yuanyin = [
|
||||
['ā', 'a'],
|
||||
['á', 'a'],
|
||||
['ǎ', 'a'],
|
||||
['à', 'a'],
|
||||
['ē', 'e'],
|
||||
['é', 'e'],
|
||||
['ě', 'e'],
|
||||
['è', 'e'],
|
||||
['ī', 'i'],
|
||||
['í', 'i'],
|
||||
['ǐ', 'i'],
|
||||
['ì', 'i'],
|
||||
['ō', 'o'],
|
||||
['ó', 'o'],
|
||||
['ǒ', 'o'],
|
||||
['ò', 'o'],
|
||||
['ū', 'u'],
|
||||
['ú', 'u'],
|
||||
['ǔ', 'u'],
|
||||
['ù', 'u'],
|
||||
['ǖ', 'v'],
|
||||
['ǘ', 'v'],
|
||||
['ǚ', 'v'],
|
||||
['ǜ', 'v'],
|
||||
]
|
||||
|
||||
const parse = async() => {
|
||||
let datas = (await fs.readFile(sourceFilePath)).toString()
|
||||
datas = datas.replace(/ +=> +(\w|\+)+ */gm, ' ')
|
||||
for (const [y1, y2] of yuanyin) datas = datas.replaceAll(y1, y2)
|
||||
// console.log(datas)
|
||||
const lines = datas.split('\n')
|
||||
const dict = {}
|
||||
for (let line of lines) {
|
||||
if (!line || line.startsWith('#')) continue
|
||||
line = line.trim().replace(/^[\w+]+: */, '')
|
||||
let [p1, comment] = line.split('#')
|
||||
let [z, ps] = comment.split(/(?: *\? *-> *| *-> *)/)
|
||||
const ys = new Set([p1.trim()])
|
||||
if (ps != null) ps.split(/(?: +| *, *)/).forEach(y => ys.add(y.trim()))
|
||||
dict[z.trim()] = Array.from(ys)
|
||||
}
|
||||
|
||||
fs.writeFile(distFilePath, JSON.stringify(dict))
|
||||
}
|
||||
|
||||
|
||||
parse()
|
||||
|
||||
// let dict = {}
|
||||
// let line = 'U+2CBBF: qi # 𬮿 ?-> gai,ai'
|
||||
|
||||
// line = line.trim().replace(/^[\w+]+: */, '')
|
||||
// let [p1, comment] = line.split('#')
|
||||
// let [z, ps] = comment.split(/ *\? *-> */)
|
||||
// const ys = dict[z.trim()] = [p1.trim()]
|
||||
// console.log(ps)
|
||||
// if (ps != null) ys.push(...ps.split(/(?: +| *, *)/).map(y => y.trim()))
|
||||
// console.log(dict)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,241 @@
|
||||
|
||||
const easeInOutQuad = (t: number, b: number, c: number, d: number): number => {
|
||||
t /= d / 2
|
||||
if (t < 1) return (c / 2) * t * t + b
|
||||
t--
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b
|
||||
}
|
||||
|
||||
type Noop = () => void
|
||||
const noop: Noop = () => {}
|
||||
|
||||
const handleScrollY = (element: HTMLElement, to: number, duration = 300, fn = noop): Noop => {
|
||||
if (!element) {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
// @ts-expect-error
|
||||
const start = element.scrollTop ?? element.scrollY ?? 0
|
||||
let cancel = false
|
||||
if (to > start) {
|
||||
let maxScrollTop = element.scrollHeight - element.clientHeight
|
||||
if (to > maxScrollTop) to = maxScrollTop
|
||||
} else if (to < start) {
|
||||
if (to < 0) to = 0
|
||||
} else {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
const change = to - start
|
||||
const increment = 10
|
||||
if (!change) {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
|
||||
let currentTime = 0
|
||||
let val
|
||||
|
||||
const animateScroll = () => {
|
||||
currentTime += increment
|
||||
val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))
|
||||
if (element.scrollTo) {
|
||||
element.scrollTo(0, val)
|
||||
} else {
|
||||
element.scrollTop = val
|
||||
}
|
||||
if (currentTime < duration) {
|
||||
if (cancel) return fn()
|
||||
window.setTimeout(animateScroll, increment)
|
||||
} else {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
animateScroll()
|
||||
return () => {
|
||||
cancel = true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置滚动条位置
|
||||
* @param {*} element 要设置滚动的容器 dom
|
||||
* @param {*} to 滚动的目标位置
|
||||
* @param {*} duration 滚动完成时间 ms
|
||||
* @param {*} fn 滚动完成后的回调
|
||||
* @param {*} delay 延迟执行时间
|
||||
*/
|
||||
export const scrollTo = (element: HTMLElement, to: number, duration = 300, fn = () => {}, delay = 0): () => void => {
|
||||
let cancelFn: () => void
|
||||
let timeout: number | null
|
||||
if (delay) {
|
||||
let scrollCancelFn: Noop
|
||||
cancelFn = () => {
|
||||
timeout == null ? scrollCancelFn?.() : clearTimeout(timeout)
|
||||
}
|
||||
timeout = window.setTimeout(() => {
|
||||
timeout = null
|
||||
scrollCancelFn = handleScrollY(element, to, duration, fn)
|
||||
}, delay)
|
||||
} else {
|
||||
cancelFn = handleScrollY(element, to, duration, fn) ?? noop
|
||||
}
|
||||
return cancelFn
|
||||
}
|
||||
const handleScrollX = (element: HTMLElement, to: number, duration = 300, fn = () => {}): () => void => {
|
||||
if (!element) {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
// @ts-expect-error
|
||||
const start = element.scrollLeft || element.scrollX || 0
|
||||
let cancel = false
|
||||
if (to > start) {
|
||||
let maxScrollLeft = element.scrollWidth - element.clientWidth
|
||||
if (to > maxScrollLeft) to = maxScrollLeft
|
||||
} else if (to < start) {
|
||||
if (to < 0) to = 0
|
||||
} else {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
const change = to - start
|
||||
const increment = 10
|
||||
if (!change) {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
|
||||
let currentTime = 0
|
||||
let val
|
||||
|
||||
const animateScroll = () => {
|
||||
currentTime += increment
|
||||
val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))
|
||||
if (element.scrollTo) {
|
||||
element.scrollTo(val, 0)
|
||||
} else {
|
||||
element.scrollLeft = val
|
||||
}
|
||||
if (currentTime < duration) {
|
||||
if (cancel) return fn()
|
||||
window.setTimeout(animateScroll, increment)
|
||||
} else {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
animateScroll()
|
||||
return () => {
|
||||
cancel = true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置滚动条位置
|
||||
* @param {*} element 要设置滚动的容器 dom
|
||||
* @param {*} to 滚动的目标位置
|
||||
* @param {*} duration 滚动完成时间 ms
|
||||
* @param {*} fn 滚动完成后的回调
|
||||
* @param {*} delay 延迟执行时间
|
||||
*/
|
||||
export const scrollXTo = (element: HTMLElement, to: number, duration = 300, fn = () => {}, delay = 0): () => void => {
|
||||
let cancelFn: Noop
|
||||
let timeout: number | null
|
||||
if (delay) {
|
||||
let scrollCancelFn: Noop
|
||||
cancelFn = () => {
|
||||
timeout == null ? scrollCancelFn?.() : clearTimeout(timeout)
|
||||
}
|
||||
timeout = window.setTimeout(() => {
|
||||
timeout = null
|
||||
scrollCancelFn = handleScrollX(element, to, duration, fn)
|
||||
}, delay)
|
||||
} else {
|
||||
cancelFn = handleScrollX(element, to, duration, fn)
|
||||
}
|
||||
return cancelFn
|
||||
}
|
||||
|
||||
const handleScrollXR = (element: HTMLElement, to: number, duration = 300, fn = () => {}): () => void => {
|
||||
if (!element) {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
// @ts-expect-error
|
||||
const start = element.scrollLeft || element.scrollX as number || 0
|
||||
let cancel = false
|
||||
if (to < start) {
|
||||
let maxScrollLeft = -element.scrollWidth + element.clientWidth
|
||||
if (to < maxScrollLeft) to = maxScrollLeft
|
||||
} else if (to > start) {
|
||||
if (to > 0) to = 0
|
||||
} else {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
|
||||
const change = to - start
|
||||
const increment = 10
|
||||
if (!change) {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
|
||||
let currentTime = 0
|
||||
let val
|
||||
|
||||
const animateScroll = () => {
|
||||
currentTime += increment
|
||||
val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))
|
||||
|
||||
if (element.scrollTo) {
|
||||
element.scrollTo(val, 0)
|
||||
} else {
|
||||
element.scrollLeft = val
|
||||
}
|
||||
if (currentTime < duration) {
|
||||
if (cancel) return fn()
|
||||
window.setTimeout(animateScroll, increment)
|
||||
} else {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
animateScroll()
|
||||
return () => {
|
||||
cancel = true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置滚动条位置 (writing-mode: vertical-rl 专用)
|
||||
* @param element 要设置滚动的容器 dom
|
||||
* @param to 滚动的目标位置
|
||||
* @param duration 滚动完成时间 ms
|
||||
* @param fn 滚动完成后的回调
|
||||
* @param delay 延迟执行时间
|
||||
*/
|
||||
export const scrollXRTo = (element: HTMLElement, to: number, duration = 300, fn = () => {}, delay = 0): () => void => {
|
||||
let cancelFn: Noop
|
||||
let timeout: number | null
|
||||
if (delay) {
|
||||
let scrollCancelFn: Noop
|
||||
cancelFn = () => {
|
||||
timeout == null ? scrollCancelFn?.() : clearTimeout(timeout)
|
||||
}
|
||||
timeout = window.setTimeout(() => {
|
||||
timeout = null
|
||||
scrollCancelFn = handleScrollXR(element, to, duration, fn)
|
||||
}, delay)
|
||||
} else {
|
||||
cancelFn = handleScrollXR(element, to, duration, fn)
|
||||
}
|
||||
return cancelFn
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置标题
|
||||
*/
|
||||
let dom_title = document.getElementsByTagName('title')[0]
|
||||
export const setTitle = (title: string | null) => {
|
||||
if (!title) title = '洛雪音乐助手'
|
||||
dom_title.innerText = title
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// 业务工具方法
|
||||
|
||||
export const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfoOnline => {
|
||||
const meta: Record<string, any> = {
|
||||
songId: oldMusicInfo.songmid, // 歌曲ID,mg源为copyrightId,local为文件路径
|
||||
albumName: oldMusicInfo.albumName, // 歌曲专辑名称
|
||||
albumId: oldMusicInfo.albumId,
|
||||
picUrl: oldMusicInfo.img, // 歌曲图片链接
|
||||
qualitys: oldMusicInfo.types,
|
||||
_qualitys: oldMusicInfo._types,
|
||||
}
|
||||
if (meta._qualitys.flac32bit && !meta._qualitys.flac24bit) {
|
||||
meta._qualitys.flac24bit = meta._qualitys.flac32bit
|
||||
delete meta._qualitys.flac32bit
|
||||
|
||||
meta.qualitys = (meta.qualitys as any[]).map(quality => {
|
||||
if (quality.type == 'flac32bit') quality.type = 'flac24bit'
|
||||
return quality
|
||||
})
|
||||
}
|
||||
|
||||
switch (oldMusicInfo.source) {
|
||||
case 'kg':
|
||||
meta.hash = oldMusicInfo.hash
|
||||
break
|
||||
case 'tx':
|
||||
meta.strMediaMid = oldMusicInfo.strMediaMid
|
||||
meta.albumMid = oldMusicInfo.albumMid
|
||||
break
|
||||
case 'mg':
|
||||
meta.copyrightId = oldMusicInfo.copyrightId
|
||||
meta.lrcUrl = oldMusicInfo.lrcUrl
|
||||
meta.mrcUrl = oldMusicInfo.mrcUrl
|
||||
meta.trcUrl = oldMusicInfo.trcUrl
|
||||
break
|
||||
}
|
||||
return {
|
||||
id: `${oldMusicInfo.source as string}_${oldMusicInfo.songmid as string}`,
|
||||
name: oldMusicInfo.name,
|
||||
singer: oldMusicInfo.singer,
|
||||
source: oldMusicInfo.source,
|
||||
interval: oldMusicInfo.interval,
|
||||
meta: meta as LX.Music.MusicInfoOnline['meta'],
|
||||
}
|
||||
}
|
||||
|
||||
export const toOldMusicInfo = (minfo: LX.Music.MusicInfoOnline) => {
|
||||
const oInfo: Record<string, any> = {
|
||||
name: minfo.name,
|
||||
singer: minfo.singer,
|
||||
source: minfo.source,
|
||||
songmid: minfo.meta.songId,
|
||||
albumId: minfo.meta.albumId,
|
||||
interval: minfo.interval,
|
||||
albumName: minfo.meta.albumName,
|
||||
img: minfo.meta.picUrl,
|
||||
types: minfo.meta.qualitys,
|
||||
_types: minfo.meta._qualitys,
|
||||
typeUrl: {},
|
||||
}
|
||||
switch (minfo.source) {
|
||||
case 'kg':
|
||||
oInfo.hash = minfo.meta.hash
|
||||
break
|
||||
case 'tx':
|
||||
oInfo.strMediaMid = minfo.meta.strMediaMid
|
||||
oInfo.albumMid = minfo.meta.albumMid
|
||||
break
|
||||
case 'mg':
|
||||
oInfo.copyrightId = minfo.meta.copyrightId
|
||||
oInfo.lrcUrl = minfo.meta.lrcUrl
|
||||
oInfo.mrcUrl = minfo.meta.mrcUrl
|
||||
oInfo.trcUrl = minfo.meta.trcUrl
|
||||
break
|
||||
}
|
||||
|
||||
return oInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复2.0.0-dev.8之前的新列表数据音质
|
||||
* @param musicInfo
|
||||
*/
|
||||
export const fixNewMusicInfoQuality = (musicInfo: LX.Music.MusicInfo) => {
|
||||
if (musicInfo.source == 'local') return musicInfo
|
||||
|
||||
// @ts-expect-error
|
||||
if (musicInfo.meta._qualitys.flac32bit && !musicInfo.meta._qualitys.flac24bit) {
|
||||
// @ts-expect-error
|
||||
musicInfo.meta._qualitys.flac24bit = musicInfo.meta._qualitys.flac32bit
|
||||
// @ts-expect-error
|
||||
delete musicInfo.meta._qualitys.flac32bit
|
||||
|
||||
musicInfo.meta.qualitys = musicInfo.meta.qualitys.map(quality => {
|
||||
// @ts-expect-error
|
||||
if (quality.type == 'flac32bit') quality.type = 'flac24bit'
|
||||
return quality
|
||||
})
|
||||
}
|
||||
|
||||
return musicInfo
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useRouter, useRoute, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
computed,
|
||||
watch,
|
||||
watchEffect,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
toRaw,
|
||||
useCssModule,
|
||||
toRef,
|
||||
toRefs,
|
||||
shallowRef,
|
||||
unref,
|
||||
markRaw,
|
||||
ComputedRef,
|
||||
Ref,
|
||||
ShallowRef,
|
||||
defineProps,
|
||||
defineEmits,
|
||||
defineComponent,
|
||||
shallowReactive,
|
||||
defineExpose,
|
||||
} from 'vue'
|
||||
// import { useStore } from 'vuex'
|
||||
|
||||
// export const useState = name => {
|
||||
// const store = useStore()
|
||||
// return store.state[name]
|
||||
// }
|
||||
// export const useGetter = (...names) => {
|
||||
// const store = useStore()
|
||||
// return store.getters[names.join('/')]
|
||||
// }
|
||||
// export const useRefGetter = (...names) => {
|
||||
// const store = useStore()
|
||||
// return computed(() => store.getters[names.join('/')])
|
||||
// }
|
||||
|
||||
// export const useAction = (...names) => {
|
||||
// const store = useStore()
|
||||
// return params => {
|
||||
// return store.dispatch(names.join('/'), params)
|
||||
// }
|
||||
// }
|
||||
// export const useCommit = (...names) => {
|
||||
// const store = useStore()
|
||||
// return params => {
|
||||
// return store.commit(names.join('/'), params)
|
||||
// }
|
||||
// }
|
||||
|
||||
export const markRawList = <T extends any[]>(list: T) => {
|
||||
for (const item of list) {
|
||||
markRaw(item)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
export {
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
ref,
|
||||
toRaw,
|
||||
reactive,
|
||||
watch,
|
||||
watchEffect,
|
||||
computed,
|
||||
useCssModule,
|
||||
toRef,
|
||||
toRefs,
|
||||
shallowRef,
|
||||
unref,
|
||||
onMounted,
|
||||
markRaw,
|
||||
ComputedRef,
|
||||
Ref,
|
||||
ShallowRef,
|
||||
defineProps,
|
||||
defineEmits,
|
||||
defineComponent,
|
||||
shallowReactive,
|
||||
defineExpose,
|
||||
}
|
||||
Reference in New Issue
Block a user