新增FLAC格式音乐标签信息写入与封面嵌入

pull/225/head
lyswhut 2020-03-22 18:33:51 +08:00
parent 343c53b41c
commit a4c5670409
14 changed files with 383 additions and 254 deletions

29
package-lock.json generated
View File

@ -9846,11 +9846,12 @@
"dev": true "dev": true
}, },
"image-size": { "image-size": {
"version": "0.5.5", "version": "0.8.3",
"resolved": "https://registry.npm.taobao.org/image-size/download/image-size-0.5.5.tgz", "resolved": "https://registry.npm.taobao.org/image-size/download/image-size-0.8.3.tgz",
"integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", "integrity": "sha1-8LVohX4DTym6/9NwE1h/LAyti0Y=",
"dev": true, "requires": {
"optional": true "queue": "6.0.1"
}
}, },
"import-cwd": { "import-cwd": {
"version": "2.1.0", "version": "2.1.0",
@ -9940,8 +9941,7 @@
"inherits": { "inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz?cache=0&sync_timestamp=1560975547815&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Finherits%2Fdownload%2Finherits-2.0.4.tgz", "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz?cache=0&sync_timestamp=1560975547815&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Finherits%2Fdownload%2Finherits-2.0.4.tgz",
"integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=", "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w="
"dev": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -10753,6 +10753,13 @@
"tslib": "^1.10.0" "tslib": "^1.10.0"
}, },
"dependencies": { "dependencies": {
"image-size": {
"version": "0.5.5",
"resolved": "https://registry.npm.taobao.org/image-size/download/image-size-0.5.5.tgz",
"integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
"dev": true,
"optional": true
},
"mime": { "mime": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npm.taobao.org/mime/download/mime-1.6.0.tgz", "resolved": "https://registry.npm.taobao.org/mime/download/mime-1.6.0.tgz",
@ -13415,6 +13422,14 @@
"integrity": "sha1-YOWl/WSn+L+k0qsu1v30yFutFU4=", "integrity": "sha1-YOWl/WSn+L+k0qsu1v30yFutFU4=",
"dev": true "dev": true
}, },
"queue": {
"version": "6.0.1",
"resolved": "https://registry.npm.taobao.org/queue/download/queue-6.0.1.tgz",
"integrity": "sha1-q9WlsDdpEvBwolcp4Lan1WVoN5E=",
"requires": {
"inherits": "~2.0.3"
}
},
"randombytes": { "randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npm.taobao.org/randombytes/download/randombytes-2.1.0.tgz", "resolved": "https://registry.npm.taobao.org/randombytes/download/randombytes-2.1.0.tgz",

View File

@ -209,6 +209,7 @@
"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",
"image-size": "^0.8.3",
"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",

View File

@ -1,6 +1,6 @@
### 新增 ### 新增
- 新增FLAC格式音乐标签信息写入 - 新增FLAC格式音乐标签信息写入与封面嵌入
### 优化 ### 优化

View File

@ -1,10 +1,280 @@
// https://github.com/claus/flac-metadata // https://github.com/claus/flac-metadata
module.exports.Processor = require('./lib/Processor') const Transform = require('stream').Transform
module.exports.data = { const MetaDataBlock = require('./lib/MetaDataBlock')
MetaDataBlock: require('./lib/data/MetaDataBlock'), const MetaDataBlockStreamInfo = require('./lib/MetaDataBlockStreamInfo')
MetaDataBlockStreamInfo: require('./lib/data/MetaDataBlockStreamInfo'), const MetaDataBlockVorbisComment = require('./lib/MetaDataBlockVorbisComment')
MetaDataBlockVorbisComment: require('./lib/data/MetaDataBlockVorbisComment'), const MetaDataBlockPicture = require('./lib/MetaDataBlockPicture')
MetaDataBlockPicture: require('./lib/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
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.mdb =
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

View File

@ -1,214 +0,0 @@
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

View File

@ -1,38 +1,94 @@
const fs = require('fs') const fs = require('fs')
const { Processor: FlacProcessor, data: { MetaDataBlockVorbisComment: FlacComment } } = require('./flac-metadata') 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 vendor = 'reference libFLAC 1.2.1 20070917'
module.exports = (filenPath, meta) => {
const reader = fs.createReadStream(filenPath) const writeMeta = (filePath, meta, picPath) => {
const tempPath = filenPath + '.lxmtemp' const comments = Object.keys(meta).map(key => `${key.toUpperCase()}=${meta[key]}`)
const data = {
vorbis: {
vendor,
comments,
},
}
if (picPath) {
const apicData = Buffer.from(fs.readFileSync(picPath, 'binary'), 'binary')
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 writer = fs.createWriteStream(tempPath)
const flacProcessor = new FlacProcessor() const flacProcessor = new FlacProcessor()
if (meta.APIC) delete meta.APIC flacProcessor.writeMeta(data)
const comments = []
for (const key in meta) {
comments.push(`${key.toUpperCase()}=${meta[key]}`)
}
let isInjected = false
flacProcessor.on('preprocess', function(mdb) {
// Remove existing VORBIS_COMMENT block, if any.
if (mdb.type === flacProcessor.MDB_TYPE_VORBIS_COMMENT) mdb.remove()
// Inject new VORBIS_COMMENT block.
if ((mdb.removed || mdb.isLast) && !isInjected) {
isInjected = true
let mdbVorbis = FlacComment.create(mdb.isLast, vendor, comments)
this.push(mdbVorbis.publish())
}
})
reader.pipe(flacProcessor).pipe(writer).on('finish', () => { reader.pipe(flacProcessor).pipe(writer).on('finish', () => {
fs.unlink(filenPath, err => { fs.unlink(filePath, err => {
if (err) return console.log(err.message) if (err) return console.log(err.message)
fs.rename(tempPath, filenPath, err => { fs.rename(tempPath, filePath, err => {
if (err) console.log(err.message) 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 picPath = filePath.replace(/\.flac$/, '') + path.extname(picUrl).replace(extReg, '$1')
request(picUrl)
.on('response', respones => {
if (respones.statusCode !== 200 && respones.statusCode != 206) return writeMeta(filePath, meta)
respones
.pipe(fs.createWriteStream(picPath))
.on('finish', () => {
if (respones.complete) {
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)
})
}

View File

@ -26,6 +26,7 @@ module.exports = (filePath, meta) => {
NodeID3.write(meta, filePath) NodeID3.write(meta, filePath)
} else { } else {
delete meta.APIC delete meta.APIC
NodeID3.write(meta, filePath)
} }
fs.unlink(picPath, err => { fs.unlink(picPath, err => {
if (err) console.log(err.message) if (err) console.log(err.message)

View File

@ -58,7 +58,7 @@
"download_name_title": "下载歌曲时的命名方式", "download_name_title": "下载歌曲时的命名方式",
"download_name": "文件命名方式", "download_name": "文件命名方式",
"download_embed_pic_title": "是否将封面嵌入音频文件中", "download_embed_pic_title": "是否将封面嵌入音频文件中",
"download_embed_pic": "封面嵌入只支持MP3格式", "download_embed_pic": "封面嵌入",
"download_lyric_title": "是否同时下载歌词文件", "download_lyric_title": "是否同时下载歌词文件",
"download_lyric": "歌词下载", "download_lyric": "歌词下载",
"download_name1": "歌名 - 歌手", "download_name1": "歌名 - 歌手",

View File

@ -54,7 +54,7 @@
"download_name_title": "下載歌曲時的命名方式", "download_name_title": "下載歌曲時的命名方式",
"download_name": "文件命名方式", "download_name": "文件命名方式",
"download_embed_pic_title": "是否將封面嵌入音頻文件中", "download_embed_pic_title": "是否將封面嵌入音頻文件中",
"download_embed_pic": "封面嵌入只支持MP3格式", "download_embed_pic": "封面嵌入",
"download_lyric_title": "是否同時下載歌詞文件", "download_lyric_title": "是否同時下載歌詞文件",
"download_lyric": "歌詞下載", "download_lyric": "歌詞下載",
"download_name1": "歌名 - 歌手", "download_name1": "歌名 - 歌手",

View File

@ -58,7 +58,7 @@
"download_name_title": "Naming when downloading songs", "download_name_title": "Naming when downloading songs",
"download_name": "File naming", "download_name": "File naming",
"download_embed_pic_title": "Whether to embed the cover in the audio file", "download_embed_pic_title": "Whether to embed the cover in the audio file",
"download_embed_pic": "Cover embedding (only supports MP3 format)", "download_embed_pic": "Cover embedding",
"download_lyric_title": "Whether to download lyrics files at the same time", "download_lyric_title": "Whether to download lyrics files at the same time",
"download_lyric": "Lyrics download", "download_lyric": "Lyrics download",
"download_name1": "name - singer", "download_name1": "name - singer",