Improve chart folder download performance

This commit is contained in:
Geomitron
2023-12-28 13:29:11 -06:00
parent e1f2b126e0
commit abea03e099
4 changed files with 30 additions and 105 deletions

View File

@@ -42,7 +42,7 @@
"lodash": "^4.17.21",
"mkdirp": "^3.0.1",
"mv": "^2.1.1",
"parse-sng": "^3.1.1",
"parse-sng": "^3.1.2",
"rimraf": "^5.0.5",
"rxjs": "~7.8.1",
"sanitize-filename": "^1.6.3",

10
pnpm-lock.yaml generated
View File

@@ -57,8 +57,8 @@ dependencies:
specifier: ^2.1.1
version: 2.1.1
parse-sng:
specifier: ^3.1.1
version: 3.1.1
specifier: ^3.1.2
version: 3.1.2
rimraf:
specifier: ^5.0.5
version: 5.0.5
@@ -8370,8 +8370,8 @@ packages:
engines: {node: '>= 0.10'}
dev: true
/parse-sng@3.1.1:
resolution: {integrity: sha512-BOU9Y4LE5VsEc76lZonVXcbKrbGbetzdP28avnWvioNEhACzTEhtqDdtNmgsXiMO3lvkveRcM5wp6hEniSIQkA==}
/parse-sng@3.1.2:
resolution: {integrity: sha512-gAFsAJ6yaU2uH8UeSRC+EYFAN/urm/ICRJSLSwP1sqVBEF/VhN0XAxNNFTeLGsgdv1V2gx6pLA8sS9M6YX/FCA==}
dependencies:
binary-parser: 2.2.1
events: 3.3.0
@@ -9193,7 +9193,7 @@ packages:
lodash: 4.17.21
midievents: 2.0.0
midifile: 2.0.0
parse-sng: 3.1.1
parse-sng: 3.1.2
sanitize-filename: 1.6.3
sharp: 0.32.6
stream-audio-fingerprint: github.com/Geomitron/stream-audio-fingerprint/197e8a7ff60165b18bf1debc23dab4814996a20d

View File

@@ -60,7 +60,7 @@ export class SearchService {
this.search().subscribe()
}
get areMorePages() { return this.songsResponse.page && this.groupedSongs.length === this.songsResponse.page * resultsPerPage }
get areMorePages() { return this.songsResponse?.page && this.groupedSongs.length === this.songsResponse.page * resultsPerPage }
/**
* General search, uses the `/search?q=` endpoint.

View File

@@ -1,6 +1,6 @@
import { randomUUID } from 'crypto'
import EventEmitter from 'events'
import { createWriteStream, WriteStream } from 'fs'
import { createWriteStream } from 'fs'
import { access, constants } from 'fs/promises'
import { round, throttle } from 'lodash'
import { mkdirp } from 'mkdirp'
@@ -8,6 +8,8 @@ import mv from 'mv'
import { SngStream } from 'parse-sng'
import { join } from 'path'
import { rimraf } from 'rimraf'
import { Readable } from 'stream'
import { ReadableStream } from 'stream/web'
import { inspect } from 'util'
import { tempPath } from '../../../src-shared/Paths'
@@ -150,48 +152,20 @@ export class ChartDownload {
const fileSize = BigInt(sngResponse.headers.get('Content-Length')!)
if (this.isSng) {
const writeStream = createWriteStream(join(this.tempPath, this.destinationName))
const reader = sngResponse.body.getReader()
let downloadedByteCount = BigInt(0)
const sngStream = Readable.fromWeb(sngResponse.body as ReadableStream<Uint8Array>, { highWaterMark: 2e+9 })
// eslint-disable-next-line no-constant-condition
while (true) {
let result: ReadableStreamReadResult<Uint8Array>
try {
result = await reader.read()
} catch (err) {
throw { header: 'Failed to download the chart file', body: inspect(err) }
}
sngStream.pipe(createWriteStream(join(this.tempPath, this.destinationName), { highWaterMark: 2e+9 }))
if (this._canceled) {
await reader.cancel()
writeStream.end()
return
}
if (result.done) { writeStream.end(); return }
downloadedByteCount += BigInt(result.value.length)
const downloadPercent = round(100 * Number(downloadedByteCount / BigInt(1000)) / Number(fileSize / BigInt(1000)), 1)
this.showProgress(`Downloading... (${downloadPercent}%)`, downloadPercent)
await new Promise<void>((resolve, reject) => {
writeStream.write(result.value, err => {
if (err) {
reject({ header: 'Failed to download the chart file', body: inspect(err) })
} else {
resolve()
}
})
await new Promise<void>((resolve, reject) => {
let downloadedByteCount = BigInt(0)
sngStream.on('end', resolve)
sngStream.on('error', err => reject(err))
sngStream.on('data', data => {
downloadedByteCount += BigInt(data.length)
const downloadPercent = round(100 * Number(downloadedByteCount / BigInt(1000)) / Number(fileSize / BigInt(1000)), 1)
this.showProgress(`Downloading... (${downloadPercent}%)`, downloadPercent)
})
if (writeStream.writableNeedDrain) {
await new Promise<void>((resolve, reject) => {
writeStream.once('drain', resolve)
writeStream.once('error', err => reject({ header: 'Failed to download the chart file', body: inspect(err) }))
})
}
}
})
} else {
const sngStream = new SngStream(() => sngResponse.body!, { generateSongIni: true })
let downloadedByteCount = BigInt(0)
@@ -200,68 +174,19 @@ export class ChartDownload {
await new Promise<void>((resolve, reject) => {
sngStream.on('file', async (fileName, fileStream) => {
let writeStream: WriteStream
let reader: ReadableStreamDefaultReader<Uint8Array>
try {
writeStream = createWriteStream(join(this.tempPath, this.destinationName, fileName))
writeStream.on('error', () => { /** Surpress unhandled promise rejection */ })
reader = fileStream.getReader()
} catch (err) {
reject(err)
return
}
const nodeFileStream = Readable.fromWeb(fileStream as ReadableStream<Uint8Array>, { highWaterMark: 2e+9 })
nodeFileStream.pipe(createWriteStream(join(this.tempPath, this.destinationName, fileName), { highWaterMark: 2e+9 }))
try {
// eslint-disable-next-line no-constant-condition
while (true) {
let result: ReadableStreamReadResult<Uint8Array>
try {
result = await reader.read()
} catch (err) {
throw { header: 'Failed to download the chart file', body: inspect(err) }
}
if (this._canceled) {
await reader.cancel()
writeStream.end()
resolve()
return
}
if (result.done) { writeStream.end(); return }
downloadedByteCount += BigInt(result.value.length)
await new Promise<void>((resolve, reject) => {
nodeFileStream.on('end', resolve)
nodeFileStream.on('error', err => reject(err))
nodeFileStream.on('data', data => {
downloadedByteCount += BigInt(data.length)
const downloadPercent =
round(100 * Number(downloadedByteCount / BigInt(1000)) / Number(fileSize / BigInt(1000)), 1)
this.showProgress(`Downloading "${fileName}"... (${downloadPercent}%)`, downloadPercent)
await new Promise<void>((resolve, reject) => {
writeStream.write(result.value, err => {
if (err) {
reject({ header: 'Failed to download the chart file', body: inspect(err) })
} else {
resolve()
}
})
})
if (writeStream.writableNeedDrain) {
await new Promise<void>((resolve, reject) => {
writeStream.once('drain', resolve)
writeStream.once('error', err => reject({
header: 'Failed to download the chart file',
body: inspect(err),
}))
})
}
}
} catch (err) {
try {
await reader.cancel()
} catch (err) { /** ignore; error already reported */ }
writeStream.end()
reject(err)
}
})
})
})
sngStream.on('end', resolve)
sngStream.on('error', err => reject(err))