更换下载库

This commit is contained in:
lyswhut
2019-11-16 20:16:09 +08:00
parent 967093328f
commit 6d935d8589
11 changed files with 441 additions and 149 deletions
+50 -49
View File
@@ -53,7 +53,7 @@ const getExt = type => {
}
}
const checkList = (list, musicInfo, type) => list.some(s => s.musicInfo.songmid === musicInfo.songmid && s.type === type)
const checkList = (list, musicInfo, type, ext) => list.some(s => s.musicInfo.songmid === musicInfo.songmid && (s.type === type || s.ext === ext))
const getStartTask = (list, downloadStatus, maxDownloadNum) => {
let downloadCount = 0
@@ -121,11 +121,24 @@ const downloadLyric = (downloadInfo, filePath) => {
})
}
const refreshUrl = function(commit, downloadInfo) {
commit('setStatusText', { downloadInfo, text: '链接失效,正在刷新链接' })
getUrl(downloadInfo, true).then(result => {
commit('updateUrl', { downloadInfo, url: result.url })
commit('setStatusText', { downloadInfo, text: '链接刷新成功' })
dls[downloadInfo.key].refreshUrl(result.url)
dls[downloadInfo.key].start()
}).catch(err => {
console.log(err)
this.dispatch('download/startTask')
})
}
// actions
const actions = {
createDownload({ state, rootState, commit }, { musicInfo, type }) {
if (checkList(state.list, musicInfo, type)) return
let ext = getExt(type)
if (checkList(state.list, musicInfo, type, ext)) return
const downloadInfo = {
isComplate: false,
status: state.downloadStatus.WAITING,
@@ -146,6 +159,11 @@ const actions = {
}
downloadInfo.filePath = path.join(rootState.setting.download.savePath, downloadInfo.fileName)
commit('addTask', downloadInfo)
try { // 删除同路径下的同名文件
fs.unlinkSync(downloadInfo.filePath)
} catch (err) {
if (err.code !== 'ENOENT') return commit('setStatusText', { downloadInfo, text: '文件删除失败' })
}
if (dls[downloadInfo.key]) {
dls[downloadInfo.key].stop().finally(() => {
delete dls[downloadInfo.key]
@@ -167,7 +185,7 @@ const actions = {
if (!downloadInfo) downloadInfo = result
// 开始任务
commit('onDownload', downloadInfo)
commit('onStart', downloadInfo)
commit('setStatusText', { downloadInfo, text: '任务初始化中' })
let msg = checkPath(rootState.setting.download.savePath)
if (msg) return commit('setStatusText', '检查下载目录出错: ' + msg)
@@ -178,12 +196,12 @@ const actions = {
fileName: downloadInfo.fileName,
method: 'get',
override: true,
onEnd() {
if (downloadInfo.progress.progress != '100.00') {
delete dls[downloadInfo.key]
return this.dispatch('download/startTask', downloadInfo)
}
commit('onEnd', downloadInfo)
onCompleted() {
// if (downloadInfo.progress.progress != '100.00') {
// delete dls[downloadInfo.key]
// return this.dispatch('download/startTask', downloadInfo)
// }
commit('onCompleted', downloadInfo)
_this.dispatch('download/startTask')
const filePath = path.join(options.path, options.fileName)
@@ -199,54 +217,40 @@ const actions = {
_this.dispatch('download/startTask')
return
}
let code
if (err.message.includes('Response status was')) {
code = err.message.replace(/Response status was (\d+)$/, '$1')
} else if (err.code === 'ETIMEDOUT' || err.code == 'ENOTFOUND') {
code = err.code
if (err.code == 'ENOTFOUND') {
refreshUrl.call(_this, commit, downloadInfo)
} else {
console.log('Download failed, Attempting Retry')
dls[downloadInfo.key].resume()
dls[downloadInfo.key].start()
commit('setStatusText', { downloadInfo, text: '正在重试' })
return
}
switch (code) {
case '401':
case '403':
case '410':
case 'ETIMEDOUT':
case 'ENOTFOUND':
commit('setStatusText', { downloadInfo, text: '链接失效,正在刷新链接' })
getUrl(downloadInfo, true).then(result => {
commit('updateUrl', { downloadInfo, url: result.url })
commit('setStatusText', { downloadInfo, text: '链接刷新成功' })
dls[downloadInfo.key].url = dls[downloadInfo.key].requestURL = result.url
dls[downloadInfo.key].__initProtocol(result.url)
dls[downloadInfo.key].resume()
}).catch(err => {
console.log(err)
_this.dispatch('download/startTask')
})
}
},
// onStateChanged(state) {
// console.log(state)
// },
onDownload() {
commit('onDownload', downloadInfo)
console.log('on download')
onFail(response) {
commit('onError', downloadInfo)
if (++tryNum[downloadInfo.key] > 2) {
_this.dispatch('download/startTask')
return
}
switch (response.statusCode) {
case 401:
case 403:
case 410:
refreshUrl.call(_this, commit, downloadInfo)
}
},
onStart() {
commit('onStart', downloadInfo)
console.log('on start')
},
onProgress(status) {
commit('onProgress', { downloadInfo, status })
console.log(status)
},
onPause() {
onStop() {
commit('pauseTask', downloadInfo)
_this.dispatch('download/startTask')
},
onResume() {
commit('resumeTask', downloadInfo)
},
}
commit('setStatusText', { downloadInfo, text: '获取URL中...' })
let p = options.url ? Promise.resolve() : getUrl(downloadInfo).then(result => {
@@ -315,9 +319,6 @@ const mutations = {
downloadInfo.status = state.downloadStatus.PAUSE
downloadInfo.statusText = '暂停下载'
},
resumeTask(state, downloadInfo) {
downloadInfo.statusText = '开始下载'
},
setStatusText(state, { downloadInfo, index, text }) { // 设置状态文本
if (downloadInfo) {
downloadInfo.statusText = text
@@ -352,7 +353,7 @@ const mutations = {
state.list[index].status = status
}
},
onEnd(state, downloadInfo) {
onCompleted(state, downloadInfo) {
downloadInfo.isComplate = true
downloadInfo.status = state.downloadStatus.COMPLETED
downloadInfo.statusText = '下载完成'
@@ -361,7 +362,7 @@ const mutations = {
downloadInfo.status = state.downloadStatus.ERROR
downloadInfo.statusText = '任务出错'
},
onDownload(state, downloadInfo) {
onStart(state, downloadInfo) {
downloadInfo.status = state.downloadStatus.RUN
downloadInfo.statusText = '正在下载'
},
+306
View File
@@ -0,0 +1,306 @@
import fs from 'fs'
import path from 'path'
import request from 'request'
import { EventEmitter } from 'events'
import { performance } from 'perf_hooks'
import { STATUS } from './util'
const defaultChunkInfo = {
path: null,
startByte: 0,
endByte: '',
}
const defaultRequestOptions = {
method: 'GET',
headers: {},
}
const defaultOptions = {
}
class Task extends EventEmitter {
/**
*
* @param {String} url download url
* @param {Object} chunkInfo
* @param {Object} options
*/
constructor(url, savePath, filename, options = {}) {
super()
this.resumeLastChunk = null
this.downloadUrl = url
this.chunkInfo = Object.assign({}, defaultChunkInfo, {
path: path.join(savePath, filename),
startByte: 0,
})
if (!this.chunkInfo.endByte) this.chunkInfo.endByte = ''
this.options = Object.assign({}, defaultOptions, options)
this.requestOptions = Object.assign({}, defaultRequestOptions, this.options.requestOptions || {})
if (!this.requestOptions.headers) this.requestOptions.headers = {}
this.progress = {
total: 0,
downloaded: 0,
speed: 0,
}
this.statsEstimate = {
time: 0,
bytes: 0,
prevBytes: 0,
}
this.status = STATUS.idle
}
__init() {
this.status = STATUS.init
const { path, startByte, endByte } = this.chunkInfo
if (startByte) this.requestOptions.headers.range = `bytes=${startByte}-${endByte}`
return new Promise((resolve, reject) => {
if (!path) return resolve()
fs.stat(path, (errStat, stats) => {
if (errStat) {
if (errStat.code !== 'ENOENT') {
this.__handleError(errStat)
reject(errStat)
return
}
} else if (stats.size >= 10) {
fs.open(path, 'r', (errOpen, fd) => {
if (errOpen) {
this.__handleError(errOpen)
reject(errOpen)
return
}
fs.read(fd, Buffer.alloc(10), 0, 10, stats.size - 10, (errRead, bytesRead, buffer) => {
if (errRead) {
this.__handleError(errRead)
reject(errRead)
return
}
fs.close(fd, errClose => {
if (errClose) {
this.__handleError(errClose)
reject(errClose)
return
}
// resume download
// console.log(buffer)
this.resumeLastChunk = buffer
this.progress.downloaded = stats.size
this.requestOptions.headers.range = `bytes=${stats.size - 10}-${endByte || ''}`
resolve()
})
})
})
return
}
resolve()
})
})
}
__httpFetch(url, options) {
// console.log(options)
this.request = request(url, options)
.on('response', response => {
if (response.statusCode !== 200 && response.statusCode !== 206) {
this.status = STATUS.failed
this.emit('fail', response)
this.__closeRequest()
this.__closeWriteStream()
return
}
this.emit('response', response)
try {
this.__initDownload(response)
} catch (error) {
return this.__handleError(error)
}
this.status = STATUS.running
response
.on('data', this.__handleWriteData.bind(this))
.on('error', err => this.__handleError(err))
.on('end', () => this.__handleComplete())
})
.on('error', err => this.__handleError(err))
.on('close', () => this.__closeWriteStream())
}
__initDownload(response) {
this.progress.total = parseInt(response.headers['content-length'] || 0)
let options = {}
let isResumable = this.options.forceResume || response.headers['accept-ranges'] !== 'none'
if (isResumable) {
options.flags = 'a'
if (this.progress.downloaded) this.progress.total -= 10
} else {
if (this.chunkInfo.startByte > 0) return this.__handleError(new Error('The resource cannot be resumed download.'))
}
this.progress.total += this.progress.downloaded
this.statsEstimate.prevBytes = this.progress.downloaded
if (!this.chunkInfo.path) return this.__handleError(new Error('Chunk save Path is not set.'))
this.ws = fs.createWriteStream(this.chunkInfo.path, options)
this.ws.on('finish', () => this.__closeWriteStream())
this.ws.on('error', async err => {
await this.__handleError(err)
fs.unlink(this.chunkInfo.path, () => this.__handleError(err))
})
}
__handleComplete() {
if (this.status == STATUS.error) return
this.__closeWriteStream().then(() => {
if (this.progress.downloaded == this.progress.total) {
this.status = STATUS.completed
this.emit('completed')
} else {
this.status = STATUS.stopped
this.emit('stop')
}
})
console.log('end')
}
__handleError(error) {
if (this.status == STATUS.error) return
this.status = STATUS.error
this.__closeRequest()
this.__closeWriteStream()
this.emit('error', error)
}
__closeWriteStream() {
return new Promise((resolve, reject) => {
if (!this.ws) return resolve()
console.log('close write stream')
this.ws.close(err => {
if (err) {
this.status = STATUS.error
this.emit('error', err)
reject(err)
return
}
this.ws = null
resolve()
})
})
}
__closeRequest() {
if (!this.request) return
console.log('close request')
this.request.abort()
this.request = null
}
__handleWriteData(chunk) {
if (this.resumeLastChunk) {
chunk = this.__handleDiffChunk(chunk)
if (!chunk) {
this.__handleError(new Error('Resume failed, response chunk does not match.'))
this.stop()
return
}
}
// console.log('data', chunk)
if (this.status == STATUS.stopped || this.ws == null) return console.log('cancel write')
this.__calculateProgress(chunk.length)
this.ws.write(chunk, err => {
if (!err) return
console.log(err)
this.__handleError(err)
this.stop()
})
}
__handleDiffChunk(chunk) {
// console.log('diff', chunk)
let resumeLastChunkLen = this.resumeLastChunk.length
let chunkLen = chunk.length
let isOk
if (chunkLen >= resumeLastChunkLen) {
isOk = chunk.slice(0, resumeLastChunkLen).toString('hex') === this.resumeLastChunk.toString('hex')
if (!isOk) return null
this.resumeLastChunk = null
return chunk.slice(resumeLastChunkLen)
} else {
isOk = chunk.slice(0, chunkLen).toString('hex') === this.resumeLastChunk.slice(0, chunkLen).toString('hex')
if (!isOk) return null
this.resumeLastChunk = this.resumeLastChunk.slice(chunkLen)
return chunk.slice(chunkLen)
}
}
__handleStop() {
return new Promise((resolve, reject) => {
if (this.request) {
this.request.abort()
this.request = null
}
if (this.ws) {
this.ws.close(err => {
if (err) {
reject(err)
this.emit('error', err)
return
}
this.ws = null
resolve()
})
} else {
resolve()
}
})
}
__calculateProgress(receivedBytes) {
const currentTime = performance.now()
const elaspsedTime = currentTime - this.statsEstimate.time
const progress = this.progress
progress.downloaded += receivedBytes
progress.progress = progress.total ? (progress.downloaded / progress.total) * 100 : -1
// emit the progress every second or if finished
if (progress.downloaded === progress.total || elaspsedTime > 1000) {
this.statsEstimate.time = currentTime
this.statsEstimate.bytes = progress.downloaded - this.statsEstimate.prevBytes
this.statsEstimate.prevBytes = progress.downloaded
this.emit('progress', {
total: progress.total,
downloaded: progress.downloaded,
progress: progress.progress,
speed: this.statsEstimate.bytes,
})
}
}
async start() {
this.status = STATUS.running
await this.__init()
this.__httpFetch(this.downloadUrl, this.requestOptions)
this.emit('start')
}
async stop() {
if (this.status === STATUS.stopped) return
this.status = STATUS.stopped
await this.__handleStop()
this.emit('stop')
}
refreshUrl(url) {
this.downloadUrl = url
}
}
export default Task
+23 -34
View File
@@ -1,6 +1,6 @@
import { DownloaderHelper } from 'node-downloader-helper'
import Downloader from './Downloader'
// import { pauseResumeTimer } from './util'
import { sizeFormate } from '../index'
import { sizeFormate, getProxyInfo } from '../index'
import { debugDownload } from '../env'
// these are the default options
@@ -21,38 +21,34 @@ export default ({
fileName,
method = 'get',
headers,
override,
forceResume,
// resumeTime = 5000,
onEnd = () => {},
onCompleted = () => {},
onError = () => {},
onStateChanged = () => {},
onDownload = () => {},
onPause = () => {},
onResume = () => {},
onFail = () => {},
onStart = () => {},
onStop = () => {},
onProgress = () => {},
resumeInfo,
} = {}) => {
const dl = new DownloaderHelper(url, path, {
fileName,
method,
headers,
override,
const dl = new Downloader(url, path, fileName, {
requestOptions: {
method,
headers,
proxy: getProxyInfo(),
},
forceResume,
})
dl.on('end', () => {
onEnd()
dl.on('completed', () => {
onCompleted()
debugDownload && console.log('Download Completed')
}).on('error', err => {
if (err.message === 'socket hang up') return
onError(err)
debugDownload && console.error('Something happend', err)
}).on('stateChanged', state => {
onStateChanged(state)
debugDownload && console.log('State: ', state)
}).on('download', () => {
onDownload()
}).on('start', () => {
onStart()
// pauseResumeTimer(dl, resumeTime)
}).on('progress', stats => {
const progress = stats.progress.toFixed(2)
@@ -68,24 +64,17 @@ export default ({
const total = sizeFormate(stats.total)
console.log(`${speed}/s - ${progress}% [${downloaded}/${total}]`)
}
}).on('pause', () => {
onPause()
}).on('stop', () => {
onStop()
debugDownload && console.log('paused')
}).on('resume', () => {
onResume()
debugDownload && console.log('resume')
}).on('fail', resp => {
onFail(resp)
debugDownload && console.log('fail')
})
debugDownload && console.log('Downloading: ', url)
if (resumeInfo) {
dl.__total = resumeInfo.totalFileSize // <--- Workaround
// dl.__filePath = resumeInfo.filePath // <--- Workaround
dl.__isResumable = true // <--- Workaround
dl.resume()
} else {
dl.start()
}
dl.start()
return dl
}
+9 -22
View File
@@ -1,24 +1,11 @@
import { DH_STATES } from 'node-downloader-helper'
export const pauseResumeTimer = (_dl, wait) => {
setTimeout(() => {
if (_dl.state === DH_STATES.FINISHED || _dl.state === DH_STATES.FAILED) {
return
}
_dl
.pause()
.then(() => console.log(`Paused for ${wait / 1000} seconds`))
.then(() =>
setTimeout(() => {
if (!_dl.isResumable()) {
console.warn(
"This URL doesn't support resume, it will start from the beginning",
)
}
return _dl.resume()
}, wait),
)
}, wait)
exports.STATUS = {
idle: 'IDLE',
init: 'INIT',
running: 'RUNNING',
paused: 'PAUSED',
stopped: 'STOPPED',
completed: 'COMPLETED',
error: 'ERROR',
failed: 'FAILED',
}
+5
View File
@@ -374,3 +374,8 @@ export const clearCache = () => rendererInvoke('clearCache')
* @param {*} height
*/
export const setWindowSize = (width, height) => rendererSend('setWindowSize', { width, height })
export const getProxyInfo = () => window.globalObj.proxy.enable
? `http://${window.globalObj.proxy.username}:${window.globalObj.proxy.password}@${window.globalObj.proxy.host}:${window.globalObj.proxy.port};`
: undefined
+1 -4
View File
@@ -4,6 +4,7 @@ import { debugRequest } from './env'
import { requestMsg } from './message'
import { bHh } from './music/options'
import { deflateRawSync } from 'zlib'
import { getProxyInfo } from './index'
// import fs from 'fs'
const defaultHeaders = {
@@ -219,10 +220,6 @@ export const http_jsonp = (url, options, callback) => {
})
}
const getProxyInfo = () => window.globalObj.proxy.enable
? `http://${window.globalObj.proxy.username}:${window.globalObj.proxy.password}@${window.globalObj.proxy.host}:${window.globalObj.proxy.port};`
: undefined
const regx = /(?:\d\w)+/g
const fetchData = (url, method, {
+25 -19
View File
@@ -80,9 +80,11 @@ export default {
return this.listId == 'download'
},
playListIndex() {
if (this.listId != 'download') return
let path = this.list[this.playIndex].filePath
return this.showList.findIndex(i => i.filePath == path)
if (this.listId != 'download' || !this.list.length) return
let info = this.list[this.playIndex]
if (!info) return -1
let key = info.key
return this.showList.findIndex(i => i.key == key)
},
showList() {
switch (this.tabId) {
@@ -120,16 +122,16 @@ export default {
...mapMutations('player', ['setList']),
...mapMutations('download', ['pauseTask']),
handlePauseTask(index) {
let info = this.showList[index]
let info = this.list[index]
let dl = this.dls[info.key]
dl ? dl.pause() : this.pauseTask(info)
dl ? dl.stop() : this.pauseTask(info)
console.log('pause')
},
handleStartTask(index) {
console.log('start')
let info = this.showList[index]
let info = this.list[index]
let dl = this.dls[info.key]
dl ? dl.resume() : this.startTask(info)
dl ? dl.start() : this.startTask(info)
},
handleDoubleClick(index) {
if (
@@ -145,7 +147,9 @@ export default {
this.clickIndex = -1
},
handleClick(index) {
let info = this.showList[index]
const key = this.showList[index].key
index = this.list.findIndex(i => i.key === key)
let info = this.list[index]
if (info.isComplate) {
this.handlePlay(index)
} else if (info.status === this.downloadStatus.RUN) {
@@ -155,26 +159,28 @@ export default {
}
},
handlePlay(index) {
if (!checkPath(this.showList[index].filePath)) return
let path = this.showList[index].filePath
if (!checkPath(this.list[index].filePath)) return
let path = this.list[index].filePath
this.setList({ list: this.list, listId: 'download', index: this.list.findIndex(i => i.filePath === path) })
},
handleListBtnClick(info) {
const key = this.showList[info.index].key
let index = this.list.findIndex(i => i.key === key)
switch (info.action) {
case 'play':
this.handlePlay(info.index)
this.handlePlay(index)
break
case 'start':
this.handleStartTask(info.index)
this.handleStartTask(index)
break
case 'pause':
this.handlePauseTask(info.index)
this.handlePauseTask(index)
break
case 'remove':
this.removeTask(info.index)
this.removeTask(index)
break
case 'file':
this.handleOpenFolder(info.index)
this.handleOpenFolder(index)
break
}
},
@@ -190,7 +196,7 @@ export default {
case 'start':
this.selectdData.forEach(item => {
if (item.isComplate || item.status == this.downloadStatus.RUN) return
let index = this.showList.indexOf(item)
let index = this.list.indexOf(item)
if (index < 0) return
this.handleStartTask(index)
})
@@ -200,13 +206,13 @@ export default {
this.selectdData.forEach(item => {
if (item.isComplate || item.status == this.downloadStatus.PAUSE) return
if (item.status == this.downloadStatus.RUN) return runs.push(item)
let index = this.showList.indexOf(item)
let index = this.list.indexOf(item)
if (index < 0) return
this.handlePauseTask(index)
})
runs.forEach(item => {
if (item.isComplate || item.status == this.downloadStatus.PAUSE) return
let index = this.showList.indexOf(item)
let index = this.list.indexOf(item)
if (index < 0) return
this.handlePauseTask(index)
})
@@ -219,7 +225,7 @@ export default {
this.resetSelect()
},
handleOpenFolder(index) {
let path = this.showList[index].filePath
let path = this.list[index].filePath
if (!checkPath(path)) return
openDirInExplorer(path)
},
+2 -2
View File
@@ -78,7 +78,7 @@ div.scroll(:class="$style.setting")
material-checkbox(id="setting_download_isDownloadLrc" v-model="current_setting.download.isDownloadLrc" label="是否启用")
dt 网络设置
dd
h3 代理设置(歌曲下载暂不支持代理)
h3 代理设置
div
p
material-checkbox(id="setting_network_proxy_enable" v-model="current_setting.network.proxy.enable" @change="handleProxyChange('enable')" label="是否启用")
@@ -108,7 +108,7 @@ div.scroll(:class="$style.setting")
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleExportAllData") 导出
dt 其他
dd
h3 缓存大小(清理缓存后图片等资源将需要重新下载)
h3 缓存大小(清理缓存后图片等资源将需要重新下载,不建议清除,软件会自动将大小维持在200M左右)
div
p
| 软件已使用缓存大小: