From 5e4094e065647de14a5fc5142323aeb33a2bbe3b Mon Sep 17 00:00:00 2001 From: Geovanni Pacheco Date: Fri, 4 Apr 2025 04:15:55 -0300 Subject: [PATCH 1/9] Add missing chart difficulties generation tools --- .vscode/extensions.json | 17 +- README.md | 3 +- package.json | 4 +- pnpm-lock.yaml | 15 + .../app/components/tools/tools.component.html | 149 +++-- .../app/components/tools/tools.component.ts | 53 +- .../app/core/services/settings.service.ts | 11 + src-electron/IpcHandler.ts | 2 + .../chartDifficultyGenerator.ts | 540 ++++++++++++++++++ .../generateDifficultiesHandler.ipc.ts | 164 ++++++ .../ipc/generate-difficulties/mid2chart.ts | 379 ++++++++++++ src-electron/preload.ts | 2 + src-shared/Settings.ts | 32 +- src-shared/interfaces/ipc.interface.ts | 2 + 14 files changed, 1287 insertions(+), 86 deletions(-) create mode 100644 src-electron/ipc/generate-difficulties/chartDifficultyGenerator.ts create mode 100644 src-electron/ipc/generate-difficulties/generateDifficultiesHandler.ipc.ts create mode 100644 src-electron/ipc/generate-difficulties/mid2chart.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 2268a32..51015ce 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,11 +1,10 @@ { - "recommendations": [ - "dbaeumer.vscode-eslint", - "mike-co.import-sorter", - "sibiraj-s.vscode-scss-formatter", - "hbenl.vscode-mocha-test-adapter", - "angular.ng-template", - "bradlc.vscode-tailwindcss", - "csstools.postcss" - ] + "recommendations": [ + "dbaeumer.vscode-eslint", + "mike-co.import-sorter", + "hbenl.vscode-mocha-test-adapter", + "angular.ng-template", + "bradlc.vscode-tailwindcss", + "csstools.postcss" + ] } diff --git a/README.md b/README.md index d80cd97..60d9f7b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
-**Bridge** is a desktop application that allows you to search for and download charts that can be played in games like Clone Hero, YARG, etc... +**Bridge** is a desktop application that allows you to search for and download charts that can be played in games like Clone Hero, YARG, etc... This is the desktop version of [Chorus Encore](https://www.enchor.us/). @@ -23,6 +23,7 @@ Head over to the [Releases](https://github.com/Geomitron/Bridge/releases) page t - ✅ A variety of themes. - ✅ Advanced song search. - ✅ Chart issue scanner (for people making charts). +- ✅ Auto generate difficulties for songs. (Experimental) ### What's new in v3.4.0 diff --git a/package.json b/package.json index e3effea..623cec4 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "scripts": { "start": "concurrently \"npm run start:angular\" \"npm run start:electron\" -n angular,electron -c red,yellow", "start:angular": "ng serve", - "start:electron": "nodemon --exec \"tsc -p src-electron/tsconfig.electron.json && node src-electron/rename-to-mjs.js && electron ./dist/electron/src-electron/main.js --dev\" --watch src-electron/ -e ts", + "start:electron": "nodemon --exec \"tsc -p src-electron/tsconfig.electron.json && node src-electron/rename-to-mjs.js && electron ./dist/electron/src-electron/main.js --no-sandbox --dev\" --watch src-electron/ -e ts", "build:windows": "ng build -c production && tsc -p src-electron/tsconfig.electron.json && node src-electron/rename-to-mjs.js && electron-builder build --windows", "build:mac": "ng build -c production && tsc -p src-electron/tsconfig.electron.json && node src-electron/rename-to-mjs.js && electron-builder build --mac", "build:linux": "ng build -c production && tsc -p src-electron/tsconfig.electron.json && node src-electron/rename-to-mjs.js && electron-builder build --linux", @@ -45,6 +45,7 @@ "exceljs": "^4.4.0", "fs-extra": "11.2.0", "lodash": "4.17.21", + "midi-file": "^1.2.4", "parse-sng": "4.0.3", "rxjs": "7.8.1", "sanitize-filename": "1.6.3", @@ -92,6 +93,7 @@ }, "pnpm": { "onlyBuiltDependencies": [ + "@parcel/watcher", "electron", "esbuild", "exifreader", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f2e677..91d4bff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,9 +65,15 @@ importers: fs-extra: specifier: 11.2.0 version: 11.2.0 + herochartio: + specifier: ^1.1.0 + version: 1.1.0 lodash: specifier: 4.17.21 version: 4.17.21 + midi-file: + specifier: ^1.2.4 + version: 1.2.4 parse-sng: specifier: 4.0.3 version: 4.0.3 @@ -2783,6 +2789,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + herochartio@1.1.0: + resolution: {integrity: sha512-Dcq6T/+FpBYeVcUCbCuV6mZBmDyRhBmsI7ec216fPU/p4AEJCNWlT+dlXJVzxlFSTAkqzU9aqbbYx//S+ok9DA==} + hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} @@ -7373,6 +7382,12 @@ snapshots: dependencies: function-bind: 1.1.2 + herochartio@1.1.0: + dependencies: + fs-extra: 8.1.0 + lodash: 4.17.21 + midi-file: 1.2.4 + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 diff --git a/src-angular/app/components/tools/tools.component.html b/src-angular/app/components/tools/tools.component.html index d3f3509..a7f7c50 100644 --- a/src-angular/app/components/tools/tools.component.html +++ b/src-angular/app/components/tools/tools.component.html @@ -1,62 +1,97 @@ -
-
Chart issue scanning
-
-
+
+
+ Chart Missing Difficulties (experimental) +
+
+ +
+
+
@@ -160,7 +172,8 @@
@@ -172,12 +185,14 @@
@@ -207,7 +222,8 @@
-
- Github - Discord - - - - - Patreon - -
- -
-
- - - +
+ -
- +
+
+ + + +
+ +
+ +
diff --git a/src-angular/app/components/settings/settings.component.ts b/src-angular/app/components/settings/settings.component.ts index 447b2c7..08f50bf 100644 --- a/src-angular/app/components/settings/settings.component.ts +++ b/src-angular/app/components/settings/settings.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core' +import { ChangeDetectorRef, Component, ElementRef, HostBinding, OnInit, ViewChild } from '@angular/core' import { FormControl } from '@angular/forms' import _ from 'lodash' @@ -11,12 +11,15 @@ import { themes } from 'src-shared/Settings' standalone: false, }) export class SettingsComponent implements OnInit { + @HostBinding('class.contents') contents = true + @ViewChild('themeDropdown', { static: true }) themeDropdown: ElementRef public chartFolderName: FormControl public isSng: FormControl public downloadVideos: FormControl public isCompactTable: FormControl + public generateMissingDifficulties: FormControl public artistColumn: FormControl public albumColumn: FormControl @@ -49,6 +52,8 @@ export class SettingsComponent implements OnInit { this.isSng.valueChanges.subscribe(value => settingsService.isSng = value) this.downloadVideos = new FormControl(ss.downloadVideos, { nonNullable: true }) this.downloadVideos.valueChanges.subscribe(value => settingsService.downloadVideos = value) + this.generateMissingDifficulties = new FormControl(ss.generateMissingDifficulties, { nonNullable: true }) + this.generateMissingDifficulties.valueChanges.subscribe(value => settingsService.generateMissingDifficulties = value) this.isCompactTable = new FormControl(settingsService.isCompactTable, { nonNullable: true }) this.isCompactTable.valueChanges.subscribe(value => ss.isCompactTable = value) diff --git a/src-angular/app/components/tools/tools.component.html b/src-angular/app/components/tools/tools.component.html index a7f7c50..69a5dea 100644 --- a/src-angular/app/components/tools/tools.component.html +++ b/src-angular/app/components/tools/tools.component.html @@ -73,7 +73,7 @@

Error scanning charts for issues:

library
-
diff --git a/src-angular/app/components/browse/search-bar/search-bar.component.ts b/src-angular/app/components/browse/search-bar/search-bar.component.ts index ab62531..18492c5 100644 --- a/src-angular/app/components/browse/search-bar/search-bar.component.ts +++ b/src-angular/app/components/browse/search-bar/search-bar.component.ts @@ -5,6 +5,7 @@ import dayjs from 'dayjs' import { distinctUntilChanged, switchMap, throttleTime } from 'rxjs' import { Difficulty, Instrument } from 'scan-chart' import { SearchService } from 'src-angular/app/core/services/search.service' +import { SettingsService } from 'src-angular/app/core/services/settings.service' import { difficulties, difficultyDisplay, drumsReviewedDisplay, drumTypeDisplay, DrumTypeName, drumTypeNames, instrumentDisplay, instruments } from 'src-shared/UtilFunctions' @Component({ @@ -38,6 +39,7 @@ export class SearchBarComponent implements OnInit, AfterViewInit { constructor( private searchService: SearchService, private fb: FormBuilder, + public settingsService: SettingsService, ) { } ngOnInit() { diff --git a/src-electron/ipc/GenerateMissingDifficultiesHandler.ipc.ts b/src-electron/ipc/GenerateMissingDifficultiesHandler.ipc.ts index 86c8b68..b2845b0 100644 --- a/src-electron/ipc/GenerateMissingDifficultiesHandler.ipc.ts +++ b/src-electron/ipc/GenerateMissingDifficultiesHandler.ipc.ts @@ -23,90 +23,68 @@ export async function generateMissingDifficulties() { try { const chartFolders = await getChartFolders(settings.chartsDifficultyGenerationPath) const limiter = new Bottleneck({ maxConcurrent: 20 }) // Ensures memory use stays bounded + let chartsWithMissingDifficulties = 0 - const charts: { chart: ParsedChart; path: string }[] = [] - for (const chartFolder of chartFolders) { - limiter.schedule(async () => { - const isSng = chartFolder.files.length === 1 && hasSngExtension(chartFolder.files[0]) + await limiter.schedule(async () => { + return Promise.all(chartFolders.map(async chartFolderPath => { + const missingDifficulties = await getChartMissingDifficulties({ chartFolderPath }) - if (isSng) { - console.info('SNG files are not supported yet.', chartFolder) + if (missingDifficulties.size === 0) { return } - const files = await getChartFilesFromFolder(chartFolder) + chartsWithMissingDifficulties++ - if (files.length === 0) { - return - } - - if (files.length > 1) { - console.info('Multiple charts found in folder.', chartFolder) - return - } - - const file = files[0] - - const result: { chart: ParsedChart; path: string } = { - chart: parseChartFile(file.data, file.fileName.endsWith('.chart') ? 'chart' : 'mid'), - path: chartFolder.path, + for (const [instrument, difficulties] of missingDifficulties) { + for (const difficulty of difficulties) { + generateDifficulty({ + action: 'add', + chartFolderPath, + instrument, + difficulty, + }) + } } - charts.push(result) - emitIpcEvent('updateChartsDifficultyGeneration', { status: 'progress', message: `${charts.length}/${chartFolders.length} scanned...` }) - }) - } + })) + }) - await new Promise((resolve, reject) => { - limiter.on('error', err => { - reject(err) - limiter.stop() - }) - - limiter.on('idle', async () => { - let chartsWithMissingDifficulties = 0 + emitIpcEvent('updateChartsDifficultyGeneration', { + status: 'done', + message: `${chartsWithMissingDifficulties} charts with missing difficulties added to queue.`, + }) + } catch (err) { + emitIpcEvent('updateChartsDifficultyGeneration', { status: 'error', message: inspect(err) }) + } +} - for (const { chart, path: chartPath } of charts) { +export async function getChartMissingDifficulties({ chartFolderPath }: { chartFolderPath: string }) { + const files = await getChartFilesFromFolder({ chartFolderPath }) - const missingDifficulties = getChartMissingDifficultiesByInstrument({ chart }) + if (files.length === 0) { + throw new Error('No chart files found in folder.') + } - if (missingDifficulties.size === 0) { - continue - } + if (files.length > 1) { + throw new Error('Multiple charts found in folder.') + } - chartsWithMissingDifficulties++ - - for (const [instrument, difficulties] of missingDifficulties) { - for (const difficulty of difficulties) { - generateDifficulty({ - action: 'add', - chartFolderPath: chartPath, - instrument, - difficulty, - }) - } - } + const file = files[0] - } + if (hasSngExtension(file.fileName)) { + throw new Error('SNG files are not supported yet.') + } - emitIpcEvent('updateChartsDifficultyGeneration', { - status: 'done', - message: `${chartsWithMissingDifficulties} charts with missing difficulties added to queue.`, - }) - resolve() - }) - }) + const chart = parseChartFile(file.data, file.fileName.endsWith('.chart') ? 'chart' : 'mid') - } catch (err) { - emitIpcEvent('updateChartsDifficultyGeneration', { status: 'error', message: inspect(err) }) - } + return getChartMissingDifficultiesByInstrument({ chart }) } /** * @returns valid chart folders in `path` and all its subdirectories. */ async function getChartFolders(path: string) { - const chartFolders: { path: string; files: string[] }[] = [] + const chartFolders: string[] = [] const entries = await readdir(path, { withFileTypes: true }) @@ -117,29 +95,24 @@ async function getChartFolders(path: string) { chartFolders.push(..._.flatMap(await Promise.all(subfolders))) - const sngFiles = entries.filter(entry => !entry.isDirectory() && hasSngExtension(entry.name)) - chartFolders.push(...sngFiles.map(sf => ({ path, files: [sf.name] }))) - if ( subfolders.length === 0 && // Charts won't contain other charts appearsToBeChartFolder(entries.map(entry => getExtension(entry.name))) ) { - chartFolders.push({ - path, - files: entries.filter(entry => !entry.isDirectory()).map(entry => entry.name), - }) + chartFolders.push(path) emitIpcEvent('updateChartsDifficultyGeneration', { status: 'progress', message: `${chartFolders.length} charts found...` }) } return chartFolders } -async function getChartFilesFromFolder(chartFolder: { path: string; files: string[] }): Promise<{ fileName: string; data: Uint8Array }[]> { +async function getChartFilesFromFolder({ chartFolderPath }: { chartFolderPath: string }): Promise<{ fileName: string; data: Uint8Array }[]> { const files: { fileName: string; data: Uint8Array }[] = [] - for (const fileName of chartFolder.files) { + const chartFolderFiles = await readdir(chartFolderPath) + for (const fileName of chartFolderFiles) { if (hasChartExtension(fileName)) { - files.push({ fileName, data: await readFile(chartFolder.path + '/' + fileName) }) + files.push({ fileName, data: await readFile(chartFolderPath + '/' + fileName) }) } } diff --git a/src-electron/ipc/download/ChartDownload.ts b/src-electron/ipc/download/ChartDownload.ts index 6540406..ed6a8c0 100644 --- a/src-electron/ipc/download/ChartDownload.ts +++ b/src-electron/ipc/download/ChartDownload.ts @@ -14,6 +14,8 @@ import { inspect } from 'util' import { tempPath } from '../../../src-shared/Paths.js' import { resolveChartFolderName } from '../../../src-shared/UtilFunctions.js' +import { generateDifficulty } from '../GenerateDifficultyHandler.ipc.js' +import { getChartMissingDifficulties } from '../GenerateMissingDifficultiesHandler.ipc.js' import { getSettings } from '../SettingsHandler.ipc.js' export interface DownloadMessage { @@ -91,6 +93,7 @@ export class ChartDownload { case 0: await this.checkFilesystem(); this.stepCompletedCount++; if (this._canceled) { return } // break omitted case 1: await this.downloadChart(); this.stepCompletedCount++; if (this._canceled) { return } // break omitted case 2: await this.transferChart(); this.stepCompletedCount++; if (this._canceled) { return } // break omitted + case 3: await this.generateDifficulties(); this.stepCompletedCount++; if (this._canceled) { return } // break omitted } } catch (err) { this.showProgress.cancel() @@ -240,8 +243,44 @@ export class ChartDownload { throw { header: 'Failed to delete temporary folder', body: inspect(err) } } - this.showProgress.cancel() - this.eventEmitter.emit('end', join(settings.libraryPath!, this.chartFolderPath)) + if (!settings.generateMissingDifficulties || this.isSng) { + this.showProgress.cancel() + this.eventEmitter.emit('end', join(settings.libraryPath!, this.chartFolderPath)) + } + } + + private async generateDifficulties() { + const settings = await getSettings() + if (this._canceled) { return } + + if (settings.generateMissingDifficulties && !this.isSng) { + this.showProgress('Generating difficulties...') + + try { + const chartFolderPath = join(settings.libraryPath!, this.chartFolderPath) + const missingDifficulties = await getChartMissingDifficulties({ chartFolderPath }) + + if (missingDifficulties.size === 0) { + return + } + + for (const [instrument, difficulties] of missingDifficulties) { + for (const difficulty of difficulties) { + generateDifficulty({ + action: 'add', + chartFolderPath, + instrument, + difficulty, + }) + } + } + + this.showProgress.cancel() + this.eventEmitter.emit('end', join(settings.libraryPath!, this.chartFolderPath)) + } catch (err) { + throw { header: 'Failed to generate difficulties', body: inspect(err) } + } + } } } diff --git a/src-electron/ipc/generate-difficulty/GenerateDifficulty.ts b/src-electron/ipc/generate-difficulty/GenerateDifficulty.ts index 6b382bc..7a63053 100644 --- a/src-electron/ipc/generate-difficulty/GenerateDifficulty.ts +++ b/src-electron/ipc/generate-difficulty/GenerateDifficulty.ts @@ -170,8 +170,10 @@ export class GenerateDifficulty { private async hasChartBackup() { const chartFileBasename = getBasename(this.chartFilePath) - return exists(chartFileBasename + '.mid' + CHART_BACKUP_EXTENSION) - || exists(chartFileBasename + '.chart' + CHART_BACKUP_EXTENSION) + const midBackupExists = await exists(chartFileBasename + '.mid' + CHART_BACKUP_EXTENSION) + const chartBackupExists = await exists(chartFileBasename + '.chart' + CHART_BACKUP_EXTENSION) + + return midBackupExists || chartBackupExists } private async createChartBackup() { @@ -197,6 +199,8 @@ export class GenerateDifficulty { const outputPath = join(this.chartFolderPath, 'notes.chart') await writeFile(outputPath, this.chartContent) + await new Promise(resolve => setTimeout(resolve, 1000)) + this.showProgress.cancel() this.eventEmitter.emit('end', outputPath) } diff --git a/src-electron/ipc/generate-difficulty/chartDifficultyGenerator.ts b/src-electron/ipc/generate-difficulty/chartDifficultyGenerator.ts index 19b36e1..9a1c085 100644 --- a/src-electron/ipc/generate-difficulty/chartDifficultyGenerator.ts +++ b/src-electron/ipc/generate-difficulty/chartDifficultyGenerator.ts @@ -391,9 +391,9 @@ function serializeSections({ sections }: { sections: { [key: string]: string[] } return [ `[${key}]`, '{', - ...lines, + ...lines.map(line => ` ${line}`), '}', - ] + ].join('\n') }).join('\n') } From bd8b2f18f7db0212c0fd0834c99f4c93449e9747 Mon Sep 17 00:00:00 2001 From: Geovanni Pacheco Date: Sat, 5 Apr 2025 21:23:19 -0300 Subject: [PATCH 6/9] Fix html files format --- .eslintrc.json | 10 +- .prettierrc | 9 + .vscode/settings.json | 2 +- package.json | 3 +- .../search-bar/search-bar.component.html | 300 ++++++++++-------- .../settings/settings.component.html | 111 +++---- .../app/components/tools/tools.component.html | 40 ++- 7 files changed, 259 insertions(+), 216 deletions(-) create mode 100644 .prettierrc diff --git a/.eslintrc.json b/.eslintrc.json index deb72d6..bc41be2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -90,15 +90,7 @@ "plugins": ["prettier"], "extends": ["plugin:prettier/recommended"], "rules": { - "prettier/prettier": ["error", { - "parser": "angular", - "endOfLine": "auto", - "printWidth": 150, - "useTabs": true, - "singleQuote": true, - "htmlWhitespaceSensitivity": "css", - "bracketSameLine": true - }] + "prettier/prettier": ["error"] } }, { diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..3659c7d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "parser": "angular", + "endOfLine": "auto", + "printWidth": 150, + "useTabs": true, + "singleQuote": true, + "htmlWhitespaceSensitivity": "css", + "bracketSameLine": true +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 218cd5e..9928278 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,7 +22,7 @@ "files.trimTrailingWhitespace": false }, "[html]": { - "editor.defaultFormatter": "vscode.html-language-features", + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" } diff --git a/package.json b/package.json index 623cec4..1eedf2b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "build:windows": "ng build -c production && tsc -p src-electron/tsconfig.electron.json && node src-electron/rename-to-mjs.js && electron-builder build --windows", "build:mac": "ng build -c production && tsc -p src-electron/tsconfig.electron.json && node src-electron/rename-to-mjs.js && electron-builder build --mac", "build:linux": "ng build -c production && tsc -p src-electron/tsconfig.electron.json && node src-electron/rename-to-mjs.js && electron-builder build --linux", - "release": "ng build -c production && tsc -p src-electron/tsconfig.electron.json && node src-electron/rename-to-mjs.js && electron-builder build" + "release": "ng build -c production && tsc -p src-electron/tsconfig.electron.json && node src-electron/rename-to-mjs.js && electron-builder build", + "lint": "eslint . --ext .ts,.html" }, "dependencies": { "@angular/animations": "19.2.1", diff --git a/src-angular/app/components/browse/search-bar/search-bar.component.html b/src-angular/app/components/browse/search-bar/search-bar.component.html index fc711f2..85b5078 100644 --- a/src-angular/app/components/browse/search-bar/search-bar.component.html +++ b/src-angular/app/components/browse/search-bar/search-bar.component.html @@ -1,16 +1,16 @@ -
@@ -245,15 +239,14 @@
- Github - Discord + Github + Discord - - + Patreon @@ -262,11 +255,9 @@
- diff --git a/src-angular/app/components/tools/tools.component.html b/src-angular/app/components/tools/tools.component.html index 69a5dea..4aeaa46 100644 --- a/src-angular/app/components/tools/tools.component.html +++ b/src-angular/app/components/tools/tools.component.html @@ -7,11 +7,14 @@ Issue scan directory
- @if (settingsService.issueScanDirectory !== undefined) { - + }
@@ -21,16 +24,22 @@ Spreadsheet output directory
- @if (settingsService.spreadsheetOutputDirectory !== undefined) { - + }
-
- @if (settingsService.chartsDifficultyGenerationDirectory !== undefined) { - + } - +
-