新增FLAC格式音乐标签信息写入
parent
bc7c3968b6
commit
83f0ca918a
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lx-music-desktop",
|
"name": "lx-music-desktop",
|
||||||
"version": "0.17.0",
|
"version": "0.18.0",
|
||||||
"description": "一个免费的音乐下载助手",
|
"description": "一个免费的音乐下载助手",
|
||||||
"main": "./dist/electron/main.js",
|
"main": "./dist/electron/main.js",
|
||||||
"productName": "lx-music-desktop",
|
"productName": "lx-music-desktop",
|
||||||
|
@ -141,13 +141,13 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
|
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.8.7",
|
"@babel/core": "^7.9.0",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
"@babel/polyfill": "^7.8.7",
|
"@babel/polyfill": "^7.8.7",
|
||||||
"@babel/preset-env": "^7.8.7",
|
"@babel/preset-env": "^7.9.0",
|
||||||
"autoprefixer": "^9.7.4",
|
"autoprefixer": "^9.7.4",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.1.0",
|
||||||
"babel-minify-webpack-plugin": "^0.3.1",
|
"babel-minify-webpack-plugin": "^0.3.1",
|
||||||
"babel-preset-minify": "^0.5.1",
|
"babel-preset-minify": "^0.5.1",
|
||||||
"browserslist": "^4.10.0",
|
"browserslist": "^4.10.0",
|
||||||
|
@ -164,9 +164,9 @@
|
||||||
"electron-builder": "^22.4.1",
|
"electron-builder": "^22.4.1",
|
||||||
"electron-debug": "^3.0.1",
|
"electron-debug": "^3.0.1",
|
||||||
"electron-devtools-installer": "^2.2.4",
|
"electron-devtools-installer": "^2.2.4",
|
||||||
"electron-to-chromium": "^1.3.379",
|
"electron-to-chromium": "^1.3.380",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-standard": "^14.1.0",
|
"eslint-config-standard": "^14.1.1",
|
||||||
"eslint-formatter-friendly": "^7.0.0",
|
"eslint-formatter-friendly": "^7.0.0",
|
||||||
"eslint-loader": "^3.0.3",
|
"eslint-loader": "^3.0.3",
|
||||||
"eslint-plugin-html": "^6.0.0",
|
"eslint-plugin-html": "^6.0.0",
|
||||||
|
@ -209,11 +209,10 @@
|
||||||
"electron-log": "^4.1.0",
|
"electron-log": "^4.1.0",
|
||||||
"electron-store": "^5.1.1",
|
"electron-store": "^5.1.1",
|
||||||
"electron-updater": "^4.2.5",
|
"electron-updater": "^4.2.5",
|
||||||
"flac-metadata": "^0.1.1",
|
|
||||||
"js-htmlencode": "^0.3.0",
|
"js-htmlencode": "^0.3.0",
|
||||||
"lrc-file-parser": "^1.0.1",
|
"lrc-file-parser": "^1.0.1",
|
||||||
"needle": "^2.3.3",
|
"needle": "^2.3.3",
|
||||||
"node-id3": "^0.1.14",
|
"node-id3": "^0.1.15",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-electron": "^1.0.6",
|
"vue-electron": "^1.0.6",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
### 新增
|
### 新增
|
||||||
|
|
||||||
|
- 新增FLAC格式音乐标签信息写入
|
||||||
|
|
||||||
### 优化
|
### 优化
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
// https://github.com/claus/flac-metadata
|
||||||
|
|
||||||
|
module.exports.Processor = require('./lib/Processor')
|
||||||
|
|
||||||
|
module.exports.data = {
|
||||||
|
MetaDataBlock: require('./lib/data/MetaDataBlock'),
|
||||||
|
MetaDataBlockStreamInfo: require('./lib/data/MetaDataBlockStreamInfo'),
|
||||||
|
MetaDataBlockVorbisComment: require('./lib/data/MetaDataBlockVorbisComment'),
|
||||||
|
MetaDataBlockPicture: require('./lib/data/MetaDataBlockPicture'),
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
const Transform = require('stream').Transform
|
||||||
|
|
||||||
|
const MetaDataBlock = require('./data/MetaDataBlock')
|
||||||
|
const MetaDataBlockStreamInfo = require('./data/MetaDataBlockStreamInfo')
|
||||||
|
const MetaDataBlockVorbisComment = require('./data/MetaDataBlockVorbisComment')
|
||||||
|
const MetaDataBlockPicture = require('./data/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
|
||||||
|
|
||||||
|
if (!(this instanceof Processor)) return new Processor(options)
|
||||||
|
if (options && !!options.parseMetaDataBlocks) { this.parseMetaDataBlocks = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
_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.emit('postprocess', this.mdb)
|
||||||
|
this.state = this.mdbLast ? STATE_PASS_THROUGH : 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 Processor.MDB_TYPE_STREAMINFO:
|
||||||
|
this.mdb = new MetaDataBlockStreamInfo(this.mdbLast)
|
||||||
|
break
|
||||||
|
case Processor.MDB_TYPE_VORBIS_COMMENT:
|
||||||
|
this.mdb = new MetaDataBlockVorbisComment(this.mdbLast)
|
||||||
|
break
|
||||||
|
case Processor.MDB_TYPE_PICTURE:
|
||||||
|
this.mdb = new MetaDataBlockPicture(this.mdbLast)
|
||||||
|
break
|
||||||
|
case Processor.MDB_TYPE_PADDING:
|
||||||
|
case Processor.MDB_TYPE_APPLICATION:
|
||||||
|
case Processor.MDB_TYPE_SEEKTABLE:
|
||||||
|
case Processor.MDB_TYPE_CUESHEET:
|
||||||
|
case Processor.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 {
|
||||||
|
// The consumer may change the MDB's isLast flag in the preprocess handler.
|
||||||
|
// Here that flag is updated in the MDB header.
|
||||||
|
if (this.mdbLast !== this.mdb.isLast) {
|
||||||
|
if (this.mdb.isLast) {
|
||||||
|
header |= 0x80000000
|
||||||
|
} else {
|
||||||
|
header &= 0x7fffffff
|
||||||
|
}
|
||||||
|
slice.writeUInt32BE(header >>> 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
|
@ -1,32 +1,32 @@
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const flac = require('flac-metadata')
|
const { Processor: FlacProcessor, data: { MetaDataBlockVorbisComment: FlacComment } } = require('./flac-metadata')
|
||||||
|
const vendor = 'reference libFLAC 1.2.1 20070917'
|
||||||
|
|
||||||
module.exports = (filenPath, meta) => {
|
module.exports = (filenPath, meta) => {
|
||||||
const reader = fs.createReadStream(filenPath)
|
const reader = fs.createReadStream(filenPath)
|
||||||
const tempPath = filenPath + '.lxmtemp'
|
const tempPath = filenPath + '.lxmtemp'
|
||||||
const writer = fs.createWriteStream(tempPath)
|
const writer = fs.createWriteStream(tempPath)
|
||||||
const processor = new flac.Processor()
|
const flacProcessor = new FlacProcessor()
|
||||||
if (meta.APIC) delete meta.APIC
|
if (meta.APIC) delete meta.APIC
|
||||||
|
|
||||||
const comments = []
|
const comments = []
|
||||||
for (const key in meta) {
|
for (const key in meta) {
|
||||||
comments.push(`${key.toUpperCase()}=${meta[key]}`)
|
comments.push(`${key.toUpperCase()}=${meta[key]}`)
|
||||||
}
|
}
|
||||||
const vendor = 'lx-music-desktop'
|
|
||||||
|
|
||||||
processor.on('preprocess', function(mdb) {
|
let isInjected = false
|
||||||
|
flacProcessor.on('preprocess', function(mdb) {
|
||||||
// Remove existing VORBIS_COMMENT block, if any.
|
// Remove existing VORBIS_COMMENT block, if any.
|
||||||
if (mdb.type === flac.Processor.MDB_TYPE_VORBIS_COMMENT) {
|
if (mdb.type === flacProcessor.MDB_TYPE_VORBIS_COMMENT) mdb.remove()
|
||||||
mdb.remove()
|
|
||||||
}
|
|
||||||
// Inject new VORBIS_COMMENT block.
|
// Inject new VORBIS_COMMENT block.
|
||||||
if (mdb.removed || mdb.isLast) {
|
if ((mdb.removed || mdb.isLast) && !isInjected) {
|
||||||
let mdbVorbis = flac.data.MetaDataBlockVorbisComment.create(mdb.isLast, vendor, comments)
|
isInjected = true
|
||||||
|
let mdbVorbis = FlacComment.create(mdb.isLast, vendor, comments)
|
||||||
this.push(mdbVorbis.publish())
|
this.push(mdbVorbis.publish())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
reader.pipe(processor).pipe(writer).on('finish', () => {
|
reader.pipe(flacProcessor).pipe(writer).on('finish', () => {
|
||||||
fs.unlink(filenPath, err => {
|
fs.unlink(filenPath, err => {
|
||||||
if (err) return console.log(err.message)
|
if (err) return console.log(err.message)
|
||||||
fs.rename(tempPath, filenPath, err => {
|
fs.rename(tempPath, filenPath, err => {
|
||||||
|
|
|
@ -91,7 +91,7 @@ const getUrl = (downloadInfo, isRefresh) => {
|
||||||
* @param {*} isEmbedPic // 是否嵌入图片
|
* @param {*} isEmbedPic // 是否嵌入图片
|
||||||
*/
|
*/
|
||||||
const saveMeta = (downloadInfo, filePath, isEmbedPic) => {
|
const saveMeta = (downloadInfo, filePath, isEmbedPic) => {
|
||||||
if (downloadInfo.type === 'ape' || downloadInfo.type === 'flac') return
|
if (downloadInfo.type === 'ape') return
|
||||||
const promise = isEmbedPic
|
const promise = isEmbedPic
|
||||||
? downloadInfo.musicInfo.img
|
? downloadInfo.musicInfo.img
|
||||||
? Promise.resolve(downloadInfo.musicInfo.img)
|
? Promise.resolve(downloadInfo.musicInfo.img)
|
||||||
|
@ -104,6 +104,12 @@ const saveMeta = (downloadInfo, filePath, isEmbedPic) => {
|
||||||
album: downloadInfo.musicInfo.albumName,
|
album: downloadInfo.musicInfo.albumName,
|
||||||
APIC: url,
|
APIC: url,
|
||||||
})
|
})
|
||||||
|
}).catch(() => {
|
||||||
|
setMeta(filePath, {
|
||||||
|
title: downloadInfo.musicInfo.name,
|
||||||
|
artist: downloadInfo.musicInfo.singer,
|
||||||
|
album: downloadInfo.musicInfo.albumName,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { getProxyInfo } from './index'
|
||||||
|
|
||||||
const request = (url, options, callback) => {
|
const request = (url, options, callback) => {
|
||||||
let data
|
let data
|
||||||
if (options.method == 'get') options.headers['Content-Length'] = 0
|
|
||||||
if (options.body) {
|
if (options.body) {
|
||||||
data = options.body
|
data = options.body
|
||||||
} else if (options.form) {
|
} else if (options.form) {
|
||||||
|
|
Loading…
Reference in New Issue