mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-11 06:09:39 +00:00
More download bugfixes
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -2142,6 +2142,15 @@
|
||||
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/destroy": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/destroy/-/destroy-1.0.0.tgz",
|
||||
"integrity": "sha512-nE3ePJLWPRu/qFHN8mj3fWnkr9K9ezwoiG4yOis2DuLeAawlnOOT/pM29JQkityrwfEvkblU5O9iS1bsiMqtDw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/electron-window-state": {
|
||||
"version": "2.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/electron-window-state/-/electron-window-state-2.0.33.tgz",
|
||||
@@ -5423,8 +5432,7 @@
|
||||
"destroy": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
|
||||
"dev": true
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||
},
|
||||
"detect-file": {
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"@angular/router": "~9.1.4",
|
||||
"cli-color": "^2.0.0",
|
||||
"comparators": "^3.0.2",
|
||||
"destroy": "^1.0.4",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"fomantic-ui": "^2.8.3",
|
||||
"jquery": "^3.4.1",
|
||||
@@ -56,6 +57,7 @@
|
||||
"@angular/compiler-cli": "~9.1.4",
|
||||
"@angular/language-service": "~9.1.4",
|
||||
"@types/cli-color": "^2.0.0",
|
||||
"@types/destroy": "^1.0.0",
|
||||
"@types/electron-window-state": "^2.0.33",
|
||||
"@types/mv": "^2.1.0",
|
||||
"@types/needle": "^2.0.4",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, ChangeDetectorRef } from '@angular/core'
|
||||
import { DownloadProgress } from '../../../../../electron/shared/interfaces/download.interface'
|
||||
import { DownloadService } from '../../../../core/services/download.service'
|
||||
import { ElectronService } from 'src/app/core/services/electron.service'
|
||||
import { ElectronService } from '../../../../core/services/electron.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-downloads-modal',
|
||||
@@ -13,6 +13,10 @@ export class DownloadsModalComponent {
|
||||
downloads: DownloadProgress[] = []
|
||||
|
||||
constructor(private electronService: ElectronService, private downloadService: DownloadService, ref: ChangeDetectorRef) {
|
||||
electronService.receiveIPC('queue-updated', (order) => {
|
||||
this.downloads.sort((a, b) => order.indexOf(a.versionID) - order.indexOf(b.versionID))
|
||||
})
|
||||
|
||||
downloadService.onDownloadUpdated(download => {
|
||||
const index = this.downloads.findIndex(thisDownload => thisDownload.versionID == download.versionID)
|
||||
if (index == -1) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable, EventEmitter } from '@angular/core'
|
||||
import { ElectronService } from './electron.service'
|
||||
import { NewDownload, DownloadProgress } from '../../../electron/shared/interfaces/download.interface'
|
||||
import * as _ from 'underscore'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -46,14 +45,7 @@ export class DownloadService {
|
||||
}
|
||||
|
||||
onDownloadUpdated(callback: (download: DownloadProgress) => void) {
|
||||
const debouncedCallback = _.throttle(callback, 30, { trailing: false })
|
||||
this.downloadUpdatedEmitter.subscribe((download: DownloadProgress) => {
|
||||
if (download.type == 'fastUpdate') { // 'good' updates can happen so frequently that the UI doesn't update correctly
|
||||
debouncedCallback(download)
|
||||
} else {
|
||||
callback(download)
|
||||
}
|
||||
})
|
||||
this.downloadUpdatedEmitter.subscribe(callback)
|
||||
}
|
||||
|
||||
cancelDownload(versionID: number) {
|
||||
|
||||
@@ -10,9 +10,11 @@ import { mkdir as _mkdir } from 'fs'
|
||||
import { ProgressType, NewDownload } from 'src/electron/shared/interfaces/download.interface'
|
||||
import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface'
|
||||
import { FileTransfer } from './FileTransfer'
|
||||
import * as _rimraf from 'rimraf'
|
||||
|
||||
const randomBytes = promisify(_randomBytes)
|
||||
const mkdir = promisify(_mkdir)
|
||||
const rimraf = promisify(_rimraf)
|
||||
|
||||
type EventCallback = {
|
||||
/** Note: this will not be the last event if `retry()` is called. */
|
||||
@@ -31,6 +33,8 @@ export class ChartDownload {
|
||||
private callbacks = {} as Callbacks
|
||||
private files: DriveFile[]
|
||||
private percent = 0 // Needs to be stored here because errors won't know the exact percent
|
||||
private chartPath: string
|
||||
private dropFastUpdate = false
|
||||
|
||||
private readonly individualFileProgressPortion: number
|
||||
private readonly destinationFolderName: string
|
||||
@@ -68,6 +72,13 @@ export class ChartDownload {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the GUI to indicate that a retry will be attempted.
|
||||
*/
|
||||
displayRetrying() {
|
||||
this.updateGUI('', 'Waiting for other downloads to finish to retry...', 'good')
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the download if it is running.
|
||||
*/
|
||||
@@ -76,6 +87,7 @@ export class ChartDownload {
|
||||
const cancelFn = this.cancelFn
|
||||
this.cancelFn = undefined
|
||||
cancelFn()
|
||||
rimraf(this.chartPath) // Delete temp folder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +95,15 @@ export class ChartDownload {
|
||||
* Updates the GUI with new information about this chart download.
|
||||
*/
|
||||
private updateGUI(header: string, description: string, type: ProgressType) {
|
||||
if (type == 'fastUpdate') {
|
||||
if (this.dropFastUpdate) {
|
||||
return
|
||||
} else {
|
||||
this.dropFastUpdate = true
|
||||
setTimeout(() => this.dropFastUpdate = false, 30)
|
||||
}
|
||||
}
|
||||
|
||||
emitIPCEvent('download-updated', {
|
||||
versionID: this.versionID,
|
||||
title: `${this.data.avTagName} - ${this.data.artist}`,
|
||||
@@ -108,9 +129,8 @@ export class ChartDownload {
|
||||
*/
|
||||
async beginDownload() {
|
||||
// CREATE DOWNLOAD DIRECTORY
|
||||
let chartPath: string
|
||||
try {
|
||||
chartPath = await this.createDownloadFolder()
|
||||
this.chartPath = await this.createDownloadFolder()
|
||||
} catch (err) {
|
||||
this.retryFn = () => this.beginDownload()
|
||||
this.updateGUI('Access Error', err.message, 'error')
|
||||
@@ -119,7 +139,7 @@ export class ChartDownload {
|
||||
|
||||
// DOWNLOAD FILES
|
||||
for (let i = 0; i < this.files.length; i++) {
|
||||
const downloader = new FileDownloader(this.files[i].webContentLink, join(chartPath, this.files[i].name))
|
||||
const downloader = new FileDownloader(this.files[i].webContentLink, join(this.chartPath, this.files[i].name))
|
||||
this.cancelFn = () => downloader.cancelDownload()
|
||||
|
||||
const downloadComplete = this.addDownloadEventListeners(downloader, i)
|
||||
@@ -129,7 +149,7 @@ export class ChartDownload {
|
||||
|
||||
// EXTRACT FILES
|
||||
if (this.isArchive) {
|
||||
const extractor = new FileExtractor(chartPath)
|
||||
const extractor = new FileExtractor(this.chartPath)
|
||||
this.cancelFn = () => extractor.cancelExtract()
|
||||
|
||||
const extractComplete = this.addExtractorEventListeners(extractor)
|
||||
@@ -138,7 +158,7 @@ export class ChartDownload {
|
||||
}
|
||||
|
||||
// TRANSFER FILES
|
||||
const transfer = new FileTransfer(chartPath, this.destinationFolderName)
|
||||
const transfer = new FileTransfer(this.chartPath, this.destinationFolderName)
|
||||
this.cancelFn = () => transfer.cancelTransfer()
|
||||
|
||||
const transferComplete = this.addTransferEventListeners(transfer)
|
||||
|
||||
@@ -29,10 +29,11 @@ class DownloadHandler implements IPCEmitHandler<'download'> {
|
||||
}
|
||||
}
|
||||
|
||||
private retryDownload(data: Download) { // TODO: cause this to send a GUI update that says waiting for download to finish...
|
||||
private retryDownload(data: Download) {
|
||||
const index = this.retryWaiting.findIndex(download => download.versionID == data.versionID)
|
||||
if (index != -1) {
|
||||
const retryDownload = this.retryWaiting.splice(index, 1)[0]
|
||||
retryDownload.displayRetrying()
|
||||
if (this.currentDownload == undefined) {
|
||||
this.currentDownload = retryDownload
|
||||
retryDownload.retry()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Comparators from 'comparators'
|
||||
import { ChartDownload } from './ChartDownload'
|
||||
import { emitIPCEvent } from '../../main'
|
||||
|
||||
export class DownloadQueue {
|
||||
|
||||
@@ -15,7 +16,7 @@ export class DownloadQueue {
|
||||
}
|
||||
|
||||
pop() {
|
||||
return this.downloadQueue.pop()
|
||||
return this.downloadQueue.shift()
|
||||
}
|
||||
|
||||
get(versionID: number) {
|
||||
@@ -27,6 +28,7 @@ export class DownloadQueue {
|
||||
if (index != -1) {
|
||||
this.downloadQueue[index].cancel()
|
||||
this.downloadQueue.splice(index, 1)
|
||||
emitIPCEvent('queue-updated', this.downloadQueue.map(download => download.versionID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,5 +41,6 @@ export class DownloadQueue {
|
||||
}
|
||||
|
||||
this.downloadQueue.sort(comparator)
|
||||
emitIPCEvent('queue-updated', this.downloadQueue.map(download => download.versionID))
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { AnyFunction } from '../../shared/UtilFunctions'
|
||||
import { createWriteStream } from 'fs'
|
||||
import * as needle from 'needle'
|
||||
// TODO: replace needle with got (for cancel() method) (if before-headers event is possible?)
|
||||
// TODO: add download throttle library and setting
|
||||
import { getSettings } from '../SettingsHandler.ipc'
|
||||
import { googleTimer } from './GoogleTimer'
|
||||
import { DownloadError } from './ChartDownload'
|
||||
@@ -37,6 +38,7 @@ export class FileDownloader {
|
||||
private callbacks = {} as Callbacks
|
||||
private retryCount: number
|
||||
private wasCanceled = false
|
||||
private req: NodeJS.ReadableStream
|
||||
|
||||
/**
|
||||
* @param url The download link.
|
||||
@@ -55,7 +57,6 @@ export class FileDownloader {
|
||||
* Download the file after waiting for the google rate limit.
|
||||
*/
|
||||
beginDownload() {
|
||||
console.log('Begin download...')
|
||||
if (getSettings().libraryPath == undefined) {
|
||||
this.failDownload(downloadErrors.libraryFolder())
|
||||
} else {
|
||||
@@ -75,7 +76,7 @@ export class FileDownloader {
|
||||
*/
|
||||
private requestDownload(cookieHeader?: string) {
|
||||
this.callbacks.requestSent()
|
||||
const req = needle.get(this.url, {
|
||||
this.req = needle.get(this.url, {
|
||||
'follow_max': 10,
|
||||
'open_timeout': 5000,
|
||||
'headers': Object.assign({
|
||||
@@ -86,7 +87,7 @@ export class FileDownloader {
|
||||
)
|
||||
})
|
||||
|
||||
req.on('timeout', this.cancelable((type: string) => {
|
||||
this.req.on('timeout', this.cancelable((type: string) => {
|
||||
this.retryCount++
|
||||
if (this.retryCount <= this.RETRY_MAX) {
|
||||
console.log(`TIMEOUT: Retry attempt ${this.retryCount}...`)
|
||||
@@ -96,20 +97,20 @@ export class FileDownloader {
|
||||
}
|
||||
}))
|
||||
|
||||
req.on('err', this.cancelable((err: Error) => {
|
||||
this.req.on('err', this.cancelable((err: Error) => {
|
||||
this.failDownload(downloadErrors.connectionError(err))
|
||||
}))
|
||||
|
||||
req.on('header', this.cancelable((statusCode, headers: Headers) => {
|
||||
this.req.on('header', this.cancelable((statusCode, headers: Headers) => {
|
||||
if (statusCode != 200) {
|
||||
this.failDownload(downloadErrors.responseError(statusCode))
|
||||
return
|
||||
}
|
||||
|
||||
if (headers['content-type'].startsWith('text/html')) {
|
||||
this.handleHTMLResponse(req, headers['set-cookie'])
|
||||
this.handleHTMLResponse(headers['set-cookie'])
|
||||
} else {
|
||||
this.handleDownloadResponse(req)
|
||||
this.handleDownloadResponse()
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -117,14 +118,12 @@ export class FileDownloader {
|
||||
/**
|
||||
* A Google Drive HTML response to a download request usually means this is the "file too large to scan for viruses" warning.
|
||||
* This function sends the request that results from clicking "download anyway", or generates an error if it can't be found.
|
||||
* @param req The download request.
|
||||
* @param cookieHeader The "cookie=" header of this request.
|
||||
*/
|
||||
private handleHTMLResponse(req: NodeJS.ReadableStream, cookieHeader: string) {
|
||||
console.log('HTML Response...')
|
||||
private handleHTMLResponse(cookieHeader: string) {
|
||||
let virusScanHTML = ''
|
||||
req.on('data', this.cancelable(data => virusScanHTML += data))
|
||||
req.on('done', this.cancelable((err: Error) => {
|
||||
this.req.on('data', this.cancelable(data => virusScanHTML += data))
|
||||
this.req.on('done', this.cancelable((err: Error) => {
|
||||
if (err) {
|
||||
this.failDownload(downloadErrors.connectionError(err))
|
||||
} else {
|
||||
@@ -149,21 +148,20 @@ export class FileDownloader {
|
||||
* Pipes the data from a download response to `this.fullPath`.
|
||||
* @param req The download request.
|
||||
*/
|
||||
private handleDownloadResponse(req: NodeJS.ReadableStream) {
|
||||
console.log('Download response...')
|
||||
private handleDownloadResponse() {
|
||||
this.callbacks.downloadProgress(0)
|
||||
let downloadedSize = 0
|
||||
req.pipe(createWriteStream(this.fullPath))
|
||||
req.on('data', this.cancelable((data) => {
|
||||
this.req.pipe(createWriteStream(this.fullPath))
|
||||
this.req.on('data', this.cancelable((data) => {
|
||||
downloadedSize += data.length
|
||||
this.callbacks.downloadProgress(downloadedSize)
|
||||
}))
|
||||
|
||||
req.on('err', this.cancelable((err: Error) => {
|
||||
this.req.on('err', this.cancelable((err: Error) => {
|
||||
this.failDownload(downloadErrors.connectionError(err))
|
||||
}))
|
||||
|
||||
req.on('end', this.cancelable(() => {
|
||||
this.req.on('end', this.cancelable(() => {
|
||||
this.callbacks.complete()
|
||||
}))
|
||||
}
|
||||
@@ -181,6 +179,9 @@ export class FileDownloader {
|
||||
cancelDownload() {
|
||||
this.wasCanceled = true
|
||||
googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
|
||||
if (this.req) {
|
||||
// TODO: destroy request
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,14 +17,14 @@ type EventCallback = {
|
||||
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||
|
||||
const transferErrors = {
|
||||
readError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to read file'),
|
||||
deleteError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete file'),
|
||||
rimrafError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete folder'),
|
||||
mvError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to move folder to library')
|
||||
readError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to read file.'),
|
||||
deleteError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete file.'),
|
||||
rimrafError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete folder.'),
|
||||
mvError: (err: NodeJS.ErrnoException) => fsError(err, `Failed to move folder to library.${err.code == 'EPERM' ? ' (does the chart already exist?)' : ''}`)
|
||||
}
|
||||
|
||||
function fsError(err: NodeJS.ErrnoException, description: string) {
|
||||
return { header: `${description} (${err.code})`, body: `${err.name}: ${err.message}` }
|
||||
return { header: description, body: `${err.name}: ${err.message}` }
|
||||
}
|
||||
|
||||
export class FileTransfer {
|
||||
|
||||
@@ -76,6 +76,7 @@ export type IPCEmitEvents = {
|
||||
'download': Download
|
||||
'download-updated': DownloadProgress
|
||||
'set-settings': Settings
|
||||
'queue-updated': number[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user