mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-07-07 10:35:09 +00:00
Fix compiler issues
This commit is contained in:
@@ -1,12 +0,0 @@
|
|||||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
|
||||||
# For additional information regarding the format and rule options, please see:
|
|
||||||
# https://github.com/browserslist/browserslist#queries
|
|
||||||
|
|
||||||
# You can see what browsers were selected by your queries by running:
|
|
||||||
# npx browserslist
|
|
||||||
|
|
||||||
> 0.5%
|
|
||||||
last 2 versions
|
|
||||||
Firefox ESR
|
|
||||||
not dead
|
|
||||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
"main": "dist/electron/main.js",
|
"main": "dist/electron/main.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "run-p serve:angular serve:electron",
|
"start": "concurrently \"npm run serve:angular\" \"npm run serve:electron\" -n angular,electron -c red,yellow",
|
||||||
"serve:electron": "wait-on http-get://localhost:4200/ && nodemon --exec \"tsc -p tsconfig.electron.json && electron . --dev\" --watch src/electron -e ts",
|
"serve:electron": "nodemon --exec \"tsc -p tsconfig.electron.json && electron . --dev\" --watch src/electron -e ts",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"clean": "rimraf dist release",
|
"clean": "rimraf dist release",
|
||||||
"build:windows": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --windows",
|
"build:windows": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --windows",
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
"fomantic-ui": "^2.8.3",
|
"fomantic-ui": "^2.8.3",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.5.1",
|
||||||
"jsonfile": "^6.0.1",
|
"jsonfile": "^6.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"mv": "^2.1.1",
|
"mv": "^2.1.1",
|
||||||
"randombytes": "^2.1.0",
|
"randombytes": "^2.1.0",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
"@angular/language-service": "^17.0.4",
|
"@angular/language-service": "^17.0.4",
|
||||||
"@types/cli-color": "^2.0.0",
|
"@types/cli-color": "^2.0.0",
|
||||||
"@types/jsonfile": "^6.0.0",
|
"@types/jsonfile": "^6.0.0",
|
||||||
|
"@types/lodash": "^4.14.202",
|
||||||
"@types/mv": "^2.1.0",
|
"@types/mv": "^2.1.0",
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^18.16.0",
|
||||||
"@types/randombytes": "^2.0.0",
|
"@types/randombytes": "^2.0.0",
|
||||||
@@ -87,4 +89,4 @@
|
|||||||
"tsx": "^4.4.0",
|
"tsx": "^4.4.0",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -59,6 +59,9 @@ dependencies:
|
|||||||
jsonfile:
|
jsonfile:
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.1.0
|
version: 6.1.0
|
||||||
|
lodash:
|
||||||
|
specifier: ^4.17.21
|
||||||
|
version: 4.17.21
|
||||||
mv:
|
mv:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
@@ -115,6 +118,9 @@ devDependencies:
|
|||||||
'@types/jsonfile':
|
'@types/jsonfile':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.1.4
|
version: 6.1.4
|
||||||
|
'@types/lodash':
|
||||||
|
specifier: ^4.14.202
|
||||||
|
version: 4.14.202
|
||||||
'@types/mv':
|
'@types/mv':
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.4
|
version: 2.1.4
|
||||||
@@ -3294,6 +3300,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 14.18.63
|
'@types/node': 14.18.63
|
||||||
|
|
||||||
|
/@types/lodash@4.14.202:
|
||||||
|
resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/mime@1.3.5:
|
/@types/mime@1.3.5:
|
||||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export class ChartSidebarComponent implements OnInit {
|
|||||||
* @returns a string describing the difficulty number in the selected version.
|
* @returns a string describing the difficulty number in the selected version.
|
||||||
*/
|
*/
|
||||||
private getDiffNumber(instrument: Instrument) {
|
private getDiffNumber(instrument: Instrument) {
|
||||||
const diffNumber: number = this.selectedVersion[`diff_${instrument}`]
|
const diffNumber: number = (this.selectedVersion as any)[`diff_${instrument}`]
|
||||||
return diffNumber == -1 || diffNumber == undefined ? 'Unknown' : String(diffNumber)
|
return diffNumber == -1 || diffNumber == undefined ? 'Unknown' : String(diffNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Directive, ElementRef, Input } from '@angular/core'
|
import { Directive, ElementRef, Input } from '@angular/core'
|
||||||
|
|
||||||
import * as _ from 'underscore'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[appProgressBar]',
|
selector: '[appProgressBar]',
|
||||||
})
|
})
|
||||||
export class ProgressBarDirective {
|
export class ProgressBarDirective {
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
private _$progressBar: any
|
||||||
|
|
||||||
progress: (percent: number) => void
|
progress: (percent: number) => void
|
||||||
|
|
||||||
@Input() set percent(percent: number) {
|
@Input() set percent(percent: number) {
|
||||||
@@ -23,5 +26,4 @@ export class ProgressBarDirective {
|
|||||||
}
|
}
|
||||||
return this._$progressBar
|
return this._$progressBar
|
||||||
}
|
}
|
||||||
private _$progressBar: any
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { ElectronService } from './electron.service'
|
|||||||
})
|
})
|
||||||
export class AlbumArtService {
|
export class AlbumArtService {
|
||||||
|
|
||||||
constructor(private electronService: ElectronService) { }
|
|
||||||
|
|
||||||
private imageCache: { [songID: number]: string } = {}
|
private imageCache: { [songID: number]: string } = {}
|
||||||
|
|
||||||
|
constructor(private electronService: ElectronService) { }
|
||||||
|
|
||||||
async getImage(songID: number): Promise<string | null> {
|
async getImage(songID: number): Promise<string | null> {
|
||||||
if (this.imageCache[songID] == undefined) {
|
if (this.imageCache[songID] == undefined) {
|
||||||
const albumArtResult = await this.electronService.invoke('album-art', songID)
|
const albumArtResult = await this.electronService.invoke('album-art', songID)
|
||||||
|
|||||||
@@ -5,11 +5,6 @@ import { SearchService } from './search.service'
|
|||||||
|
|
||||||
// Note: this class prevents event cycles by only emitting events if the checkbox changes
|
// Note: this class prevents event cycles by only emitting events if the checkbox changes
|
||||||
|
|
||||||
interface SelectionEvent {
|
|
||||||
songID: number
|
|
||||||
selected: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { join, parse } from 'path'
|
import { parse } from 'path'
|
||||||
import { rimraf } from 'rimraf'
|
import { rimraf } from 'rimraf'
|
||||||
|
|
||||||
import { NewDownload, ProgressType } from 'src/electron/shared/interfaces/download.interface'
|
import { NewDownload, ProgressType } from 'src/electron/shared/interfaces/download.interface'
|
||||||
import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface'
|
import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface'
|
||||||
import { emitIPCEvent } from '../../main'
|
import { emitIPCEvent } from '../../main'
|
||||||
import { hasVideoExtension } from '../../shared/ElectronUtilFunctions'
|
import { hasVideoExtension } from '../../shared/ElectronUtilFunctions'
|
||||||
import { interpolate, sanitizeFilename } from '../../shared/UtilFunctions'
|
import { sanitizeFilename } from '../../shared/UtilFunctions'
|
||||||
import { getSettings } from '../SettingsHandler.ipc'
|
import { getSettings } from '../SettingsHandler.ipc'
|
||||||
import { FileDownloader, getDownloader } from './FileDownloader'
|
// import { FileDownloader, getDownloader } from './FileDownloader'
|
||||||
import { FileExtractor } from './FileExtractor'
|
|
||||||
import { FilesystemChecker } from './FilesystemChecker'
|
import { FilesystemChecker } from './FilesystemChecker'
|
||||||
import { FileTransfer } from './FileTransfer'
|
import { FileTransfer } from './FileTransfer'
|
||||||
|
|
||||||
@@ -143,23 +142,23 @@ export class ChartDownload {
|
|||||||
for (let i = 0; i < this.files.length; i++) {
|
for (let i = 0; i < this.files.length; i++) {
|
||||||
let wasCanceled = false
|
let wasCanceled = false
|
||||||
this.cancelFn = () => { wasCanceled = true }
|
this.cancelFn = () => { wasCanceled = true }
|
||||||
const downloader = getDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name))
|
// const downloader = getDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name))
|
||||||
if (wasCanceled) { return }
|
if (wasCanceled) { return }
|
||||||
this.cancelFn = () => downloader.cancelDownload()
|
// this.cancelFn = () => downloader.cancelDownload()
|
||||||
|
|
||||||
const downloadComplete = this.addDownloadEventListeners(downloader, i)
|
// const downloadComplete = this.addDownloadEventListeners(downloader, i)
|
||||||
downloader.beginDownload()
|
// downloader.beginDownload()
|
||||||
await downloadComplete
|
// await downloadComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXTRACT FILES
|
// EXTRACT FILES
|
||||||
if (this.isArchive) {
|
if (this.isArchive) {
|
||||||
const extractor = new FileExtractor(this.tempPath)
|
// const extractor = new FileExtractor(this.tempPath)
|
||||||
this.cancelFn = () => extractor.cancelExtract()
|
// this.cancelFn = () => extractor.cancelExtract()
|
||||||
|
|
||||||
const extractComplete = this.addExtractorEventListeners(extractor)
|
// const extractComplete = this.addExtractorEventListeners(extractor)
|
||||||
extractor.beginExtract()
|
// extractor.beginExtract()
|
||||||
await extractComplete
|
// await extractComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRANSFER FILES
|
// TRANSFER FILES
|
||||||
@@ -196,68 +195,68 @@ export class ChartDownload {
|
|||||||
* Defines what happens in response to `FileDownloader` events.
|
* Defines what happens in response to `FileDownloader` events.
|
||||||
* @returns a `Promise` that resolves when the download finishes.
|
* @returns a `Promise` that resolves when the download finishes.
|
||||||
*/
|
*/
|
||||||
private addDownloadEventListeners(downloader: FileDownloader, fileIndex: number) {
|
// private addDownloadEventListeners(downloader: FileDownloader, fileIndex: number) {
|
||||||
let downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
|
// let downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
|
||||||
let downloadStartPoint = 0 // How far into the individual file progress portion the download progress starts
|
// let downloadStartPoint = 0 // How far into the individual file progress portion the download progress starts
|
||||||
let fileProgress = 0
|
// let fileProgress = 0
|
||||||
|
|
||||||
downloader.on('waitProgress', (remainingSeconds: number, totalSeconds: number) => {
|
// downloader.on('waitProgress', (remainingSeconds: number, totalSeconds: number) => {
|
||||||
downloadStartPoint = this.individualFileProgressPortion / 2
|
// downloadStartPoint = this.individualFileProgressPortion / 2
|
||||||
this.percent =
|
// this.percent =
|
||||||
this._allFilesProgress + interpolate(remainingSeconds, totalSeconds, 0, 0, this.individualFileProgressPortion / 2)
|
// this._allFilesProgress + interpolate(remainingSeconds, totalSeconds, 0, 0, this.individualFileProgressPortion / 2)
|
||||||
this.updateGUI(downloadHeader, `Waiting for Google rate limit... (${remainingSeconds}s)`, 'good')
|
// this.updateGUI(downloadHeader, `Waiting for Google rate limit... (${remainingSeconds}s)`, 'good')
|
||||||
})
|
// })
|
||||||
|
|
||||||
downloader.on('requestSent', () => {
|
// downloader.on('requestSent', () => {
|
||||||
fileProgress = downloadStartPoint
|
// fileProgress = downloadStartPoint
|
||||||
this.percent = this._allFilesProgress + fileProgress
|
// this.percent = this._allFilesProgress + fileProgress
|
||||||
this.updateGUI(downloadHeader, 'Sending request...', 'good')
|
// this.updateGUI(downloadHeader, 'Sending request...', 'good')
|
||||||
})
|
// })
|
||||||
|
|
||||||
downloader.on('downloadProgress', (bytesDownloaded: number) => {
|
// downloader.on('downloadProgress', (bytesDownloaded: number) => {
|
||||||
downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
|
// downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
|
||||||
const size = Number(this.files[fileIndex].size)
|
// const size = Number(this.files[fileIndex].size)
|
||||||
fileProgress = interpolate(bytesDownloaded, 0, size, downloadStartPoint, this.individualFileProgressPortion)
|
// fileProgress = interpolate(bytesDownloaded, 0, size, downloadStartPoint, this.individualFileProgressPortion)
|
||||||
this.percent = this._allFilesProgress + fileProgress
|
// this.percent = this._allFilesProgress + fileProgress
|
||||||
this.updateGUI(downloadHeader, `Downloading... (${Math.round(1000 * bytesDownloaded / size) / 10}%)`, 'good')
|
// this.updateGUI(downloadHeader, `Downloading... (${Math.round(1000 * bytesDownloaded / size) / 10}%)`, 'good')
|
||||||
})
|
// })
|
||||||
|
|
||||||
downloader.on('error', this.handleError.bind(this))
|
// downloader.on('error', this.handleError.bind(this))
|
||||||
|
|
||||||
return new Promise<void>(resolve => {
|
// return new Promise<void>(resolve => {
|
||||||
downloader.on('complete', () => {
|
// downloader.on('complete', () => {
|
||||||
this._allFilesProgress += this.individualFileProgressPortion
|
// this._allFilesProgress += this.individualFileProgressPortion
|
||||||
resolve()
|
// resolve()
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines what happens in response to `FileExtractor` events.
|
* Defines what happens in response to `FileExtractor` events.
|
||||||
* @returns a `Promise` that resolves when the extraction finishes.
|
* @returns a `Promise` that resolves when the extraction finishes.
|
||||||
*/
|
*/
|
||||||
private addExtractorEventListeners(extractor: FileExtractor) {
|
// private addExtractorEventListeners(extractor: FileExtractor) {
|
||||||
let archive = ''
|
// let archive = ''
|
||||||
|
|
||||||
extractor.on('start', filename => {
|
// extractor.on('start', filename => {
|
||||||
archive = filename
|
// archive = filename
|
||||||
this.updateGUI(`[${archive}]`, 'Extracting...', 'good')
|
// this.updateGUI(`[${archive}]`, 'Extracting...', 'good')
|
||||||
})
|
// })
|
||||||
|
|
||||||
extractor.on('extractProgress', (percent, filecount) => {
|
// extractor.on('extractProgress', (percent, filecount) => {
|
||||||
this.percent = interpolate(percent, 0, 100, 80, 95)
|
// this.percent = interpolate(percent, 0, 100, 80, 95)
|
||||||
this.updateGUI(`[${archive}] (${filecount} file${filecount == 1 ? '' : 's'} extracted)`, `Extracting... (${percent}%)`, 'good')
|
// this.updateGUI(`[${archive}] (${filecount} file${filecount == 1 ? '' : 's'} extracted)`, `Extracting... (${percent}%)`, 'good')
|
||||||
})
|
// })
|
||||||
|
|
||||||
extractor.on('error', this.handleError.bind(this))
|
// extractor.on('error', this.handleError.bind(this))
|
||||||
|
|
||||||
return new Promise<void>(resolve => {
|
// return new Promise<void>(resolve => {
|
||||||
extractor.on('complete', () => {
|
// extractor.on('complete', () => {
|
||||||
this.percent = 95
|
// this.percent = 95
|
||||||
resolve()
|
// resolve()
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines what happens in response to `FileTransfer` events.
|
* Defines what happens in response to `FileTransfer` events.
|
||||||
|
|||||||
@@ -1,368 +1,368 @@
|
|||||||
import Bottleneck from 'bottleneck'
|
// import Bottleneck from 'bottleneck'
|
||||||
import { createWriteStream, writeFile as _writeFile } from 'fs'
|
// import { createWriteStream, writeFile as _writeFile } from 'fs'
|
||||||
import { google } from 'googleapis'
|
// import { google } from 'googleapis'
|
||||||
import * as needle from 'needle'
|
// import * as needle from 'needle'
|
||||||
import { join } from 'path'
|
// import { join } from 'path'
|
||||||
import { Readable } from 'stream'
|
// import { Readable } from 'stream'
|
||||||
import { inspect, promisify } from 'util'
|
// import { inspect, promisify } from 'util'
|
||||||
|
|
||||||
import { devLog } from '../../shared/ElectronUtilFunctions'
|
// import { devLog } from '../../shared/ElectronUtilFunctions'
|
||||||
import { tempPath } from '../../shared/Paths'
|
// import { tempPath } from '../../shared/Paths'
|
||||||
import { AnyFunction } from '../../shared/UtilFunctions'
|
// import { AnyFunction } from '../../shared/UtilFunctions'
|
||||||
import { DownloadError } from './ChartDownload'
|
// import { DownloadError } from './ChartDownload'
|
||||||
// TODO: replace needle with got (for cancel() method) (if before-headers event is possible?)
|
// // TODO: replace needle with got (for cancel() method) (if before-headers event is possible?)
|
||||||
import { googleTimer } from './GoogleTimer'
|
// import { googleTimer } from './GoogleTimer'
|
||||||
|
|
||||||
const drive = google.drive('v3')
|
// const drive = google.drive('v3')
|
||||||
const limiter = new Bottleneck({
|
// const limiter = new Bottleneck({
|
||||||
minTime: 200, // Wait 200 ms between API requests
|
// minTime: 200, // Wait 200 ms between API requests
|
||||||
})
|
// })
|
||||||
|
|
||||||
const RETRY_MAX = 2
|
// const RETRY_MAX = 2
|
||||||
const writeFile = promisify(_writeFile)
|
// const writeFile = promisify(_writeFile)
|
||||||
|
|
||||||
interface EventCallback {
|
// interface EventCallback {
|
||||||
'waitProgress': (remainingSeconds: number, totalSeconds: number) => void
|
// 'waitProgress': (remainingSeconds: number, totalSeconds: number) => void
|
||||||
/** Note: this event can be called multiple times if the connection times out or a large file is downloaded */
|
// /** Note: this event can be called multiple times if the connection times out or a large file is downloaded */
|
||||||
'requestSent': () => void
|
// 'requestSent': () => void
|
||||||
'downloadProgress': (bytesDownloaded: number) => void
|
// 'downloadProgress': (bytesDownloaded: number) => void
|
||||||
/** Note: after calling retry, the event lifecycle restarts */
|
// /** Note: after calling retry, the event lifecycle restarts */
|
||||||
'error': (err: DownloadError, retry: () => void) => void
|
// 'error': (err: DownloadError, retry: () => void) => void
|
||||||
'complete': () => void
|
// 'complete': () => void
|
||||||
}
|
// }
|
||||||
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
// type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||||
export type FileDownloader = APIFileDownloader | SlowFileDownloader
|
// export type FileDownloader = APIFileDownloader | SlowFileDownloader
|
||||||
|
|
||||||
const downloadErrors = {
|
// const downloadErrors = {
|
||||||
timeout: (type: string) => { return { header: 'Timeout', body: `The download server could not be reached. (type=${type})` } },
|
// timeout: (type: string) => { return { header: 'Timeout', body: `The download server could not be reached. (type=${type})` } },
|
||||||
connectionError: (err: Error) => { return { header: 'Connection Error', body: `${err.name}: ${err.message}` } },
|
// connectionError: (err: Error) => { return { header: 'Connection Error', body: `${err.name}: ${err.message}` } },
|
||||||
responseError: (statusCode: string) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } },
|
// responseError: (statusCode: string) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } },
|
||||||
htmlError: (path: string) => { return { header: 'Download server returned HTML instead of a file.', body: path, isLink: true } },
|
// htmlError: (path: string) => { return { header: 'Download server returned HTML instead of a file.', body: path, isLink: true } },
|
||||||
linkError: (url: string) => { return { header: 'Invalid link', body: `The download link is not formatted correctly: ${url}` } },
|
// linkError: (url: string) => { return { header: 'Invalid link', body: `The download link is not formatted correctly: ${url}` } },
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Downloads a file from `url` to `fullPath`.
|
// * Downloads a file from `url` to `fullPath`.
|
||||||
* Will handle google drive virus scan warnings. Provides event listeners for download progress.
|
// * Will handle google drive virus scan warnings. Provides event listeners for download progress.
|
||||||
* On error, provides the ability to retry.
|
// * On error, provides the ability to retry.
|
||||||
* Will only send download requests once every `getSettings().rateLimitDelay` seconds if a Google account has not been authenticated.
|
// * Will only send download requests once every `getSettings().rateLimitDelay` seconds.
|
||||||
* @param url The download link.
|
// */
|
||||||
* @param fullPath The full path to where this file should be stored (including the filename).
|
// class SlowFileDownloader {
|
||||||
*/
|
|
||||||
export function getDownloader(url: string, fullPath: string): FileDownloader {
|
|
||||||
return new SlowFileDownloader(url, fullPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// private callbacks = {} as Callbacks
|
||||||
* Downloads a file from `url` to `fullPath`.
|
// private retryCount: number
|
||||||
* On error, provides the ability to retry.
|
// private wasCanceled = false
|
||||||
*/
|
// private req: NodeJS.ReadableStream
|
||||||
class APIFileDownloader {
|
|
||||||
private readonly URL_REGEX = /uc\?id=([^&]*)&export=download/u
|
|
||||||
|
|
||||||
private callbacks = {} as Callbacks
|
// /**
|
||||||
private retryCount: number
|
// * @param url The download link.
|
||||||
private wasCanceled = false
|
// * @param fullPath The full path to where this file should be stored (including the filename).
|
||||||
private fileID: string
|
// */
|
||||||
private downloadStream: Readable
|
// constructor(private url: string, private fullPath: string) { }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* @param url The download link.
|
// * Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called)
|
||||||
* @param fullPath The full path to where this file should be stored (including the filename).
|
// */
|
||||||
*/
|
// on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
|
||||||
constructor(private url: string, private fullPath: string) {
|
// this.callbacks[event] = callback
|
||||||
// url looks like: "https://drive.google.com/uc?id=1TlxtOZlVgRgX7-1tyW0d5QzXVfL-MC3Q&export=download"
|
// }
|
||||||
this.fileID = this.URL_REGEX.exec(url)[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called)
|
// * Download the file after waiting for the google rate limit.
|
||||||
*/
|
// */
|
||||||
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
|
// beginDownload() {
|
||||||
this.callbacks[event] = callback
|
// googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => {
|
||||||
}
|
// this.callbacks.waitProgress(remainingSeconds, totalSeconds)
|
||||||
|
// }))
|
||||||
|
|
||||||
/**
|
// googleTimer.on('complete', this.cancelable(() => {
|
||||||
* Download the file after waiting for the google rate limit.
|
// this.requestDownload()
|
||||||
*/
|
// }))
|
||||||
beginDownload() {
|
// }
|
||||||
if (this.fileID == undefined) {
|
|
||||||
this.failDownload(downloadErrors.linkError(this.url))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startDownloadStream()
|
// /**
|
||||||
}
|
// * Sends a request to download the file at `this.url`.
|
||||||
|
// * @param cookieHeader the "cookie=" header to include this request.
|
||||||
|
// */
|
||||||
|
// private requestDownload(cookieHeader?: string) {
|
||||||
|
// this.callbacks.requestSent()
|
||||||
|
// this.req = needle.get(this.url, {
|
||||||
|
// 'follow_max': 10,
|
||||||
|
// 'open_timeout': 5000,
|
||||||
|
// 'headers': Object.assign({
|
||||||
|
// 'Referer': this.url,
|
||||||
|
// 'Accept': '*/*',
|
||||||
|
// },
|
||||||
|
// (cookieHeader ? { 'Cookie': cookieHeader } : undefined)
|
||||||
|
// ),
|
||||||
|
// })
|
||||||
|
|
||||||
/**
|
// this.req.on('timeout', this.cancelable((type: string) => {
|
||||||
* Uses the Google Drive API to start a download stream for the file with `this.fileID`.
|
// this.retryCount++
|
||||||
*/
|
// if (this.retryCount <= RETRY_MAX) {
|
||||||
private startDownloadStream() {
|
// devLog(`TIMEOUT: Retry attempt ${this.retryCount}...`)
|
||||||
limiter.schedule(this.cancelable(async () => {
|
// this.requestDownload(cookieHeader)
|
||||||
this.callbacks.requestSent()
|
// } else {
|
||||||
try {
|
// this.failDownload(downloadErrors.timeout(type))
|
||||||
this.downloadStream = (await drive.files.get({
|
// }
|
||||||
fileId: this.fileID,
|
// }))
|
||||||
alt: 'media',
|
|
||||||
}, {
|
|
||||||
responseType: 'stream',
|
|
||||||
})).data
|
|
||||||
|
|
||||||
if (this.wasCanceled) { return }
|
// this.req.on('err', this.cancelable((err: Error) => {
|
||||||
|
// this.failDownload(downloadErrors.connectionError(err))
|
||||||
|
// }))
|
||||||
|
|
||||||
this.handleDownloadResponse()
|
// this.req.on('header', this.cancelable((statusCode, headers: Headers) => {
|
||||||
} catch (err) {
|
// if (statusCode != 200) {
|
||||||
this.retryCount++
|
// this.failDownload(downloadErrors.responseError(statusCode))
|
||||||
if (this.retryCount <= RETRY_MAX) {
|
// return
|
||||||
devLog(`Failed to get file: Retry attempt ${this.retryCount}...`)
|
// }
|
||||||
if (this.wasCanceled) { return }
|
|
||||||
this.startDownloadStream()
|
|
||||||
} else {
|
|
||||||
devLog(inspect(err))
|
|
||||||
if (err?.code && err?.response?.statusText) {
|
|
||||||
this.failDownload(downloadErrors.responseError(`${err.code} (${err.response.statusText})`))
|
|
||||||
} else {
|
|
||||||
this.failDownload(downloadErrors.responseError(err?.code ?? 'unknown'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// if (headers['content-type'].startsWith('text/html')) {
|
||||||
* Pipes the data from a download response to `this.fullPath`.
|
// this.handleHTMLResponse(headers['set-cookie'])
|
||||||
* @param req The download request.
|
// } else {
|
||||||
*/
|
// this.handleDownloadResponse()
|
||||||
private handleDownloadResponse() {
|
// }
|
||||||
this.callbacks.downloadProgress(0)
|
// }))
|
||||||
let downloadedSize = 0
|
// }
|
||||||
const writeStream = createWriteStream(this.fullPath)
|
|
||||||
|
|
||||||
try {
|
// /**
|
||||||
this.downloadStream.pipe(writeStream)
|
// * A Google Drive HTML response to a download request usually means this is the "file too large to scan for viruses" warning.
|
||||||
} catch (err) {
|
// * This function sends the request that results from clicking "download anyway", or generates an error if it can't be found.
|
||||||
this.failDownload(downloadErrors.connectionError(err))
|
// * @param cookieHeader The "cookie=" header of this request.
|
||||||
}
|
// */
|
||||||
|
// private handleHTMLResponse(cookieHeader: string) {
|
||||||
|
// let virusScanHTML = ''
|
||||||
|
// 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 {
|
||||||
|
// try {
|
||||||
|
// const confirmTokenRegex = /confirm=([0-9A-Za-z\-_]+)&/g
|
||||||
|
// const confirmTokenResults = confirmTokenRegex.exec(virusScanHTML)
|
||||||
|
// const confirmToken = confirmTokenResults[1]
|
||||||
|
// const downloadID = this.url.substr(this.url.indexOf('id=') + 'id='.length)
|
||||||
|
// this.url = `https://drive.google.com/uc?confirm=${confirmToken}&id=${downloadID}`
|
||||||
|
// const warningCode = /download_warning_([^=]*)=/.exec(cookieHeader)[1]
|
||||||
|
// const NID = /NID=([^;]*);/.exec(cookieHeader)[1].replace('=', '%')
|
||||||
|
// const newHeader = `download_warning_${warningCode}=${confirmToken}; NID=${NID}`
|
||||||
|
// this.requestDownload(newHeader)
|
||||||
|
// } catch (e) {
|
||||||
|
// this.saveHTMLError(virusScanHTML).then(path => {
|
||||||
|
// this.failDownload(downloadErrors.htmlError(path))
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
|
||||||
this.downloadStream.on('data', this.cancelable((chunk: Buffer) => {
|
// /**
|
||||||
downloadedSize += chunk.length
|
// * Pipes the data from a download response to `this.fullPath`.
|
||||||
}))
|
// * @param req The download request.
|
||||||
|
// */
|
||||||
|
// private handleDownloadResponse() {
|
||||||
|
// this.callbacks.downloadProgress(0)
|
||||||
|
// let downloadedSize = 0
|
||||||
|
// this.req.pipe(createWriteStream(this.fullPath))
|
||||||
|
// this.req.on('data', this.cancelable(data => {
|
||||||
|
// downloadedSize += data.length
|
||||||
|
// this.callbacks.downloadProgress(downloadedSize)
|
||||||
|
// }))
|
||||||
|
|
||||||
const progressUpdater = setInterval(() => {
|
// this.req.on('err', this.cancelable((err: Error) => {
|
||||||
this.callbacks.downloadProgress(downloadedSize)
|
// this.failDownload(downloadErrors.connectionError(err))
|
||||||
}, 100)
|
// }))
|
||||||
|
|
||||||
this.downloadStream.on('error', this.cancelable((err: Error) => {
|
// this.req.on('end', this.cancelable(() => {
|
||||||
clearInterval(progressUpdater)
|
// this.callbacks.complete()
|
||||||
this.failDownload(downloadErrors.connectionError(err))
|
// }))
|
||||||
}))
|
// }
|
||||||
|
|
||||||
this.downloadStream.on('end', this.cancelable(() => {
|
// private async saveHTMLError(text: string) {
|
||||||
clearInterval(progressUpdater)
|
// const errorPath = join(tempPath, 'HTMLError.html')
|
||||||
writeStream.end()
|
// await writeFile(errorPath, text)
|
||||||
this.downloadStream.destroy()
|
// return errorPath
|
||||||
this.downloadStream = null
|
// }
|
||||||
|
|
||||||
this.callbacks.complete()
|
// /**
|
||||||
}))
|
// * Display an error message and provide a function to retry the download.
|
||||||
}
|
// */
|
||||||
|
// private failDownload(error: DownloadError) {
|
||||||
|
// this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Display an error message and provide a function to retry the download.
|
// * Stop the process of downloading the file. (no more events will be fired after this is called)
|
||||||
*/
|
// */
|
||||||
private failDownload(error: DownloadError) {
|
// cancelDownload() {
|
||||||
this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
|
// this.wasCanceled = true
|
||||||
}
|
// googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
|
||||||
|
// if (this.req) {
|
||||||
|
// // TODO: destroy request
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Stop the process of downloading the file. (no more events will be fired after this is called)
|
// * Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
|
||||||
*/
|
// */
|
||||||
cancelDownload() {
|
// private cancelable<F extends AnyFunction>(fn: F) {
|
||||||
this.wasCanceled = true
|
// return (...args: Parameters<F>): ReturnType<F> => {
|
||||||
googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
|
// if (this.wasCanceled) { return }
|
||||||
if (this.downloadStream) {
|
// return fn(...Array.from(args))
|
||||||
this.downloadStream.destroy()
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
|
// * Downloads a file from `url` to `fullPath`.
|
||||||
*/
|
// * On error, provides the ability to retry.
|
||||||
private cancelable<F extends AnyFunction>(fn: F) {
|
// */
|
||||||
return (...args: Parameters<F>): ReturnType<F> => {
|
// class APIFileDownloader {
|
||||||
if (this.wasCanceled) { return }
|
// private readonly URL_REGEX = /uc\?id=([^&]*)&export=download/u
|
||||||
return fn(...Array.from(args))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// private callbacks = {} as Callbacks
|
||||||
* Downloads a file from `url` to `fullPath`.
|
// private retryCount: number
|
||||||
* Will handle google drive virus scan warnings. Provides event listeners for download progress.
|
// private wasCanceled = false
|
||||||
* On error, provides the ability to retry.
|
// private fileID: string
|
||||||
* Will only send download requests once every `getSettings().rateLimitDelay` seconds.
|
// private downloadStream: Readable
|
||||||
*/
|
|
||||||
class SlowFileDownloader {
|
|
||||||
|
|
||||||
private callbacks = {} as Callbacks
|
// /**
|
||||||
private retryCount: number
|
// * @param url The download link.
|
||||||
private wasCanceled = false
|
// * @param fullPath The full path to where this file should be stored (including the filename).
|
||||||
private req: NodeJS.ReadableStream
|
// */
|
||||||
|
// constructor(private url: string, private fullPath: string) {
|
||||||
|
// // url looks like: "https://drive.google.com/uc?id=1TlxtOZlVgRgX7-1tyW0d5QzXVfL-MC3Q&export=download"
|
||||||
|
// this.fileID = this.URL_REGEX.exec(url)[1]
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* @param url The download link.
|
// * Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called)
|
||||||
* @param fullPath The full path to where this file should be stored (including the filename).
|
// */
|
||||||
*/
|
// on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
|
||||||
constructor(private url: string, private fullPath: string) { }
|
// this.callbacks[event] = callback
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called)
|
// * Download the file after waiting for the google rate limit.
|
||||||
*/
|
// */
|
||||||
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
|
// beginDownload() {
|
||||||
this.callbacks[event] = callback
|
// if (this.fileID == undefined) {
|
||||||
}
|
// this.failDownload(downloadErrors.linkError(this.url))
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
// this.startDownloadStream()
|
||||||
* Download the file after waiting for the google rate limit.
|
// }
|
||||||
*/
|
|
||||||
beginDownload() {
|
|
||||||
googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => {
|
|
||||||
this.callbacks.waitProgress(remainingSeconds, totalSeconds)
|
|
||||||
}))
|
|
||||||
|
|
||||||
googleTimer.on('complete', this.cancelable(() => {
|
// /**
|
||||||
this.requestDownload()
|
// * Uses the Google Drive API to start a download stream for the file with `this.fileID`.
|
||||||
}))
|
// */
|
||||||
}
|
// private startDownloadStream() {
|
||||||
|
// limiter.schedule(this.cancelable(async () => {
|
||||||
|
// this.callbacks.requestSent()
|
||||||
|
// try {
|
||||||
|
// this.downloadStream = (await drive.files.get({
|
||||||
|
// fileId: this.fileID,
|
||||||
|
// alt: 'media',
|
||||||
|
// }, {
|
||||||
|
// responseType: 'stream',
|
||||||
|
// })).data
|
||||||
|
|
||||||
/**
|
// if (this.wasCanceled) { return }
|
||||||
* Sends a request to download the file at `this.url`.
|
|
||||||
* @param cookieHeader the "cookie=" header to include this request.
|
|
||||||
*/
|
|
||||||
private requestDownload(cookieHeader?: string) {
|
|
||||||
this.callbacks.requestSent()
|
|
||||||
this.req = needle.get(this.url, {
|
|
||||||
'follow_max': 10,
|
|
||||||
'open_timeout': 5000,
|
|
||||||
'headers': Object.assign({
|
|
||||||
'Referer': this.url,
|
|
||||||
'Accept': '*/*',
|
|
||||||
},
|
|
||||||
(cookieHeader ? { 'Cookie': cookieHeader } : undefined)
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
this.req.on('timeout', this.cancelable((type: string) => {
|
// this.handleDownloadResponse()
|
||||||
this.retryCount++
|
// } catch (err) {
|
||||||
if (this.retryCount <= RETRY_MAX) {
|
// this.retryCount++
|
||||||
devLog(`TIMEOUT: Retry attempt ${this.retryCount}...`)
|
// if (this.retryCount <= RETRY_MAX) {
|
||||||
this.requestDownload(cookieHeader)
|
// devLog(`Failed to get file: Retry attempt ${this.retryCount}...`)
|
||||||
} else {
|
// if (this.wasCanceled) { return }
|
||||||
this.failDownload(downloadErrors.timeout(type))
|
// this.startDownloadStream()
|
||||||
}
|
// } else {
|
||||||
}))
|
// devLog(inspect(err))
|
||||||
|
// if (err?.code && err?.response?.statusText) {
|
||||||
|
// this.failDownload(downloadErrors.responseError(`${err.code} (${err.response.statusText})`))
|
||||||
|
// } else {
|
||||||
|
// this.failDownload(downloadErrors.responseError(err?.code ?? 'unknown'))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
|
||||||
this.req.on('err', this.cancelable((err: Error) => {
|
// /**
|
||||||
this.failDownload(downloadErrors.connectionError(err))
|
// * Pipes the data from a download response to `this.fullPath`.
|
||||||
}))
|
// * @param req The download request.
|
||||||
|
// */
|
||||||
|
// private handleDownloadResponse() {
|
||||||
|
// this.callbacks.downloadProgress(0)
|
||||||
|
// let downloadedSize = 0
|
||||||
|
// const writeStream = createWriteStream(this.fullPath)
|
||||||
|
|
||||||
this.req.on('header', this.cancelable((statusCode, headers: Headers) => {
|
// try {
|
||||||
if (statusCode != 200) {
|
// this.downloadStream.pipe(writeStream)
|
||||||
this.failDownload(downloadErrors.responseError(statusCode))
|
// } catch (err) {
|
||||||
return
|
// this.failDownload(downloadErrors.connectionError(err))
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (headers['content-type'].startsWith('text/html')) {
|
// this.downloadStream.on('data', this.cancelable((chunk: Buffer) => {
|
||||||
this.handleHTMLResponse(headers['set-cookie'])
|
// downloadedSize += chunk.length
|
||||||
} else {
|
// }))
|
||||||
this.handleDownloadResponse()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// const progressUpdater = setInterval(() => {
|
||||||
* A Google Drive HTML response to a download request usually means this is the "file too large to scan for viruses" warning.
|
// this.callbacks.downloadProgress(downloadedSize)
|
||||||
* This function sends the request that results from clicking "download anyway", or generates an error if it can't be found.
|
// }, 100)
|
||||||
* @param cookieHeader The "cookie=" header of this request.
|
|
||||||
*/
|
|
||||||
private handleHTMLResponse(cookieHeader: string) {
|
|
||||||
let virusScanHTML = ''
|
|
||||||
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 {
|
|
||||||
try {
|
|
||||||
const confirmTokenRegex = /confirm=([0-9A-Za-z\-_]+)&/g
|
|
||||||
const confirmTokenResults = confirmTokenRegex.exec(virusScanHTML)
|
|
||||||
const confirmToken = confirmTokenResults[1]
|
|
||||||
const downloadID = this.url.substr(this.url.indexOf('id=') + 'id='.length)
|
|
||||||
this.url = `https://drive.google.com/uc?confirm=${confirmToken}&id=${downloadID}`
|
|
||||||
const warningCode = /download_warning_([^=]*)=/.exec(cookieHeader)[1]
|
|
||||||
const NID = /NID=([^;]*);/.exec(cookieHeader)[1].replace('=', '%')
|
|
||||||
const newHeader = `download_warning_${warningCode}=${confirmToken}; NID=${NID}`
|
|
||||||
this.requestDownload(newHeader)
|
|
||||||
} catch (e) {
|
|
||||||
this.saveHTMLError(virusScanHTML).then(path => {
|
|
||||||
this.failDownload(downloadErrors.htmlError(path))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// this.downloadStream.on('error', this.cancelable((err: Error) => {
|
||||||
* Pipes the data from a download response to `this.fullPath`.
|
// clearInterval(progressUpdater)
|
||||||
* @param req The download request.
|
// this.failDownload(downloadErrors.connectionError(err))
|
||||||
*/
|
// }))
|
||||||
private handleDownloadResponse() {
|
|
||||||
this.callbacks.downloadProgress(0)
|
|
||||||
let downloadedSize = 0
|
|
||||||
this.req.pipe(createWriteStream(this.fullPath))
|
|
||||||
this.req.on('data', this.cancelable(data => {
|
|
||||||
downloadedSize += data.length
|
|
||||||
this.callbacks.downloadProgress(downloadedSize)
|
|
||||||
}))
|
|
||||||
|
|
||||||
this.req.on('err', this.cancelable((err: Error) => {
|
// this.downloadStream.on('end', this.cancelable(() => {
|
||||||
this.failDownload(downloadErrors.connectionError(err))
|
// clearInterval(progressUpdater)
|
||||||
}))
|
// writeStream.end()
|
||||||
|
// this.downloadStream.destroy()
|
||||||
|
// this.downloadStream = null
|
||||||
|
|
||||||
this.req.on('end', this.cancelable(() => {
|
// this.callbacks.complete()
|
||||||
this.callbacks.complete()
|
// }))
|
||||||
}))
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
private async saveHTMLError(text: string) {
|
// /**
|
||||||
const errorPath = join(tempPath, 'HTMLError.html')
|
// * Display an error message and provide a function to retry the download.
|
||||||
await writeFile(errorPath, text)
|
// */
|
||||||
return errorPath
|
// private failDownload(error: DownloadError) {
|
||||||
}
|
// this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Display an error message and provide a function to retry the download.
|
// * Stop the process of downloading the file. (no more events will be fired after this is called)
|
||||||
*/
|
// */
|
||||||
private failDownload(error: DownloadError) {
|
// cancelDownload() {
|
||||||
this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
|
// this.wasCanceled = true
|
||||||
}
|
// googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
|
||||||
|
// if (this.downloadStream) {
|
||||||
|
// this.downloadStream.destroy()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Stop the process of downloading the file. (no more events will be fired after this is called)
|
// * Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
|
||||||
*/
|
// */
|
||||||
cancelDownload() {
|
// private cancelable<F extends AnyFunction>(fn: F) {
|
||||||
this.wasCanceled = true
|
// return (...args: Parameters<F>): ReturnType<F> => {
|
||||||
googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
|
// if (this.wasCanceled) { return }
|
||||||
if (this.req) {
|
// return fn(...Array.from(args))
|
||||||
// TODO: destroy request
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
|
// * Downloads a file from `url` to `fullPath`.
|
||||||
*/
|
// * Will handle google drive virus scan warnings. Provides event listeners for download progress.
|
||||||
private cancelable<F extends AnyFunction>(fn: F) {
|
// * On error, provides the ability to retry.
|
||||||
return (...args: Parameters<F>): ReturnType<F> => {
|
// * Will only send download requests once every `getSettings().rateLimitDelay` seconds if a Google account has not been authenticated.
|
||||||
if (this.wasCanceled) { return }
|
// * @param url The download link.
|
||||||
return fn(...Array.from(args))
|
// * @param fullPath The full path to where this file should be stored (including the filename).
|
||||||
}
|
// */
|
||||||
}
|
// export function getDownloader(url: string, fullPath: string): FileDownloader {
|
||||||
}
|
// return new SlowFileDownloader(url, fullPath)
|
||||||
|
// }
|
||||||
|
|||||||
@@ -1,164 +1,170 @@
|
|||||||
import * as zipBin from '7zip-bin'
|
// import * as zipBin from '7zip-bin'
|
||||||
import { mkdir as _mkdir, readdir, unlink } from 'fs'
|
// import { mkdir as _mkdir, readdir, unlink } from 'fs'
|
||||||
import * as node7z from 'node-7z'
|
// import * as node7z from 'node-7z'
|
||||||
import * as unrarjs from 'node-unrar-js' // TODO find better rar library that has async extraction
|
// import * as unrarjs from 'node-unrar-js' // TODO find better rar library that has async extraction
|
||||||
import { FailReason } from 'node-unrar-js/dist/js/extractor'
|
// import { FailReason } from 'node-unrar-js/dist/js/extractor'
|
||||||
import { extname, join } from 'path'
|
// import { extname, join } from 'path'
|
||||||
import { promisify } from 'util'
|
// import { promisify } from 'util'
|
||||||
|
|
||||||
import { devLog } from '../../shared/ElectronUtilFunctions'
|
// import { devLog } from '../../shared/ElectronUtilFunctions'
|
||||||
import { AnyFunction } from '../../shared/UtilFunctions'
|
// import { AnyFunction } from '../../shared/UtilFunctions'
|
||||||
import { DownloadError } from './ChartDownload'
|
// import { DownloadError } from './ChartDownload'
|
||||||
|
|
||||||
const mkdir = promisify(_mkdir)
|
// const mkdir = promisify(_mkdir)
|
||||||
|
|
||||||
interface EventCallback {
|
// interface EventCallback {
|
||||||
'start': (filename: string) => void
|
// 'start': (filename: string) => void
|
||||||
'extractProgress': (percent: number, fileCount: number) => void
|
// 'extractProgress': (percent: number, fileCount: number) => void
|
||||||
'error': (err: DownloadError, retry: () => void | Promise<void>) => void
|
// 'error': (err: DownloadError, retry: () => void | Promise<void>) => void
|
||||||
'complete': () => void
|
// 'complete': () => void
|
||||||
}
|
// }
|
||||||
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
// type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||||
|
|
||||||
const extractErrors = {
|
// const extractErrors = {
|
||||||
readError: (err: NodeJS.ErrnoException) => { return { header: `Failed to read file (${err.code})`, body: `${err.name}: ${err.message}` } },
|
// readError: (err: NodeJS.ErrnoException) => ({ header: `Failed to read file (${err.code})`, body: `${err.name}: ${err.message}` }),
|
||||||
emptyError: () => { return { header: 'Failed to extract archive', body: 'File archive was downloaded but could not be found' } },
|
// emptyError: () => ({ header: 'Failed to extract archive', body: 'File archive was downloaded but could not be found' }),
|
||||||
rarmkdirError: (err: NodeJS.ErrnoException, sourceFile: string) => {
|
// rarmkdirError: (err: NodeJS.ErrnoException, sourceFile: string) => {
|
||||||
return { header: `Extracting archive failed. (${err.code})`, body: `${err.name}: ${err.message} (${sourceFile})` }
|
// return { header: `Extracting archive failed. (${err.code})`, body: `${err.name}: ${err.message} (${sourceFile})` }
|
||||||
},
|
// },
|
||||||
rarextractError: (result: { reason: FailReason; msg: string }, sourceFile: string) => {
|
// rarextractError: (result: { reason: FailReason; msg: string }, sourceFile: string) => {
|
||||||
return { header: `Extracting archive failed: ${result.reason}`, body: `${result.msg} (${sourceFile})` }
|
// return { header: `Extracting archive failed: ${result.reason}`, body: `${result.msg} (${sourceFile})` }
|
||||||
},
|
// },
|
||||||
}
|
// }
|
||||||
|
|
||||||
export class FileExtractor {
|
// export class FileExtractor {
|
||||||
|
|
||||||
private callbacks = {} as Callbacks
|
// private callbacks = {} as Callbacks
|
||||||
private wasCanceled = false
|
// private wasCanceled = false
|
||||||
constructor(private sourceFolder: string) { }
|
// constructor(private sourceFolder: string) { }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelExtract()` is called)
|
// * Calls `callback` when `event` fires. (no events will be fired after `this.cancelExtract()` is called)
|
||||||
*/
|
// */
|
||||||
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
|
// on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
|
||||||
this.callbacks[event] = callback
|
// this.callbacks[event] = callback
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Extract the chart from `this.sourceFolder`. (assumes there is exactly one archive file in that folder)
|
// * Extract the chart from `this.sourceFolder`. (assumes there is exactly one archive file in that folder)
|
||||||
*/
|
// */
|
||||||
beginExtract() {
|
// beginExtract() {
|
||||||
setTimeout(this.cancelable(() => {
|
// setTimeout(this.cancelable(() => {
|
||||||
readdir(this.sourceFolder, (err, files) => {
|
// readdir(this.sourceFolder, (err, files) => {
|
||||||
if (err) {
|
// if (err) {
|
||||||
this.callbacks.error(extractErrors.readError(err), () => this.beginExtract())
|
// this.callbacks.error(extractErrors.readError(err), () => this.beginExtract())
|
||||||
} else if (files.length == 0) {
|
// } else if (files.length == 0) {
|
||||||
this.callbacks.error(extractErrors.emptyError(), () => this.beginExtract())
|
// this.callbacks.error(extractErrors.emptyError(), () => this.beginExtract())
|
||||||
} else {
|
// } else {
|
||||||
this.callbacks.start(files[0])
|
// this.callbacks.start(files[0])
|
||||||
this.extract(join(this.sourceFolder, files[0]), extname(files[0]) == '.rar')
|
// this.extract(join(this.sourceFolder, files[0]), extname(files[0]) == '.rar')
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}), 100) // Wait for filesystem to process downloaded file
|
// }), 100) // Wait for filesystem to process downloaded file
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Extracts the file at `fullPath` to `this.sourceFolder`.
|
// * Extracts the file at `fullPath` to `this.sourceFolder`.
|
||||||
*/
|
// */
|
||||||
private async extract(fullPath: string, useRarExtractor: boolean) {
|
// private async extract(fullPath: string, useRarExtractor: boolean) {
|
||||||
if (useRarExtractor) {
|
// if (useRarExtractor) {
|
||||||
await this.extractRar(fullPath) // Use node-unrar-js to extract the archive
|
// await this.extractRar(fullPath) // Use node-unrar-js to extract the archive
|
||||||
} else {
|
// } else {
|
||||||
this.extract7z(fullPath) // Use node-7z to extract the archive
|
// this.extract7z(fullPath) // Use node-7z to extract the archive
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Extracts a .rar archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
|
// * Extracts a .rar archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
|
||||||
* @throws an `ExtractError` if this fails.
|
// * @throws an `ExtractError` if this fails.
|
||||||
*/
|
// */
|
||||||
private async extractRar(fullPath: string) {
|
// private async extractRar(fullPath: string) {
|
||||||
const extractor = unrarjs.createExtractorFromFile(fullPath, this.sourceFolder)
|
// const extractor = unrarjs.createExtractorFromFile(fullPath, this.sourceFolder)
|
||||||
|
|
||||||
const fileList = extractor.getFileList()
|
// const fileList = extractor.getFileList()
|
||||||
|
|
||||||
if (fileList[0].state != 'FAIL') {
|
// if (fileList[0].state != 'FAIL') {
|
||||||
|
|
||||||
// Create directories for nested archives (because unrarjs didn't feel like handling that automatically)
|
// // Create directories for nested archives (because unrarjs didn't feel like handling that automatically)
|
||||||
const headers = fileList[1].fileHeaders
|
// const headers = fileList[1].fileHeaders
|
||||||
for (const header of headers) {
|
// for (const header of headers) {
|
||||||
if (header.flags.directory) {
|
// if (header.flags.directory) {
|
||||||
try {
|
// try {
|
||||||
await mkdir(join(this.sourceFolder, header.name), { recursive: true })
|
// await mkdir(join(this.sourceFolder, header.name), { recursive: true })
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
this.callbacks.error(extractErrors.rarmkdirError(err, fullPath), () => this.extract(fullPath, extname(fullPath) == '.rar'))
|
// this.callbacks.error(
|
||||||
return
|
// extractErrors.rarmkdirError(err, fullPath),
|
||||||
}
|
// () => this.extract(fullPath, extname(fullPath) == '.rar'),
|
||||||
}
|
// )
|
||||||
}
|
// return
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const extractResult = extractor.extractAll()
|
// const extractResult = extractor.extractAll()
|
||||||
|
|
||||||
if (extractResult[0].state == 'FAIL') {
|
// if (extractResult[0].state == 'FAIL') {
|
||||||
this.callbacks.error(extractErrors.rarextractError(extractResult[0], fullPath), () => this.extract(fullPath, extname(fullPath) == '.rar'))
|
// this.callbacks.error(
|
||||||
} else {
|
// extractErrors.rarextractError(extractResult[0], fullPath),
|
||||||
this.deleteArchive(fullPath)
|
// () => this.extract(fullPath, extname(fullPath) == '.rar'),
|
||||||
}
|
// )
|
||||||
}
|
// } else {
|
||||||
|
// this.deleteArchive(fullPath)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Extracts a .zip or .7z archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
|
// * Extracts a .zip or .7z archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
|
||||||
*/
|
// */
|
||||||
private extract7z(fullPath: string) {
|
// private extract7z(fullPath: string) {
|
||||||
const zipBinPath = zipBin.path7za.replace('app.asar', 'app.asar.unpacked') // I love electron-builder packaging :)
|
// const zipBinPath = zipBin.path7za.replace('app.asar', 'app.asar.unpacked') // I love electron-builder packaging :)
|
||||||
const stream = node7z.extractFull(fullPath, this.sourceFolder, { $progress: true, $bin: zipBinPath })
|
// const stream = node7z.extractFull(fullPath, this.sourceFolder, { $progress: true, $bin: zipBinPath })
|
||||||
|
|
||||||
stream.on('progress', this.cancelable((progress: { percent: number; fileCount: number }) => {
|
// stream.on('progress', this.cancelable((progress: { percent: number; fileCount: number }) => {
|
||||||
this.callbacks.extractProgress(progress.percent, isNaN(progress.fileCount) ? 0 : progress.fileCount)
|
// this.callbacks.extractProgress(progress.percent, isNaN(progress.fileCount) ? 0 : progress.fileCount)
|
||||||
}))
|
// }))
|
||||||
|
|
||||||
let extractErrorOccured = false
|
// let extractErrorOccured = false
|
||||||
stream.on('error', this.cancelable(() => {
|
// stream.on('error', this.cancelable(() => {
|
||||||
extractErrorOccured = true
|
// extractErrorOccured = true
|
||||||
devLog(`Failed to extract [${fullPath}]; retrying with .rar extractor...`)
|
// devLog(`Failed to extract [${fullPath}]; retrying with .rar extractor...`)
|
||||||
this.extract(fullPath, true)
|
// this.extract(fullPath, true)
|
||||||
}))
|
// }))
|
||||||
|
|
||||||
stream.on('end', this.cancelable(() => {
|
// stream.on('end', this.cancelable(() => {
|
||||||
if (!extractErrorOccured) {
|
// if (!extractErrorOccured) {
|
||||||
this.deleteArchive(fullPath)
|
// this.deleteArchive(fullPath)
|
||||||
}
|
// }
|
||||||
}))
|
// }))
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Tries to delete the archive at `fullPath`.
|
// * Tries to delete the archive at `fullPath`.
|
||||||
*/
|
// */
|
||||||
private deleteArchive(fullPath: string) {
|
// private deleteArchive(fullPath: string) {
|
||||||
unlink(fullPath, this.cancelable(err => {
|
// unlink(fullPath, this.cancelable(err => {
|
||||||
if (err && err.code != 'ENOENT') {
|
// if (err && err.code != 'ENOENT') {
|
||||||
devLog(`Warning: failed to delete archive at [${fullPath}]`)
|
// devLog(`Warning: failed to delete archive at [${fullPath}]`)
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.callbacks.complete()
|
// this.callbacks.complete()
|
||||||
}))
|
// }))
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Stop the process of extracting the file. (no more events will be fired after this is called)
|
// * Stop the process of extracting the file. (no more events will be fired after this is called)
|
||||||
*/
|
// */
|
||||||
cancelExtract() {
|
// cancelExtract() {
|
||||||
this.wasCanceled = true
|
// this.wasCanceled = true
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Wraps a function that is able to be prevented if `this.cancelExtract()` was called.
|
// * Wraps a function that is able to be prevented if `this.cancelExtract()` was called.
|
||||||
*/
|
// */
|
||||||
private cancelable<F extends AnyFunction>(fn: F) {
|
// private cancelable<F extends AnyFunction>(fn: F) {
|
||||||
return (...args: Parameters<F>): ReturnType<F> => {
|
// return (...args: Parameters<F>): ReturnType<F> => {
|
||||||
if (this.wasCanceled) { return }
|
// if (this.wasCanceled) { return }
|
||||||
return fn(...Array.from(args))
|
// return fn(...Array.from(args))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Dirent, readdir as _readdir } from 'fs'
|
import { Dirent, readdir as _readdir } from 'fs'
|
||||||
import * as mv from 'mv'
|
import mv from 'mv'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { rimraf } from 'rimraf'
|
import { rimraf } from 'rimraf'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
@@ -20,7 +20,10 @@ const transferErrors = {
|
|||||||
readError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to read file.'),
|
readError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to read file.'),
|
||||||
deleteError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete file.'),
|
deleteError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete file.'),
|
||||||
rimrafError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete folder.'),
|
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?)' : ''}`),
|
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) {
|
function fsError(err: NodeJS.ErrnoException, description: string) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface EventCallback {
|
|||||||
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||||
|
|
||||||
const filesystemErrors = {
|
const filesystemErrors = {
|
||||||
libraryFolder: () => { return { header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' } },
|
libraryFolder: () => ({ header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' }),
|
||||||
libraryAccess: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to access library folder.'),
|
libraryAccess: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to access library folder.'),
|
||||||
destinationFolderExists: (destinationPath: string) => {
|
destinationFolderExists: (destinationPath: string) => {
|
||||||
return { header: 'This chart already exists in your library folder.', body: destinationPath, isLink: true }
|
return { header: 'This chart already exists in your library folder.', body: destinationPath, isLink: true }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { app, BrowserWindow, ipcMain } from 'electron'
|
import { app, BrowserWindow, ipcMain } from 'electron'
|
||||||
import * as windowStateKeeper from 'electron-window-state'
|
import windowStateKeeper from 'electron-window-state'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as url from 'url'
|
import * as url from 'url'
|
||||||
|
|
||||||
@@ -9,12 +9,13 @@ import { updateChecker } from './ipc/UpdateHandler.ipc'
|
|||||||
import { getIPCEmitHandlers, getIPCInvokeHandlers, IPCEmitEvents } from './shared/IPCHandler'
|
import { getIPCEmitHandlers, getIPCInvokeHandlers, IPCEmitEvents } from './shared/IPCHandler'
|
||||||
import { dataPath } from './shared/Paths'
|
import { dataPath } from './shared/Paths'
|
||||||
|
|
||||||
require('electron-unhandled')({ showDialog: true })
|
import unhandled = require('electron-unhandled')
|
||||||
|
unhandled({ showDialog: true })
|
||||||
|
|
||||||
export let mainWindow: BrowserWindow
|
export let mainWindow: BrowserWindow
|
||||||
const args = process.argv.slice(1)
|
const args = process.argv.slice(1)
|
||||||
const isDevBuild = args.some(val => val == '--dev')
|
const isDevBuild = args.some(val => val == '--dev')
|
||||||
const remote = require('@electron/remote/main')
|
import remote = require('@electron/remote/main')
|
||||||
|
|
||||||
|
|
||||||
remote.initialize()
|
remote.initialize()
|
||||||
@@ -66,7 +67,7 @@ function handleOSXWindowClosed() {
|
|||||||
/**
|
/**
|
||||||
* Launches and initializes Bridge's main window.
|
* Launches and initializes Bridge's main window.
|
||||||
*/
|
*/
|
||||||
function createBridgeWindow() {
|
async function createBridgeWindow() {
|
||||||
|
|
||||||
// Load window size and maximized/restored state from previous session
|
// Load window size and maximized/restored state from previous session
|
||||||
const windowState = windowStateKeeper({
|
const windowState = windowStateKeeper({
|
||||||
@@ -89,7 +90,7 @@ function createBridgeWindow() {
|
|||||||
getIPCEmitHandlers().map(handler => ipcMain.on(handler.event, (_event, ...args) => handler.handler(args[0])))
|
getIPCEmitHandlers().map(handler => ipcMain.on(handler.event, (_event, ...args) => handler.handler(args[0])))
|
||||||
|
|
||||||
// Load angular app
|
// Load angular app
|
||||||
mainWindow.loadURL(getLoadUrl())
|
await loadWindow()
|
||||||
|
|
||||||
if (isDevBuild) {
|
if (isDevBuild) {
|
||||||
mainWindow.webContents.openDevTools()
|
mainWindow.webContents.openDevTools()
|
||||||
@@ -115,10 +116,9 @@ function createBrowserWindow(windowState: windowStateKeeper.State) {
|
|||||||
frame: false,
|
frame: false,
|
||||||
title: 'Bridge',
|
title: 'Bridge',
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
// preload:
|
||||||
allowRunningInsecureContent: (isDevBuild) ? true : false,
|
allowRunningInsecureContent: (isDevBuild) ? true : false,
|
||||||
textAreasAreResizable: false,
|
textAreasAreResizable: false,
|
||||||
contextIsolation: false,
|
|
||||||
},
|
},
|
||||||
simpleFullscreen: true,
|
simpleFullscreen: true,
|
||||||
fullscreenable: false,
|
fullscreenable: false,
|
||||||
@@ -132,6 +132,15 @@ function createBrowserWindow(windowState: windowStateKeeper.State) {
|
|||||||
return new BrowserWindow(options)
|
return new BrowserWindow(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadWindow(retries = 0) {
|
||||||
|
if (retries > 10) { throw new Error('Angular frontend did not load') }
|
||||||
|
try {
|
||||||
|
await mainWindow.loadURL(getLoadUrl())
|
||||||
|
} catch (err) {
|
||||||
|
await loadWindow(retries + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load from localhost during development; load from index.html in production
|
* Load from localhost during development; load from index.html in production
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ export function hasVideoExtension(name: string) {
|
|||||||
* Log a message in the main BrowserWindow's console.
|
* Log a message in the main BrowserWindow's console.
|
||||||
* Note: Error objects can't be serialized by this; use inspect(err) before passing it here.
|
* Note: Error objects can't be serialized by this; use inspect(err) before passing it here.
|
||||||
*/
|
*/
|
||||||
export function devLog(...messages: any[]) {
|
export function devLog(...messages: unknown[]) {
|
||||||
emitIPCEvent('log', messages)
|
emitIPCEvent('log', messages)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import * as randomBytes from 'randombytes'
|
import randomBytes from 'randombytes'
|
||||||
|
import sanitize from 'sanitize-filename'
|
||||||
const sanitize = require('sanitize-filename')
|
|
||||||
|
|
||||||
// WARNING: do not import anything related to Electron; the code will not compile correctly.
|
// WARNING: do not import anything related to Electron; the code will not compile correctly.
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"declaration": false,
|
"declaration": false,
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
|
|||||||
Reference in New Issue
Block a user