This commit is contained in:
lyswhut
2022-10-29 11:36:35 +08:00
parent 66b965d9f7
commit e45cee6596
715 changed files with 57096 additions and 31771 deletions
-130
View File
@@ -1,130 +0,0 @@
const { log, isWin } = require('../../common/utils')
const { autoUpdater } = require('electron-updater')
const { mainOn, mainSend, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
autoUpdater.logger = log
// autoUpdater.autoDownload = false
autoUpdater.logger.transports.file.level = 'info'
let isFirstCheckedUpdate = true
log.info('App starting...')
// -------------------------------------------------------------------
// Open a window that displays the version
//
// THIS SECTION IS NOT REQUIRED
//
// This isn't required for auto-updates to work, but it's easier
// for the app to show a window than to have to click "About" to see
// that updates are working.
// -------------------------------------------------------------------
// let win
function sendStatusToWindow(text) {
log.info(text)
// ipcMain.send('message', text)
}
// -------------------------------------------------------------------
// Auto updates
//
// For details about these events, see the Wiki:
// https://github.com/electron-userland/electron-builder/wiki/Auto-Update#events
//
// The app doesn't need to listen to any events except `update-downloaded`
//
// Uncomment any of the below events to listen for them. Also,
// look in the previous section to see them being used.
// -------------------------------------------------------------------
// autoUpdater.on('checking-for-update', () => {
// })
// autoUpdater.on('update-available', (ev, info) => {
// })
// autoUpdater.on('update-not-available', (ev, info) => {
// })
// autoUpdater.on('error', (ev, err) => {
// })
// autoUpdater.on('download-progress', (ev, progressObj) => {
// })
// autoUpdater.on('update-downloaded', (ev, info) => {
// // Wait 5 seconds, then quit and install
// // In your application, you don't need to wait 5 seconds.
// // You could call autoUpdater.quitAndInstall(); immediately
// // setTimeout(function() {
// // autoUpdater.quitAndInstall()
// // }, 5000)
// })
let waitEvent = []
const handleSendEvent = action => {
if (global.modules.mainWindow) {
setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成
if (!global.modules.mainWindow) return
mainSend(global.modules.mainWindow, action.type, action.info)
}, 2000)
} else {
waitEvent.push(action)
}
}
module.exports = () => {
if (!isFirstCheckedUpdate) {
if (waitEvent.length) {
waitEvent.forEach((event, index) => {
setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成
if (!global.modules.mainWindow) return
mainSend(global.modules.mainWindow, event.type, event.info)
}, 2000 * (index + 1))
})
waitEvent = []
}
return
}
isFirstCheckedUpdate = false
autoUpdater.on('checking-for-update', () => {
sendStatusToWindow('Checking for update...')
})
autoUpdater.on('update-available', info => {
sendStatusToWindow('Update available.')
handleSendEvent({ type: ipcMainWindowNames.update_available, info })
})
autoUpdater.on('update-not-available', info => {
sendStatusToWindow('Update not available.')
handleSendEvent({ type: ipcMainWindowNames.update_not_available, info })
})
autoUpdater.on('error', err => {
sendStatusToWindow('Error in auto-updater.')
handleSendEvent({ type: ipcMainWindowNames.update_error, info: err.message })
})
autoUpdater.on('download-progress', progressObj => {
let log_message = 'Download speed: ' + progressObj.bytesPerSecond
log_message = log_message + ' - Downloaded ' + progressObj.percent + '%'
log_message = log_message + ' (' + progressObj.transferred + '/' + progressObj.total + ')'
sendStatusToWindow(log_message)
handleSendEvent({ type: ipcMainWindowNames.update_progress, info: progressObj })
})
autoUpdater.on('update-downloaded', info => {
sendStatusToWindow('Update downloaded.')
handleSendEvent({ type: ipcMainWindowNames.update_downloaded, info })
})
mainOn(ipcMainWindowNames.quit_update, () => {
global.isQuitting = true
setTimeout(() => {
autoUpdater.quitAndInstall(true, true)
}, 1000)
})
// 由于集合安装包中不包含win arm版,这将会导致arm版更新失败
if (isWin && process.arch.includes('arm')) {
handleSendEvent({ type: ipcMainWindowNames.update_error, info: 'failed' })
} else {
autoUpdater.checkForUpdates()
}
}
-281
View File
@@ -1,281 +0,0 @@
// 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
@@ -1,25 +0,0 @@
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
@@ -1,130 +0,0 @@
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
@@ -1,84 +0,0 @@
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
@@ -1,105 +0,0 @@
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
-95
View File
@@ -1,95 +0,0 @@
const fs = require('fs')
const fsPromises = fs.promises
const path = require('path')
const getImgSize = require('image-size')
const request = require('request')
const FlacProcessor = require('./flac-metadata')
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')
request(picUrl)
.on('response', respones => {
if (respones.statusCode !== 200 && respones.statusCode != 206) return writeMeta(filePath, meta)
respones
.pipe(fs.createWriteStream(picPath))
.on('finish', async() => {
if (respones.complete) {
await writeMeta(filePath, meta, picPath)
} else {
writeMeta(filePath, meta)
}
fs.unlink(picPath, err => {
if (err) console.log(err.message)
})
})
.on('error', err => {
if (err) console.log(err.message)
writeMeta(filePath, meta)
})
})
.on('error', err => {
if (err) console.log(err.message)
writeMeta(filePath, meta)
})
}
@@ -4,6 +4,8 @@
// exports.getAvailableFontFamilies = getAvailableFontFamilies
const fontList = require('font-list')
import { getFonts } from 'font-list'
exports.getFonts = fontList.getFonts
export {
getFonts,
}
-49
View File
@@ -1,49 +0,0 @@
const { windowSizeList } = require('../../common/config')
const { objectDeepMerge, throttle, initSetting } = require('../../common/utils')
const getStore = require('@common/store')
exports.getWindowSizeInfo = ({ windowSizeId = 1 } = {}) => {
return windowSizeList.find(i => i.id === windowSizeId) || windowSizeList[0]
}
exports.getAppSetting = () => {
return getStore('config').get('setting')
}
exports.getAppHotKeyConfig = () => {
const electronStore_hotKey = getStore('hotKey')
return {
global: electronStore_hotKey.get('global'),
local: electronStore_hotKey.get('local'),
}
}
const saveHotKeyConfig = throttle(config => {
for (const key of Object.keys(config)) {
global.appHotKey.config[key] = config[key]
getStore('hotKey').set(key, config[key])
}
})
exports.saveAppHotKeyConfig = config => {
saveHotKeyConfig(config)
}
// const saveSetting = throttle(n => {
// electronStore_config.set('setting', n)
// })
exports.updateSetting = (settings) => {
objectDeepMerge(global.appSetting, settings)
getStore('config').set('setting', global.appSetting)
exports.initSetting(false)
}
exports.initSetting = (isShowErrorAlert = true) => {
const info = initSetting(isShowErrorAlert)
global.appSetting = info.setting
global.appSettingVersion = info.version
}
exports.openDevTools = webContents => {
webContents.openDevTools({
mode: 'undocked',
})
}
+277
View File
@@ -0,0 +1,277 @@
import { throttle } from '@common/utils'
import migrateSetting from '@common/utils/migrateSetting'
import getStore from '@main/utils/store'
import { STORE_NAMES, URL_SCHEME_RXP } from '@common/constants'
import defaultSetting from '@common/defaultSetting'
import defaultHotKey from '@common/defaultHotKey'
import { migrateDataJson, migrateHotKey, migrateUserApi, parseDataFile } from './migrate'
import { commonColorNames, commonLightColorValues, commonDarkColorValues } from '@common/config'
import { nativeTheme } from 'electron'
import { joinPath } from '@common/utils/nodejs'
import themes from '@common/theme/index.json'
export const parseEnvParams = (): { cmdParams: LX.CmdParams, deeplink: string | null } => {
const cmdParams: LX.CmdParams = {}
let deeplink = null
const rx = /^-\w+/
for (let param of process.argv) {
if (URL_SCHEME_RXP.test(param)) {
deeplink = param
}
if (!rx.test(param)) continue
param = param.substring(1)
let index = param.indexOf('=')
if (index < 0) {
cmdParams[param] = true
} else {
cmdParams[param.substring(0, index)] = param.substring(index + 1)
}
}
return {
cmdParams,
deeplink,
}
}
const primitiveType = ['string', 'boolean', 'number']
const checkPrimitiveType = (val: any): boolean => val === null || primitiveType.includes(typeof val)
// const handleMergeSetting = (defaultSetting: LX.AppSetting, currentSetting: Partial<LX.AppSetting>) => {
// const updatedSettingKeys: Array<keyof LX.AppSetting> = []
// for (const key of Object.keys(defaultSetting) as Array<keyof LX.AppSetting>) {
// const currentValue: any = currentSetting[key]
// const isPrimitive = checkPrimitiveType(currentValue)
// // if (checkPrimitiveType(value)) {
// if (!isPrimitive) continue
// updatedSettingKeys.push(key)
// // @ts-expect-error
// defaultSetting[key] = currentValue
// // } else {
// // if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue)
// // }
// }
// return {
// setting: defaultSetting,
// updatedSettingKeys,
// }
// }
export const mergeSetting = (originSetting: LX.AppSetting, targetSetting?: Partial<LX.AppSetting> | null): {
setting: LX.AppSetting
updatedSettingKeys: Array<keyof LX.AppSetting>
updatedSetting: Partial<LX.AppSetting>
} => {
let originSettingCopy: LX.AppSetting = JSON.parse(JSON.stringify(originSetting))
// const defaultVersion = targetSettingCopy.version
const updatedSettingKeys: Array<keyof LX.AppSetting> = []
const updatedSetting: Partial<LX.AppSetting> = {}
if (targetSetting) {
const originSettingKeys = Object.keys(originSettingCopy)
const targetSettingKeys = Object.keys(targetSetting)
if (originSettingKeys.length > targetSettingKeys.length) {
for (const key of targetSettingKeys as Array<keyof LX.AppSetting>) {
const targetValue: any = targetSetting[key]
const isPrimitive = checkPrimitiveType(targetValue)
// if (checkPrimitiveType(value)) {
if (!isPrimitive || targetValue == originSettingCopy[key] || originSettingCopy[key] === undefined) continue
updatedSettingKeys.push(key)
updatedSetting[key] = targetValue
// @ts-expect-error
originSettingCopy[key] = targetValue
// } else {
// if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue)
// }
}
} else {
for (const key of originSettingKeys as Array<keyof LX.AppSetting>) {
const targetValue: any = targetSetting[key]
const isPrimitive = checkPrimitiveType(targetValue)
// if (checkPrimitiveType(value)) {
if (!isPrimitive || targetValue == originSettingCopy[key]) continue
updatedSettingKeys.push(key)
updatedSetting[key] = targetValue
// @ts-expect-error
originSettingCopy[key] = targetValue
// } else {
// if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue)
// }
}
}
}
return {
setting: originSettingCopy,
updatedSettingKeys,
updatedSetting,
}
}
export const updateSetting = (setting?: Partial<LX.AppSetting>, isInit: boolean = false) => {
const electronStore_config = getStore(STORE_NAMES.APP_SETTINGS)
let originSetting: LX.AppSetting
if (isInit) {
if (setting) setting = migrateSetting(setting)
originSetting = defaultSetting
} else originSetting = global.lx.appSetting
const result = mergeSetting(originSetting, setting)
result.setting.version = defaultSetting.version
electronStore_config.set({ version: result.setting.version, setting: result.setting })
return result
}
/**
* 初始化设置
*/
export const initSetting = async() => {
const electronStore_config = getStore(STORE_NAMES.APP_SETTINGS)
let setting = electronStore_config.get('setting') as LX.AppSetting | undefined
// migrate setting
if (!setting) {
const config = await parseDataFile<{ setting?: any }>('config.json')
if (config?.setting) setting = config.setting as LX.AppSetting
await migrateUserApi()
await migrateDataJson()
}
// console.log(setting)
return updateSetting(setting, true)
}
/**
* 初始化快捷键设置
*/
export const initHotKey = async() => {
const electronStore_hotKey = getStore(STORE_NAMES.HOTKEY)
let localConfig = electronStore_hotKey.get('local') as LX.HotKeyConfig | null
let globalConfig = electronStore_hotKey.get('global') as LX.HotKeyConfig | null
if (!localConfig) {
// migrate hotKey
const config = await migrateHotKey()
if (config) {
localConfig = config.local
globalConfig = config.global
} else {
localConfig = JSON.parse(JSON.stringify(defaultHotKey.global))
globalConfig = JSON.parse(JSON.stringify(defaultHotKey.local))
}
electronStore_hotKey.set('local', localConfig)
electronStore_hotKey.set('global', globalConfig)
}
return {
local: localConfig as LX.HotKeyConfig,
global: globalConfig as LX.HotKeyConfig,
}
}
type HotKeyType = 'local' | 'global'
const saveHotKeyConfig = throttle<[LX.HotKeyConfigAll]>((config: LX.HotKeyConfigAll) => {
for (const key of Object.keys(config) as HotKeyType[]) {
global.lx.hotKey.config[key] = config[key]
getStore(STORE_NAMES.HOTKEY).set(key, config[key])
}
})
export const saveAppHotKeyConfig = (config: LX.HotKeyConfigAll) => {
saveHotKeyConfig(config)
}
export const openDevTools = (webContents: Electron.WebContents) => {
webContents.openDevTools({
mode: 'undocked',
})
}
let userThemes: LX.Theme[]
export const getAllThemes = () => {
if (!userThemes) userThemes = getStore(STORE_NAMES.THEME).get('themes') as LX.Theme[] | null ?? []
return {
themes,
userThemes,
dataPath: joinPath(global.lxDataPath, 'theme_images'),
}
}
export const saveTheme = (theme: LX.Theme) => {
const targetTheme = userThemes.find(t => t.id === theme.id)
if (targetTheme) Object.assign(targetTheme, theme)
else userThemes.push(theme)
getStore(STORE_NAMES.THEME).set('themes', userThemes)
}
export const removeTheme = (id: string) => {
const index = userThemes.findIndex(t => t.id === id)
if (index < 0) return
userThemes.splice(index, 1)
getStore(STORE_NAMES.THEME).set('themes', userThemes)
}
const copyTheme = (theme: LX.Theme): LX.Theme => {
return {
...theme,
config: {
...theme.config,
extInfo: { ...theme.config.extInfo },
themeColors: { ...theme.config.themeColors },
},
}
}
export const getTheme = () => {
// fs.promises.readdir()
const shouldUseDarkColors = nativeTheme.shouldUseDarkColors
let themeId = global.lx.appSetting['theme.id'] == 'auto'
? shouldUseDarkColors
? global.lx.appSetting['theme.darkId']
: global.lx.appSetting['theme.lightId']
: global.lx.appSetting['theme.id']
// themeId = 'naruto'
// themeId = 'pink'
// themeId = 'black'
let theme = themes.find(theme => theme.id == themeId)
if (!theme) {
userThemes = getStore(STORE_NAMES.THEME).get('themes') as LX.Theme[] | null ?? []
theme = userThemes.find(theme => theme.id == themeId)
if (theme) {
if (theme.config.extInfo['--background-image'] != 'none') {
theme = copyTheme(theme)
theme.config.extInfo['--background-image'] = `url(file:///${joinPath(global.lxDataPath, 'theme_images', theme.config.extInfo['--background-image']).replaceAll('\\', '/')})`
}
} else {
themeId = global.lx.appSetting['theme.id'] == 'auto' && shouldUseDarkColors ? 'black' : 'green'
theme = themes.find(theme => theme.id == themeId) as LX.Theme
}
}
const colors: Record<string, string> = {
...theme.config.themeColors,
...theme.config.extInfo,
}
const vals = theme.isDark ? commonDarkColorValues : commonLightColorValues
for (let i = commonColorNames.length; i--;) {
colors[commonColorNames[i]] = vals[i]
}
return {
shouldUseDarkColors,
theme: {
id: global.lx.appSetting['theme.id'],
name: theme.name,
isDark: theme.isDark,
colors,
},
}
}
+156
View File
@@ -0,0 +1,156 @@
import fs from 'node:fs'
import { checkPath, joinPath } from '@common/utils/nodejs'
import { log } from '@common/utils'
import { toNewMusicInfo } from '@common/utils/tools'
import { APP_EVENT_NAMES, STORE_NAMES } from '@common/constants'
/**
* 读取配置文件
* @returns
*/
export const parseDataFile = async<T>(name: string): Promise<T | null> => {
const path = joinPath(global.lxOldDataPath, name)
if (await checkPath(path)) {
try {
return JSON.parse((await fs.promises.readFile(path)).toString())
} catch (err) {
log.error(err)
}
}
return null
}
interface OldUserListInfo {
name: string
id: string
source?: LX.OnlineSource
sourceListId?: string
locationUpdateTime?: number
list: any[]
}
/**
* 迁移 v2.0.0 之前的 list data
* @returns
*/
export const migrateDBData = async() => {
let playList = await parseDataFile<{ defaultList?: { list: any[] }, loveList?: { list: any[] }, tempList?: { list: any[] }, userList?: OldUserListInfo[] }>('playList.json')
let listDataAll: LX.List.ListDataFull = {
defaultList: [],
loveList: [],
userList: [],
tempList: [],
}
let isRequiredSave = false
if (playList) {
if (playList.defaultList) listDataAll.defaultList = playList.defaultList.list.map(m => toNewMusicInfo(m))
if (playList.loveList) listDataAll.loveList = playList.loveList.list.map(m => toNewMusicInfo(m))
if (playList.tempList) listDataAll.tempList = playList.tempList.list.map(m => toNewMusicInfo(m))
if (playList.userList) {
listDataAll.userList = playList.userList.map(l => {
return {
...l,
locationUpdateTime: l.locationUpdateTime ?? null,
list: l.list.map(m => toNewMusicInfo(m)),
}
})
}
isRequiredSave = true
} else {
const config = await parseDataFile<{ list?: { defaultList?: any[], loveList?: any[] } }>('config.json')
if (config?.list) {
const list = config.list
if (list.defaultList) listDataAll.defaultList = list.defaultList.map(m => toNewMusicInfo(m))
if (list.loveList) listDataAll.loveList = list.loveList.map(m => toNewMusicInfo(m))
isRequiredSave = true
}
}
if (isRequiredSave) await global.lx.worker.dbService.listDataOverwrite(listDataAll)
const lyricData = await parseDataFile<Record<string, LX.Music.LyricInfo>>('lyrics_edited.json')
if (lyricData) {
for await (const [id, info] of Object.entries(lyricData)) {
await global.lx.worker.dbService.editedLyricAdd(id, info)
}
}
}
// 迁移文件
const migrateFile = async(name: string, targetName: string) => {
let path = joinPath(global.lxDataPath, targetName)
let oldPath = joinPath(global.lxOldDataPath, name)
if (!await checkPath(path) && await checkPath(oldPath)) {
await fs.promises.copyFile(oldPath, path).catch(err => {
log.error(err)
}).catch(err => log.error(err))
}
}
/**
* 迁移 v2.0.0 之前的 data.json
* @returns
*/
export const migrateDataJson = async() => {
const path = joinPath(global.lxDataPath, 'data.json')
if (await checkPath(path)) return
const oldDataFile = await parseDataFile<{
searchHistoryList?: string[]
playInfo?: any
listPrevSelectId?: any
listPosition?: any
listUpdateInfo?: any
}>('data.json')
if (!oldDataFile) return
const newData: any = {}
if (oldDataFile.searchHistoryList) newData.searchHistoryList = oldDataFile.searchHistoryList
if (oldDataFile.playInfo) newData.playInfo = oldDataFile.playInfo
if (oldDataFile.listPrevSelectId) newData.listPrevSelectId = oldDataFile.listPrevSelectId
if (oldDataFile.listPosition) newData.listScrollPosition = oldDataFile.listPosition
if (oldDataFile.listUpdateInfo) newData.listUpdateInfo = oldDataFile.listUpdateInfo
await fs.promises.writeFile(path, JSON.stringify(newData)).catch(err => log.error(err))
}
const hotKeyNameMap = {
mainWindow: APP_EVENT_NAMES.winMainName,
winLyric: APP_EVENT_NAMES.winLyricName,
} as const
const updateHotKeyTypeName = (config: LX.HotKeyConfig) => {
for (const keyConfig of Object.values(config.keys)) {
if (hotKeyNameMap[keyConfig.type as keyof typeof hotKeyNameMap]) keyConfig.type = hotKeyNameMap[keyConfig.type as keyof typeof hotKeyNameMap]
}
}
/**
* 迁移 v2.0.0 之前的 hotkey
* @returns
*/
export const migrateHotKey = async() => {
const oldConfig = await parseDataFile<LX.HotKeyConfigAll>('hotKey.json')
if (oldConfig) {
let localConfig: LX.HotKeyConfig
let globalConfig: LX.HotKeyConfig
updateHotKeyTypeName(oldConfig.local)
updateHotKeyTypeName(oldConfig.global)
localConfig = oldConfig.local
globalConfig = oldConfig.global
// 移除v1.0.1及之前设置的全局声音媒体快捷键接管
if (globalConfig.keys.VolumeUp) {
delete globalConfig.keys.VolumeUp
delete globalConfig.keys.VolumeDown
delete globalConfig.keys.VolumeMute
}
return {
local: localConfig,
global: globalConfig,
}
}
return null
}
/**
* 迁移 v2.0.0 之前的user api
* @returns
*/
export const migrateUserApi = async() => migrateFile('userApi.json', STORE_NAMES.USER_API + '.json')
-60
View File
@@ -1,60 +0,0 @@
const NodeID3 = require('node-id3')
const path = require('path')
const fs = require('fs')
const request = require('request')
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')
request(meta.APIC)
.on('response', respones => {
if (respones.statusCode !== 200 && respones.statusCode != 206) {
delete meta.APIC
handleWriteMeta(meta, filePath)
return
}
respones
.pipe(fs.createWriteStream(picPath))
.on('finish', () => {
if (respones.complete) {
meta.APIC = picPath
handleWriteMeta(meta, filePath)
} else {
delete meta.APIC
handleWriteMeta(meta, filePath)
}
fs.unlink(picPath, err => {
if (err) console.log(err.message)
})
}).on('error', err => {
if (err) console.log(err.message)
delete meta.APIC
handleWriteMeta(meta, filePath)
fs.unlink(picPath, err => {
if (err) console.log(err.message)
})
})
})
.on('error', err => {
if (err) console.log(err.message)
delete meta.APIC
handleWriteMeta(meta, filePath)
})
}
-14
View File
@@ -1,14 +0,0 @@
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
}
}
@@ -1,18 +0,0 @@
todo.md
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
tsconfig.tsbuildinfo
report.*.json
.eslintcache
.DS_Store
.idea
.vscode
.yarn
*.suo
*.ntvs*
*.njsproj
*.sln
@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2021 Shigma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,10 +0,0 @@
# simplify-chinese
Convert chinese characters between simplified form and tranditional form / 汉字简繁体转换工具。
```js
const { simplify, tranditionalize } = require('simplify-chinese')
console.log(simplify('窩窩頭')) // 窝窝头
console.log(tranditionalize('窝窝头')) // 窩窩頭
```
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
export function simplify(source: string): string
export function tranditionalize(source: string): string
@@ -1,30 +0,0 @@
const { simplified, traditional } = require('./chinese')
const stMap = new Map()
const tsMap = new Map()
simplified.split('').forEach((char, index) => {
stMap.set(char, traditional[index])
tsMap.set(traditional[index], char)
})
function simplify(source) {
let result = []
for (const char of source) {
result.push(tsMap.get(char) || char)
}
return result.join('')
}
function tranditionalize(source) {
let result = []
for (const char of source) {
result.push(stMap.get(char) || char)
}
return result.join('')
}
module.exports = {
simplify,
tranditionalize,
}
@@ -1,10 +0,0 @@
{
"name": "simplify-chinese",
"description": "Convert chinese characters between simplified form and tranditional form 汉字简繁体转换工具",
"version": "1.1.0",
"main": "index.js",
"typings": "index.d.ts",
"repository": "https://github.com/koishijs/simplify-chinese.git",
"author": "Shigma <1700011071@pku.edu.cn>",
"license": "MIT"
}
+51
View File
@@ -0,0 +1,51 @@
import Store from 'electron-store'
import { dialog, shell } from 'electron'
import { join } from 'path'
import fs from 'fs'
import log from 'electron-log'
interface Stores {
[key: string]: Store
}
const stores: Stores = {}
/**
* 获取 Store 对象
* @param {string} name store 名
* @param {boolean|undefined} isIgnoredError 是否忽略错误
* @param {boolean|undefined} isShowErrorAlert=true 是否显示错误弹窗
* @returns Store
*/
export default (name: string, isIgnoredError = true, isShowErrorAlert = true): Store => {
if (stores[name]) return stores[name]
let store: Store
try {
store = stores[name] = new Store({ name, clearInvalidConfig: false, cwd: global.lxDataPath })
} catch (err: any) {
const error = err as Error
log.error(error)
if (!isIgnoredError) throw error
const backPath = join(global.lxDataPath, name + '.json.bak')
fs.copyFileSync(join(global.lxDataPath, name + '.json'), backPath)
if (isShowErrorAlert) {
dialog.showMessageBoxSync({
type: 'error',
message: name + ' data load error',
detail: `We have helped you back up the old ${name} file to: ${backPath}\nYou can try to repair and restore it manually\n\nError detail: ${error.message}`,
})
shell.showItemInFolder(backPath)
}
store = new Store({ name, clearInvalidConfig: true })
}
return store
}
export {
Store,
}