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
+71
View File
@@ -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
+80
View File
@@ -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
View File
@@ -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
+14
View File
@@ -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
}
}
+38
View File
@@ -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)
}
})
}