mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-11 14:19:38 +00:00
Various refactoring
This commit is contained in:
@@ -1,19 +1,23 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule, RouteReuseStrategy } from '@angular/router'
|
||||||
import { BrowseComponent } from './components/browse/browse.component'
|
import { BrowseComponent } from './components/browse/browse.component'
|
||||||
import { SettingsComponent } from './components/settings/settings.component'
|
import { SettingsComponent } from './components/settings/settings.component'
|
||||||
|
import { TabPersistStrategy } from './core/tab-persist.strategy'
|
||||||
|
|
||||||
|
// TODO: replace these with the correct components
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: 'browse', component: BrowseComponent },
|
{ path: 'browse', component: BrowseComponent, data: { shouldReuse: true } },
|
||||||
{ path: 'library', component: BrowseComponent },
|
{ path: 'library', redirectTo: '/browse' },
|
||||||
{ path: 'settings', component: SettingsComponent },
|
{ path: 'settings', component: SettingsComponent, data: { shouldReuse: true } },
|
||||||
{ path: 'about', component: BrowseComponent }, // TODO: replace these with the correct components
|
{ path: 'about', redirectTo: '/browse' },
|
||||||
{ path: '**', redirectTo: '/browse' }
|
{ path: '**', redirectTo: '/browse' }
|
||||||
]
|
]
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes)],
|
imports: [RouterModule.forRoot(routes)],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: RouteReuseStrategy, useClass: TabPersistStrategy },
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class AppRoutingModule { }
|
export class AppRoutingModule { }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { SettingsService } from './core/services/settings.service'
|
import { SettingsService } from './core/services/settings.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -6,12 +6,7 @@ import { SettingsService } from './core/services/settings.service'
|
|||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styles: []
|
styles: []
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent {
|
||||||
|
|
||||||
constructor(private settingsService: SettingsService) { }
|
constructor(private settingsService: SettingsService) { }
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
// Load settings
|
|
||||||
this.settingsService.getSettings()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,15 +4,15 @@ import { NgModule } from '@angular/core'
|
|||||||
import { AppRoutingModule } from './app-routing.module'
|
import { AppRoutingModule } from './app-routing.module'
|
||||||
import { AppComponent } from './app.component'
|
import { AppComponent } from './app.component'
|
||||||
import { ToolbarComponent } from './components/toolbar/toolbar.component'
|
import { ToolbarComponent } from './components/toolbar/toolbar.component'
|
||||||
import { BrowseComponent } from './components/browse/browse.component';
|
import { BrowseComponent } from './components/browse/browse.component'
|
||||||
import { SearchBarComponent } from './components/browse/search-bar/search-bar.component';
|
import { SearchBarComponent } from './components/browse/search-bar/search-bar.component'
|
||||||
import { StatusBarComponent } from './components/browse/status-bar/status-bar.component';
|
import { StatusBarComponent } from './components/browse/status-bar/status-bar.component'
|
||||||
import { ResultTableComponent } from './components/browse/result-table/result-table.component';
|
import { ResultTableComponent } from './components/browse/result-table/result-table.component'
|
||||||
import { ChartSidebarComponent } from './components/browse/chart-sidebar/chart-sidebar.component';
|
import { ChartSidebarComponent } from './components/browse/chart-sidebar/chart-sidebar.component'
|
||||||
import { ResultTableRowComponent } from './components/browse/result-table/result-table-row/result-table-row.component';
|
import { ResultTableRowComponent } from './components/browse/result-table/result-table-row/result-table-row.component'
|
||||||
import { DownloadsModalComponent } from './components/browse/status-bar/downloads-modal/downloads-modal.component';
|
import { DownloadsModalComponent } from './components/browse/status-bar/downloads-modal/downloads-modal.component'
|
||||||
import { ProgressBarDirective } from './core/directives/progress-bar.directive';
|
import { ProgressBarDirective } from './core/directives/progress-bar.directive'
|
||||||
import { CheckboxDirective } from './core/directives/checkbox.directive';
|
import { CheckboxDirective } from './core/directives/checkbox.directive'
|
||||||
import { SettingsComponent } from './components/settings/settings.component'
|
import { SettingsComponent } from './components/settings/settings.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -34,7 +34,6 @@ import { SettingsComponent } from './components/settings/settings.component'
|
|||||||
BrowserModule,
|
BrowserModule,
|
||||||
AppRoutingModule
|
AppRoutingModule
|
||||||
],
|
],
|
||||||
providers: [],
|
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
@@ -20,7 +20,7 @@ export class BrowseComponent {
|
|||||||
onResultsUpdated(results: SongResult[]) {
|
onResultsUpdated(results: SongResult[]) {
|
||||||
this.resultTable.results = results
|
this.resultTable.results = results
|
||||||
this.resultTable.onNewSearch()
|
this.resultTable.onNewSearch()
|
||||||
this.resultTable.checkAll
|
this.resultTable.checkAll(false)
|
||||||
this.chartSidebar.selectedVersion = undefined
|
this.chartSidebar.selectedVersion = undefined
|
||||||
this.statusBar.resultCount = results.length
|
this.statusBar.resultCount = results.length
|
||||||
this.statusBar.selectedResults = []
|
this.statusBar.selectedResults = []
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ export class ChartSidebarComponent {
|
|||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
}
|
}
|
||||||
let seconds = Math.round(this.selectedVersion.song_length / 1000)
|
let seconds = Math.round(this.selectedVersion.song_length / 1000)
|
||||||
if (seconds < 60) { return `${seconds} second${seconds == 1 ? '' : 's'}` }
|
if (seconds < 60) {
|
||||||
|
return `${seconds} second${seconds == 1 ? '' : 's'}`
|
||||||
|
}
|
||||||
let minutes = Math.floor(seconds / 60)
|
let minutes = Math.floor(seconds / 60)
|
||||||
let hours = 0
|
let hours = 0
|
||||||
while (minutes > 59) {
|
while (minutes > 59) {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export class SettingsComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async openLibraryDirectory() {
|
openLibraryDirectory() {
|
||||||
this.electronService.openFolder(this.settingsService.libraryDirectory)
|
this.electronService.openFolder(this.settingsService.libraryDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class CheckboxDirective implements AfterViewInit {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async check(isChecked: boolean) {
|
check(isChecked: boolean) {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
$(this.checkbox.nativeElement).checkbox('check')
|
$(this.checkbox.nativeElement).checkbox('check')
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class DownloadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addDownload(versionID: number, newDownload: NewDownload) {
|
addDownload(versionID: number, newDownload: NewDownload) {
|
||||||
if (this.downloads.findIndex(download => download.versionID == versionID) != -1) { return } // Don't download something twice
|
if (this.downloads.findIndex(download => download.versionID == versionID) == -1) { // Don't download something twice
|
||||||
this.electronService.receiveIPC('download-updated', result => {
|
this.electronService.receiveIPC('download-updated', result => {
|
||||||
this.downloadUpdatedEmitter.emit(result)
|
this.downloadUpdatedEmitter.emit(result)
|
||||||
|
|
||||||
@@ -40,6 +40,7 @@ export class DownloadService {
|
|||||||
})
|
})
|
||||||
this.electronService.sendIPC('download', { action: 'add', versionID, data: newDownload })
|
this.electronService.sendIPC('download', { action: 'add', versionID, data: newDownload })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onDownloadUpdated(callback: (download: DownloadProgress) => void) {
|
onDownloadUpdated(callback: (download: DownloadProgress) => void) {
|
||||||
const debouncedCallback = _.throttle(callback, 30)
|
const debouncedCallback = _.throttle(callback, 30)
|
||||||
|
|||||||
@@ -11,21 +11,25 @@ export class SettingsService {
|
|||||||
private settings: Settings
|
private settings: Settings
|
||||||
private currentThemeLink: HTMLLinkElement
|
private currentThemeLink: HTMLLinkElement
|
||||||
|
|
||||||
constructor(private electronService: ElectronService) { }
|
constructor(private electronService: ElectronService) {
|
||||||
|
this.getSettings() // Should resolve immediately because GetSettingsHandler returns a value, not a promise
|
||||||
|
console.log(`QUICKLY RESOLVED SETTINGS: ${this.settings}`)
|
||||||
|
}
|
||||||
|
|
||||||
async getSettings() {
|
async getSettings() {
|
||||||
if (this.settings == undefined) {
|
if (this.settings == undefined) {
|
||||||
this.settings = await this.electronService.invoke('init-settings', undefined)
|
this.settings = await this.electronService.invoke('get-settings', undefined)
|
||||||
}
|
}
|
||||||
return this.settings
|
return this.settings
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings() {
|
saveSettings() {
|
||||||
if (this.settings != undefined) {
|
if (this.settings != undefined) {
|
||||||
this.electronService.sendIPC('update-settings', this.settings)
|
this.electronService.sendIPC('set-settings', this.settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: research how to make theme changes with fomantic UI
|
||||||
changeTheme(theme: string) {
|
changeTheme(theme: string) {
|
||||||
if (this.currentThemeLink != undefined) this.currentThemeLink.remove()
|
if (this.currentThemeLink != undefined) this.currentThemeLink.remove()
|
||||||
if (theme == 'Default') { return }
|
if (theme == 'Default') { return }
|
||||||
@@ -46,6 +50,8 @@ export class SettingsService {
|
|||||||
return this.electronService.defaultSession.clearCache()
|
return this.electronService.defaultSession.clearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Individual getters/setters
|
||||||
|
// TODO: remove the undefined checks if the constructor gets the settings every time
|
||||||
get libraryDirectory() {
|
get libraryDirectory() {
|
||||||
return this.settings == undefined ? '' : this.settings.libraryPath
|
return this.settings == undefined ? '' : this.settings.libraryPath
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/app/core/tab-persist.strategy.ts
Normal file
27
src/app/core/tab-persist.strategy.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This makes each route with the 'reuse' data flag persist when not in focus.
|
||||||
|
*/
|
||||||
|
export class TabPersistStrategy extends RouteReuseStrategy {
|
||||||
|
private handles: { [path: string]: DetachedRouteHandle } = {}
|
||||||
|
|
||||||
|
shouldDetach(route: ActivatedRouteSnapshot) {
|
||||||
|
return route.data.shouldReuse || false
|
||||||
|
}
|
||||||
|
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle) {
|
||||||
|
if (route.data.shouldReuse) {
|
||||||
|
this.handles[route.routeConfig.path] = handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shouldAttach(route: ActivatedRouteSnapshot) {
|
||||||
|
return !!route.routeConfig && !!this.handles[route.routeConfig.path]
|
||||||
|
}
|
||||||
|
retrieve(route: ActivatedRouteSnapshot) {
|
||||||
|
if (!route.routeConfig) return null
|
||||||
|
return this.handles[route.routeConfig.path]
|
||||||
|
}
|
||||||
|
shouldReuseRoute(future: ActivatedRouteSnapshot, _curr: ActivatedRouteSnapshot) {
|
||||||
|
return future.data.shouldReuse || false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,24 @@ import { IPCInvokeHandler } from '../shared/IPCHandler'
|
|||||||
import Database from '../shared/Database'
|
import Database from '../shared/Database'
|
||||||
import { AlbumArtResult } from '../shared/interfaces/songDetails.interface'
|
import { AlbumArtResult } from '../shared/interfaces/songDetails.interface'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the 'album-art' event.
|
||||||
|
*/
|
||||||
export default class AlbumArtHandler implements IPCInvokeHandler<'album-art'> {
|
export default class AlbumArtHandler implements IPCInvokeHandler<'album-art'> {
|
||||||
event: 'album-art' = 'album-art'
|
event: 'album-art' = 'album-art'
|
||||||
// TODO: add method documentation
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns an `AlbumArtResult` object containing the album art for the song with `songID`.
|
||||||
|
*/
|
||||||
async handler(songID: number) {
|
async handler(songID: number) {
|
||||||
const db = await Database.getInstance()
|
const db = await Database.getInstance()
|
||||||
|
|
||||||
return db.sendQuery(this.getAlbumArtQuery(songID), 1) as Promise<AlbumArtResult>
|
return db.sendQuery(this.getAlbumArtQuery(songID), 1) as Promise<AlbumArtResult>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a database query that returns the album art for the song with `songID`.
|
||||||
|
*/
|
||||||
private getAlbumArtQuery(songID: number) {
|
private getAlbumArtQuery(songID: number) {
|
||||||
return `
|
return `
|
||||||
SELECT art
|
SELECT art
|
||||||
|
|||||||
@@ -2,16 +2,24 @@ import { IPCInvokeHandler } from '../shared/IPCHandler'
|
|||||||
import Database from '../shared/Database'
|
import Database from '../shared/Database'
|
||||||
import { VersionResult } from '../shared/interfaces/songDetails.interface'
|
import { VersionResult } from '../shared/interfaces/songDetails.interface'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the 'batch-song-details' event.
|
||||||
|
*/
|
||||||
export default class BatchSongDetailsHandler implements IPCInvokeHandler<'batch-song-details'> {
|
export default class BatchSongDetailsHandler implements IPCInvokeHandler<'batch-song-details'> {
|
||||||
event: 'batch-song-details' = 'batch-song-details'
|
event: 'batch-song-details' = 'batch-song-details'
|
||||||
// TODO: add method documentation
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns an array of all the chart versions with a songID found in `songIDs`.
|
||||||
|
*/
|
||||||
async handler(songIDs: number[]) {
|
async handler(songIDs: number[]) {
|
||||||
const db = await Database.getInstance()
|
const db = await Database.getInstance()
|
||||||
|
|
||||||
return db.sendQuery(this.getVersionQuery(songIDs)) as Promise<VersionResult[]>
|
return db.sendQuery(this.getVersionQuery(songIDs)) as Promise<VersionResult[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a database query that returns all the chart versions with a songID found in `songIDs`.
|
||||||
|
*/
|
||||||
private getVersionQuery(songIDs: number[]) {
|
private getVersionQuery(songIDs: number[]) {
|
||||||
return `
|
return `
|
||||||
SELECT *
|
SELECT *
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { exists as _exists, mkdir as _mkdir, readFile as _readFile } from 'fs'
|
|
||||||
import { dataPath, tempPath, themesPath, settingsPath } from '../shared/Paths'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import { IPCInvokeHandler } from '../shared/IPCHandler'
|
|
||||||
import { defaultSettings, Settings } from '../shared/Settings'
|
|
||||||
import SaveSettingsHandler from './SaveSettingsHandler.ipc'
|
|
||||||
|
|
||||||
const exists = promisify(_exists)
|
|
||||||
const mkdir = promisify(_mkdir)
|
|
||||||
const readFile = promisify(_readFile)
|
|
||||||
|
|
||||||
export default class InitSettingsHandler implements IPCInvokeHandler<'init-settings'> {
|
|
||||||
event: 'init-settings' = 'init-settings'
|
|
||||||
|
|
||||||
private static settings: Settings
|
|
||||||
static async getSettings() {
|
|
||||||
if (this.settings == undefined) {
|
|
||||||
this.settings = await InitSettingsHandler.initSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.settings
|
|
||||||
}
|
|
||||||
|
|
||||||
async handler() {
|
|
||||||
return InitSettingsHandler.getSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async initSettings(): Promise<Settings> {
|
|
||||||
try {
|
|
||||||
// Create data directories if they don't exists
|
|
||||||
for (const path of [dataPath, tempPath, themesPath]) {
|
|
||||||
if (!await exists(path)) {
|
|
||||||
await mkdir(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read/create settings
|
|
||||||
if (await exists(settingsPath)) {
|
|
||||||
return JSON.parse(await readFile(settingsPath, 'utf8'))
|
|
||||||
} else {
|
|
||||||
await SaveSettingsHandler.saveSettings(defaultSettings)
|
|
||||||
return defaultSettings
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to initialize settings!')
|
|
||||||
console.error('Several actions (including downloading) will unexpectedly fail')
|
|
||||||
console.error(e)
|
|
||||||
return defaultSettings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { writeFile as _writeFile } from 'fs'
|
|
||||||
import { IPCEmitHandler } from '../shared/IPCHandler'
|
|
||||||
import { Settings } from '../shared/Settings'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import { settingsPath } from '../shared/Paths'
|
|
||||||
|
|
||||||
const writeFile = promisify(_writeFile)
|
|
||||||
|
|
||||||
export default class SaveSettingsHandler implements IPCEmitHandler<'update-settings'> {
|
|
||||||
event: 'update-settings' = 'update-settings'
|
|
||||||
|
|
||||||
handler(settings: Settings) {
|
|
||||||
SaveSettingsHandler.saveSettings(settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
static async saveSettings(settings: Settings) {
|
|
||||||
const settingsJSON = JSON.stringify(settings, undefined, 2)
|
|
||||||
await writeFile(settingsPath, settingsJSON, 'utf8')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,16 +3,24 @@ import Database from '../shared/Database'
|
|||||||
import { SongSearch, SearchType, SongResult } from '../shared/interfaces/search.interface'
|
import { SongSearch, SearchType, SongResult } from '../shared/interfaces/search.interface'
|
||||||
import { escape } from 'mysql'
|
import { escape } from 'mysql'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the 'song-search' event.
|
||||||
|
*/
|
||||||
export default class SearchHandler implements IPCInvokeHandler<'song-search'> {
|
export default class SearchHandler implements IPCInvokeHandler<'song-search'> {
|
||||||
event: 'song-search' = 'song-search'
|
event: 'song-search' = 'song-search'
|
||||||
// TODO: add method documentation
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the top 20 songs that match `search`.
|
||||||
|
*/
|
||||||
async handler(search: SongSearch) {
|
async handler(search: SongSearch) {
|
||||||
const db = await Database.getInstance()
|
const db = await Database.getInstance()
|
||||||
|
|
||||||
return db.sendQuery(this.getSearchQuery(search)) as Promise<SongResult[]>
|
return db.sendQuery(this.getSearchQuery(search)) as Promise<SongResult[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a database query that returns the type of results expected by `search.type`.
|
||||||
|
*/
|
||||||
private getSearchQuery(search: SongSearch) {
|
private getSearchQuery(search: SongSearch) {
|
||||||
switch (search.type) {
|
switch (search.type) {
|
||||||
case SearchType.Any: return this.getGeneralSearchQuery(search.query)
|
case SearchType.Any: return this.getGeneralSearchQuery(search.query)
|
||||||
@@ -20,6 +28,9 @@ export default class SearchHandler implements IPCInvokeHandler<'song-search'> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a database query that returns the top 20 songs that match `search`.
|
||||||
|
*/
|
||||||
private getGeneralSearchQuery(searchString: string) {
|
private getGeneralSearchQuery(searchString: string) {
|
||||||
return `
|
return `
|
||||||
SELECT id, name, artist, album, genre, year
|
SELECT id, name, artist, album, genre, year
|
||||||
|
|||||||
88
src/electron/ipc/SettingsHandler.ipc.ts
Normal file
88
src/electron/ipc/SettingsHandler.ipc.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { exists as _exists, mkdir as _mkdir, readFile as _readFile, writeFile as _writeFile } from 'fs'
|
||||||
|
import { dataPath, tempPath, themesPath, settingsPath } from '../shared/Paths'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import { IPCInvokeHandler, IPCEmitHandler } from '../shared/IPCHandler'
|
||||||
|
import { defaultSettings, Settings } from '../shared/Settings'
|
||||||
|
|
||||||
|
const exists = promisify(_exists)
|
||||||
|
const mkdir = promisify(_mkdir)
|
||||||
|
const readFile = promisify(_readFile)
|
||||||
|
const writeFile = promisify(_writeFile)
|
||||||
|
|
||||||
|
let settings: Settings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the 'get-settings' event.
|
||||||
|
*/
|
||||||
|
export class GetSettingsHandler implements IPCInvokeHandler<'get-settings'> {
|
||||||
|
event: 'get-settings' = 'get-settings'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the current settings oject, or default settings if they couldn't be loaded.
|
||||||
|
*/
|
||||||
|
handler() {
|
||||||
|
return GetSettingsHandler.getSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the current settings oject, or default settings if they couldn't be loaded.
|
||||||
|
*/
|
||||||
|
static getSettings() {
|
||||||
|
if (settings == undefined) {
|
||||||
|
return defaultSettings
|
||||||
|
} else {
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If data directories don't exist, creates them and saves the default settings.
|
||||||
|
* Otherwise, loads user settings from data directories.
|
||||||
|
* If this process fails, default settings are used.
|
||||||
|
*/
|
||||||
|
static async initSettings() {
|
||||||
|
try {
|
||||||
|
// Create data directories if they don't exists
|
||||||
|
for (const path of [dataPath, tempPath, themesPath]) {
|
||||||
|
if (!await exists(path)) {
|
||||||
|
await mkdir(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read/create settings
|
||||||
|
if (await exists(settingsPath)) {
|
||||||
|
settings = JSON.parse(await readFile(settingsPath, 'utf8'))
|
||||||
|
} else {
|
||||||
|
await SetSettingsHandler.saveSettings(defaultSettings)
|
||||||
|
settings = defaultSettings
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to initialize settings! Default settings will be used.')
|
||||||
|
console.error(e)
|
||||||
|
settings = defaultSettings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the 'set-settings' event.
|
||||||
|
*/
|
||||||
|
export class SetSettingsHandler implements IPCEmitHandler<'set-settings'> {
|
||||||
|
event: 'set-settings' = 'set-settings'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates Bridge's settings object to `newSettings` and saves them to Bridge's data directories.
|
||||||
|
*/
|
||||||
|
handler(newSettings: Settings) {
|
||||||
|
settings = newSettings
|
||||||
|
SetSettingsHandler.saveSettings(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves `settings` to Bridge's data directories.
|
||||||
|
*/
|
||||||
|
static async saveSettings(settings: Settings) {
|
||||||
|
const settingsJSON = JSON.stringify(settings, undefined, 2)
|
||||||
|
await writeFile(settingsPath, settingsJSON, 'utf8')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,24 @@ import { IPCInvokeHandler } from '../shared/IPCHandler'
|
|||||||
import Database from '../shared/Database'
|
import Database from '../shared/Database'
|
||||||
import { VersionResult } from '../shared/interfaces/songDetails.interface'
|
import { VersionResult } from '../shared/interfaces/songDetails.interface'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the 'song-details' event.
|
||||||
|
*/
|
||||||
export default class SongDetailsHandler implements IPCInvokeHandler<'song-details'> {
|
export default class SongDetailsHandler implements IPCInvokeHandler<'song-details'> {
|
||||||
event: 'song-details' = 'song-details'
|
event: 'song-details' = 'song-details'
|
||||||
// TODO: add method documentation
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the chart versions with `songID`.
|
||||||
|
*/
|
||||||
async handler(songID: number) {
|
async handler(songID: number) {
|
||||||
const db = await Database.getInstance()
|
const db = await Database.getInstance()
|
||||||
|
|
||||||
return db.sendQuery(this.getVersionQuery(songID)) as Promise<VersionResult[]>
|
return db.sendQuery(this.getVersionQuery(songID)) as Promise<VersionResult[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a database query that returns the chart versions with `songID`.
|
||||||
|
*/
|
||||||
private getVersionQuery(songID: number) {
|
private getVersionQuery(songID: number) {
|
||||||
return `
|
return `
|
||||||
SELECT *
|
SELECT *
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { createHash, randomBytes as _randomBytes } from 'crypto'
|
|||||||
import { tempPath } from '../../shared/Paths'
|
import { tempPath } from '../../shared/Paths'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { Download, NewDownload, DownloadProgress } from '../../shared/interfaces/download.interface'
|
import { Download, DownloadProgress } from '../../shared/interfaces/download.interface'
|
||||||
import { emitIPCEvent } from '../../main'
|
import { emitIPCEvent } from '../../main'
|
||||||
import { mkdir as _mkdir } from 'fs'
|
import { mkdir as _mkdir } from 'fs'
|
||||||
import { FileExtractor } from './FileExtractor'
|
import { FileExtractor } from './FileExtractor'
|
||||||
import { sanitizeFilename, interpolate } from '../../shared/UtilFunctions'
|
import { sanitizeFilename, interpolate } from '../../shared/UtilFunctions'
|
||||||
|
import { GetSettingsHandler } from '../SettingsHandler.ipc'
|
||||||
|
|
||||||
const randomBytes = promisify(_randomBytes)
|
const randomBytes = promisify(_randomBytes)
|
||||||
const mkdir = promisify(_mkdir)
|
const mkdir = promisify(_mkdir)
|
||||||
@@ -19,6 +20,7 @@ export class DownloadHandler implements IPCEmitHandler<'download'> {
|
|||||||
// 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?)
|
||||||
|
|
||||||
downloadCallbacks: { [versionID: number]: { cancel: () => void, retry: () => void, continue: () => void } } = {}
|
downloadCallbacks: { [versionID: number]: { cancel: () => void, retry: () => void, continue: () => void } } = {}
|
||||||
|
private allFilesProgress = 0
|
||||||
|
|
||||||
async handler(data: Download) {
|
async handler(data: Download) {
|
||||||
switch (data.action) {
|
switch (data.action) {
|
||||||
@@ -27,8 +29,9 @@ export class DownloadHandler implements IPCEmitHandler<'download'> {
|
|||||||
case 'continue': this.downloadCallbacks[data.versionID].continue(); return
|
case 'continue': this.downloadCallbacks[data.versionID].continue(); return
|
||||||
case 'add': this.downloadCallbacks[data.versionID] = { cancel: () => { }, retry: () => { }, continue: () => { } }
|
case 'add': this.downloadCallbacks[data.versionID] = { cancel: () => { }, retry: () => { }, continue: () => { } }
|
||||||
}
|
}
|
||||||
|
// after this point, (data.action == add), so data.data should be defined
|
||||||
|
|
||||||
// data.action == add; data.data should be defined
|
// Initialize download object
|
||||||
const download: DownloadProgress = {
|
const download: DownloadProgress = {
|
||||||
versionID: data.versionID,
|
versionID: data.versionID,
|
||||||
title: `${data.data.avTagName} - ${data.data.artist}`,
|
title: `${data.data.avTagName} - ${data.data.artist}`,
|
||||||
@@ -37,32 +40,80 @@ export class DownloadHandler implements IPCEmitHandler<'download'> {
|
|||||||
percent: 0,
|
percent: 0,
|
||||||
type: 'good'
|
type: 'good'
|
||||||
}
|
}
|
||||||
const randomString = (await randomBytes(5)).toString('hex')
|
|
||||||
const chartPath = join(tempPath, `chart_${randomString}`)
|
|
||||||
await mkdir(chartPath)
|
|
||||||
|
|
||||||
let allFilesProgress = 0
|
// Create a temporary folder to store the downloaded files
|
||||||
|
let chartPath: string
|
||||||
|
try {
|
||||||
|
chartPath = await this.createDownloadFolder()
|
||||||
|
} catch (e) {
|
||||||
|
download.header = 'Access Error'
|
||||||
|
download.description = e.message
|
||||||
|
download.type = 'error'
|
||||||
|
this.downloadCallbacks[data.versionID].retry = () => { this.handler(data) }
|
||||||
|
emitIPCEvent('download-updated', download)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each actual download link in <data.data.links>, download the file to <chartPath>
|
||||||
// Only iterate over the keys in data.links that have link values (not hashes)
|
// Only iterate over the keys in data.links that have link values (not hashes)
|
||||||
const fileKeys = Object.keys(data.data.links).filter(link => data.data.links[link].includes('.'))
|
const fileKeys = Object.keys(data.data.links).filter(link => data.data.links[link].includes('.'))
|
||||||
const individualFileProgressPortion = 80 / fileKeys.length
|
|
||||||
for (let i = 0; i < fileKeys.length; i++) {
|
for (let i = 0; i < fileKeys.length; i++) {
|
||||||
|
// INITIALIZE DOWNLOADER
|
||||||
|
// <data.data.links[typeHash]> stores the expected hash value found in the download header
|
||||||
const typeHash = createHash('md5').update(data.data.links[fileKeys[i]]).digest('hex')
|
const typeHash = createHash('md5').update(data.data.links[fileKeys[i]]).digest('hex')
|
||||||
const downloader = await FileDownloader.asyncConstructor(data.data.links[fileKeys[i]], chartPath, fileKeys.length, data.data.links[typeHash])
|
const downloader = new FileDownloader(data.data.links[fileKeys[i]], chartPath, fileKeys.length, data.data.links[typeHash])
|
||||||
this.downloadCallbacks[data.versionID].cancel = () => downloader.cancelDownload() // Make cancel button cancel this download
|
this.downloadCallbacks[data.versionID].cancel = () => downloader.cancelDownload() // Make cancel button cancel this download
|
||||||
|
const downloadComplete = this.addDownloadEventListeners(downloader, download, fileKeys, i)
|
||||||
|
|
||||||
|
// DOWNLOAD THE NEXT FILE
|
||||||
|
downloader.beginDownload()
|
||||||
|
await downloadComplete // Wait for this download to finish before downloading the next file
|
||||||
|
}
|
||||||
|
|
||||||
|
// INITIALIZE FILE EXTRACTOR
|
||||||
|
const destinationFolderName = sanitizeFilename(`${data.data.artist} - ${data.data.avTagName} (${data.data.charter})`)
|
||||||
|
const extractor = new FileExtractor(chartPath, fileKeys.includes('archive'), destinationFolderName)
|
||||||
|
this.downloadCallbacks[data.versionID].cancel = () => extractor.cancelExtract() // Make cancel button cancel the file extraction
|
||||||
|
this.addExtractorEventListeners(extractor, download)
|
||||||
|
|
||||||
|
// EXTRACT THE DOWNLOADED ARCHIVE
|
||||||
|
extractor.beginExtract()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createDownloadFolder() {
|
||||||
|
let retryCount = 0
|
||||||
|
while (true) {
|
||||||
|
const randomString = (await randomBytes(5)).toString('hex')
|
||||||
|
const chartPath = join(tempPath, `chart_${randomString}`)
|
||||||
|
try {
|
||||||
|
await mkdir(chartPath)
|
||||||
|
return chartPath
|
||||||
|
} catch (e) {
|
||||||
|
if (retryCount > 5) {
|
||||||
|
throw new Error(`Bridge was unable to create a directory at [${chartPath}]`)
|
||||||
|
} else {
|
||||||
|
console.log(`Error creating folder [${chartPath}], retrying with a different folder...`)
|
||||||
|
retryCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addDownloadEventListeners(downloader: FileDownloader, download: DownloadProgress, fileKeys: string[], i: number) {
|
||||||
|
const individualFileProgressPortion = 80 / fileKeys.length
|
||||||
let fileProgress = 0
|
let fileProgress = 0
|
||||||
|
|
||||||
let initialWaitTime: number
|
|
||||||
downloader.on('wait', (waitTime) => {
|
downloader.on('wait', (waitTime) => {
|
||||||
download.header = `[${fileKeys[i]}] (file ${i + 1}/${fileKeys.length})`
|
download.header = `[${fileKeys[i]}] (file ${i + 1}/${fileKeys.length})`
|
||||||
download.description = `Waiting for Google rate limit... (${waitTime}s)`
|
download.description = `Waiting for Google rate limit... (${waitTime}s)`
|
||||||
download.type = 'wait'
|
download.type = 'wait'
|
||||||
initialWaitTime = waitTime
|
|
||||||
})
|
})
|
||||||
|
|
||||||
downloader.on('waitProgress', (secondsRemaining) => {
|
downloader.on('waitProgress', (secondsRemaining, initialWaitTime) => {
|
||||||
download.description = `Waiting for Google rate limit... (${secondsRemaining}s)`
|
download.description = `Waiting for Google rate limit... (${secondsRemaining}s)`
|
||||||
fileProgress = interpolate(secondsRemaining, initialWaitTime, 0, 0, individualFileProgressPortion / 2)
|
fileProgress = interpolate(secondsRemaining, initialWaitTime, 0, 0, individualFileProgressPortion / 2)
|
||||||
download.percent = allFilesProgress + fileProgress
|
console.log(`${initialWaitTime} ... ${secondsRemaining} ... 0`)
|
||||||
|
download.percent = this.allFilesProgress + fileProgress
|
||||||
download.type = 'wait'
|
download.type = 'wait'
|
||||||
emitIPCEvent('download-updated', download)
|
emitIPCEvent('download-updated', download)
|
||||||
})
|
})
|
||||||
@@ -70,14 +121,14 @@ export class DownloadHandler implements IPCEmitHandler<'download'> {
|
|||||||
downloader.on('request', () => {
|
downloader.on('request', () => {
|
||||||
download.description = `Sending request...`
|
download.description = `Sending request...`
|
||||||
fileProgress = individualFileProgressPortion / 2
|
fileProgress = individualFileProgressPortion / 2
|
||||||
download.percent = allFilesProgress + fileProgress
|
download.percent = this.allFilesProgress + fileProgress
|
||||||
download.type = 'good'
|
download.type = 'good'
|
||||||
emitIPCEvent('download-updated', download)
|
emitIPCEvent('download-updated', download)
|
||||||
})
|
})
|
||||||
|
|
||||||
downloader.on('warning', (continueAnyway) => {
|
downloader.on('warning', (continueAnyway) => {
|
||||||
download.description = 'WARNING'
|
download.description = 'WARNING'
|
||||||
this.downloadCallbacks[data.versionID].continue = continueAnyway
|
this.downloadCallbacks[download.versionID].continue = continueAnyway
|
||||||
download.type = 'warning'
|
download.type = 'warning'
|
||||||
emitIPCEvent('download-updated', download)
|
emitIPCEvent('download-updated', download)
|
||||||
})
|
})
|
||||||
@@ -99,10 +150,10 @@ export class DownloadHandler implements IPCEmitHandler<'download'> {
|
|||||||
if (filesize != -1) {
|
if (filesize != -1) {
|
||||||
download.description = `Downloading... (${Math.round(1000 * bytesDownloaded / filesize) / 10}%)`
|
download.description = `Downloading... (${Math.round(1000 * bytesDownloaded / filesize) / 10}%)`
|
||||||
fileProgress = interpolate(bytesDownloaded, 0, filesize, individualFileProgressPortion / 2, individualFileProgressPortion)
|
fileProgress = interpolate(bytesDownloaded, 0, filesize, individualFileProgressPortion / 2, individualFileProgressPortion)
|
||||||
download.percent = allFilesProgress + fileProgress
|
download.percent = this.allFilesProgress + fileProgress
|
||||||
} else {
|
} else {
|
||||||
download.description = `Downloading... (${Math.round(bytesDownloaded / 1e+5) / 10} MB)`
|
download.description = `Downloading... (${Math.round(bytesDownloaded / 1e+5) / 10} MB)`
|
||||||
download.percent = allFilesProgress + fileProgress
|
download.percent = this.allFilesProgress + fileProgress
|
||||||
}
|
}
|
||||||
download.type = 'good'
|
download.type = 'good'
|
||||||
emitIPCEvent('download-updated', download)
|
emitIPCEvent('download-updated', download)
|
||||||
@@ -112,27 +163,23 @@ export class DownloadHandler implements IPCEmitHandler<'download'> {
|
|||||||
download.header = error.header
|
download.header = error.header
|
||||||
download.description = error.body
|
download.description = error.body
|
||||||
download.type = 'error'
|
download.type = 'error'
|
||||||
this.downloadCallbacks[data.versionID].retry = retry
|
this.downloadCallbacks[download.versionID].retry = retry
|
||||||
emitIPCEvent('download-updated', download)
|
emitIPCEvent('download-updated', download)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Wait for the 'complete' event before moving on to another file download
|
// Returns a promise that resolves when the download is finished
|
||||||
await new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
downloader.on('complete', () => {
|
downloader.on('complete', () => {
|
||||||
emitIPCEvent('download-updated', download)
|
emitIPCEvent('download-updated', download)
|
||||||
allFilesProgress += individualFileProgressPortion
|
this.allFilesProgress += individualFileProgressPortion
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
|
|
||||||
downloader.beginDownload()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const destinationFolderName = sanitizeFilename(`${data.data.artist} - ${data.data.avTagName} (${data.data.charter})`)
|
private addExtractorEventListeners(extractor: FileExtractor, download: DownloadProgress) {
|
||||||
const extractor = new FileExtractor(chartPath, fileKeys.includes('archive'), destinationFolderName)
|
|
||||||
this.downloadCallbacks[data.versionID].cancel = () => extractor.cancelExtract()
|
|
||||||
|
|
||||||
let archive = ''
|
let archive = ''
|
||||||
|
|
||||||
extractor.on('extract', (filename) => {
|
extractor.on('extract', (filename) => {
|
||||||
archive = filename
|
archive = filename
|
||||||
download.header = `[${archive}]`
|
download.header = `[${archive}]`
|
||||||
@@ -169,10 +216,8 @@ export class DownloadHandler implements IPCEmitHandler<'download'> {
|
|||||||
download.header = error.header
|
download.header = error.header
|
||||||
download.description = error.body
|
download.description = error.body
|
||||||
download.type = 'error'
|
download.type = 'error'
|
||||||
this.downloadCallbacks[data.versionID].retry = retry
|
this.downloadCallbacks[download.versionID].retry = retry
|
||||||
emitIPCEvent('download-updated', download)
|
emitIPCEvent('download-updated', download)
|
||||||
})
|
})
|
||||||
|
|
||||||
extractor.beginExtract()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,11 @@ import { generateUUID, sanitizeFilename } from '../../shared/UtilFunctions'
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as needle from 'needle'
|
import * as needle from 'needle'
|
||||||
import InitSettingsHandler from '../InitSettingsHandler.ipc'
|
import { GetSettingsHandler } from '../SettingsHandler.ipc'
|
||||||
import { Settings } from 'src/electron/shared/Settings'
|
|
||||||
|
|
||||||
type EventCallback = {
|
type EventCallback = {
|
||||||
'wait': (waitTime: number) => void
|
'wait': (waitTime: number) => void
|
||||||
'waitProgress': (secondsRemaining: number) => void
|
'waitProgress': (secondsRemaining: number, initialWaitTime: number) => void
|
||||||
'request': () => void
|
'request': () => void
|
||||||
'warning': (continueAnyway: () => void) => void
|
'warning': (continueAnyway: () => void) => void
|
||||||
'download': (filename: string, filesize?: number) => void
|
'download': (filename: string, filesize?: number) => void
|
||||||
@@ -20,42 +19,40 @@ type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
|||||||
export type DownloadError = { header: string, body: string }
|
export type DownloadError = { header: string, body: string }
|
||||||
|
|
||||||
export class FileDownloader {
|
export class FileDownloader {
|
||||||
private RATE_LIMIT_DELAY: number
|
|
||||||
private readonly RETRY_MAX = 2
|
private readonly RETRY_MAX = 2
|
||||||
private static fileQueue: { // Stores the overall order that files should be downloaded
|
private static fileQueue: { // Stores the overall order that files should be downloaded
|
||||||
destinationFolder: string
|
destinationFolder: string
|
||||||
fileCount: number
|
fileCount: number
|
||||||
clock?: () => void
|
clock?: () => void
|
||||||
}[] = []
|
}[]
|
||||||
private static waitTime: number
|
private static waitTime: number
|
||||||
private static settings: Settings
|
|
||||||
|
|
||||||
private callbacks = {} as Callbacks
|
private callbacks = {} as Callbacks
|
||||||
private retryCount: number
|
private retryCount: number
|
||||||
private wasCanceled = false
|
private wasCanceled = false
|
||||||
|
|
||||||
private constructor(private url: string, private destinationFolder: string, private numFiles: number, private expectedHash?: string) { }
|
constructor(private url: string, private destinationFolder: string, private numFiles: number, private expectedHash?: string) {
|
||||||
static async asyncConstructor(url: string, destinationFolder: string, numFiles: number, expectedHash?: string) {
|
if (FileDownloader.fileQueue == undefined) {
|
||||||
const downloader = new FileDownloader(url, destinationFolder, numFiles, expectedHash)
|
// First initialization
|
||||||
if (FileDownloader.settings == undefined) {
|
FileDownloader.fileQueue = []
|
||||||
await downloader.firstInit()
|
let lastRateLimitDelay = GetSettingsHandler.getSettings().rateLimitDelay
|
||||||
}
|
|
||||||
return downloader
|
|
||||||
}
|
|
||||||
|
|
||||||
async firstInit() {
|
|
||||||
FileDownloader.settings = await InitSettingsHandler.getSettings()
|
|
||||||
FileDownloader.waitTime = 0
|
FileDownloader.waitTime = 0
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (FileDownloader.waitTime > 0) {
|
if (FileDownloader.waitTime > 0) { // Update current countdown if this setting changes
|
||||||
|
let newRateLimitDelay = GetSettingsHandler.getSettings().rateLimitDelay
|
||||||
|
if (newRateLimitDelay != lastRateLimitDelay) {
|
||||||
|
FileDownloader.waitTime -= Math.min(lastRateLimitDelay - newRateLimitDelay, FileDownloader.waitTime - 1)
|
||||||
|
lastRateLimitDelay = newRateLimitDelay
|
||||||
|
}
|
||||||
FileDownloader.waitTime--
|
FileDownloader.waitTime--
|
||||||
}
|
}
|
||||||
FileDownloader.fileQueue.forEach(download => { if (download.clock != undefined) download.clock() })
|
FileDownloader.fileQueue.forEach(download => { if (download.clock != undefined) download.clock() })
|
||||||
if (FileDownloader.waitTime == 0 && FileDownloader.fileQueue.length != 0) {
|
if (FileDownloader.waitTime <= 0 && FileDownloader.fileQueue.length != 0) {
|
||||||
FileDownloader.waitTime = this.RATE_LIMIT_DELAY
|
FileDownloader.waitTime = GetSettingsHandler.getSettings().rateLimitDelay
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls <callback> when <event> fires.
|
* Calls <callback> when <event> fires.
|
||||||
@@ -72,7 +69,7 @@ export class FileDownloader {
|
|||||||
*/
|
*/
|
||||||
beginDownload() {
|
beginDownload() {
|
||||||
// Check that the library folder has been specified
|
// Check that the library folder has been specified
|
||||||
if (FileDownloader.settings.libraryPath == undefined) {
|
if (GetSettingsHandler.getSettings().libraryPath == undefined) {
|
||||||
this.callbacks.error({ header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' }, () => this.beginDownload())
|
this.callbacks.error({ header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' }, () => this.beginDownload())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -82,12 +79,16 @@ export class FileDownloader {
|
|||||||
this.requestDownload()
|
this.requestDownload()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// The starting point of a progress bar should be recalculated each clock cycle
|
||||||
|
// It will be what it would have been if rateLimitDelay was that value the entire time
|
||||||
this.initWaitTime()
|
this.initWaitTime()
|
||||||
let queueWaitTime = this.getQueueWaitTime()
|
// This is the number of seconds that had elapsed since the last file download (at the time of starting this download)
|
||||||
this.callbacks.wait(queueWaitTime + FileDownloader.waitTime)
|
const initialTimeSinceLastDownload = GetSettingsHandler.getSettings().rateLimitDelay - FileDownloader.waitTime
|
||||||
if (queueWaitTime + FileDownloader.waitTime == 0) {
|
const initialQueueCount = this.getQueueCount()
|
||||||
FileDownloader.waitTime = this.RATE_LIMIT_DELAY
|
let waitTime = this.getWaitTime(initialTimeSinceLastDownload, initialQueueCount)
|
||||||
|
this.callbacks.wait(waitTime)
|
||||||
|
if (waitTime == 0) {
|
||||||
|
FileDownloader.waitTime = GetSettingsHandler.getSettings().rateLimitDelay
|
||||||
this.requestDownload()
|
this.requestDownload()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -95,17 +96,21 @@ export class FileDownloader {
|
|||||||
const fileQueue = FileDownloader.fileQueue.find(queue => queue.destinationFolder == this.destinationFolder)
|
const fileQueue = FileDownloader.fileQueue.find(queue => queue.destinationFolder == this.destinationFolder)
|
||||||
fileQueue.clock = () => {
|
fileQueue.clock = () => {
|
||||||
if (this.wasCanceled) { this.removeFromQueue(); return } // CANCEL POINT
|
if (this.wasCanceled) { this.removeFromQueue(); return } // CANCEL POINT
|
||||||
queueWaitTime = this.getQueueWaitTime()
|
waitTime = this.getWaitTime(GetSettingsHandler.getSettings().rateLimitDelay - FileDownloader.waitTime, this.getQueueCount())
|
||||||
if (queueWaitTime + FileDownloader.waitTime == 0) {
|
if (waitTime == 0) {
|
||||||
this.requestDownload()
|
this.requestDownload()
|
||||||
fileQueue.clock = undefined
|
fileQueue.clock = undefined
|
||||||
}
|
}
|
||||||
this.callbacks.waitProgress(queueWaitTime + FileDownloader.waitTime)
|
this.callbacks.waitProgress(waitTime, this.getWaitTime(initialTimeSinceLastDownload, this.getQueueCount()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getWaitTime(timeSinceLastDownload: number, queueCount: number) {
|
||||||
|
const rateLimitDelay = GetSettingsHandler.getSettings().rateLimitDelay
|
||||||
|
return (queueCount * rateLimitDelay) + Math.max(0, rateLimitDelay - timeSinceLastDownload)
|
||||||
|
}
|
||||||
|
|
||||||
private initWaitTime() {
|
private initWaitTime() {
|
||||||
this.RATE_LIMIT_DELAY = FileDownloader.settings.rateLimitDelay
|
|
||||||
this.retryCount = 0
|
this.retryCount = 0
|
||||||
const entry = FileDownloader.fileQueue.find(entry => entry.destinationFolder == this.destinationFolder)
|
const entry = FileDownloader.fileQueue.find(entry => entry.destinationFolder == this.destinationFolder)
|
||||||
if (entry == undefined) {
|
if (entry == undefined) {
|
||||||
@@ -117,7 +122,7 @@ export class FileDownloader {
|
|||||||
/**
|
/**
|
||||||
* Returns the number of files in front of this file in the fileQueue
|
* Returns the number of files in front of this file in the fileQueue
|
||||||
*/
|
*/
|
||||||
private getQueueWaitTime() {
|
private getQueueCount() {
|
||||||
let fileCount = 0
|
let fileCount = 0
|
||||||
for (let entry of FileDownloader.fileQueue) {
|
for (let entry of FileDownloader.fileQueue) {
|
||||||
if (entry.destinationFolder != this.destinationFolder) {
|
if (entry.destinationFolder != this.destinationFolder) {
|
||||||
@@ -127,7 +132,7 @@ export class FileDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileCount * this.RATE_LIMIT_DELAY
|
return fileCount
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeFromQueue() {
|
private removeFromQueue() {
|
||||||
@@ -207,9 +212,9 @@ export class FileDownloader {
|
|||||||
req.on('data', data => virusScanHTML += data)
|
req.on('data', data => virusScanHTML += data)
|
||||||
req.on('done', (err: Error) => {
|
req.on('done', (err: Error) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
|
try {
|
||||||
const confirmTokenRegex = /confirm=([0-9A-Za-z\-_]+)&/g
|
const confirmTokenRegex = /confirm=([0-9A-Za-z\-_]+)&/g
|
||||||
const confirmTokenResults = confirmTokenRegex.exec(virusScanHTML)
|
const confirmTokenResults = confirmTokenRegex.exec(virusScanHTML)
|
||||||
if (confirmTokenResults != null) {
|
|
||||||
const confirmToken = confirmTokenResults[1]
|
const confirmToken = confirmTokenResults[1]
|
||||||
const downloadID = this.url.substr(this.url.indexOf('id=') + 'id='.length)
|
const downloadID = this.url.substr(this.url.indexOf('id=') + 'id='.length)
|
||||||
this.url = `https://drive.google.com/uc?confirm=${confirmToken}&id=${downloadID}`
|
this.url = `https://drive.google.com/uc?confirm=${confirmToken}&id=${downloadID}`
|
||||||
@@ -217,7 +222,7 @@ export class FileDownloader {
|
|||||||
const NID = /NID=([^;]*);/.exec(cookieHeader)[1].replace('=', '%')
|
const NID = /NID=([^;]*);/.exec(cookieHeader)[1].replace('=', '%')
|
||||||
const newHeader = `download_warning_${warningCode}=${confirmToken}; NID=${NID}`
|
const newHeader = `download_warning_${warningCode}=${confirmToken}; NID=${NID}`
|
||||||
this.requestDownload(newHeader)
|
this.requestDownload(newHeader)
|
||||||
} else {
|
} catch(e) {
|
||||||
this.callbacks.error({ header: 'Invalid response', body: 'Download server returned HTML instead of a file.' }, () => this.beginDownload())
|
this.callbacks.error({ header: 'Invalid response', body: 'Download server returned HTML instead of a file.' }, () => this.beginDownload())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { join, extname } from 'path'
|
|||||||
import * as node7z from 'node-7z'
|
import * as node7z from 'node-7z'
|
||||||
import * as zipBin from '7zip-bin'
|
import * as zipBin from '7zip-bin'
|
||||||
import * as unrarjs from 'node-unrar-js'
|
import * as unrarjs from 'node-unrar-js'
|
||||||
import InitSettingsHandler from '../InitSettingsHandler.ipc'
|
import { GetSettingsHandler } from '../SettingsHandler.ipc'
|
||||||
|
|
||||||
const readdir = promisify(_readdir)
|
const readdir = promisify(_readdir)
|
||||||
const unlink = promisify(_unlink)
|
const unlink = promisify(_unlink)
|
||||||
@@ -45,7 +45,7 @@ export class FileExtractor {
|
|||||||
* Starts the chart extraction process.
|
* Starts the chart extraction process.
|
||||||
*/
|
*/
|
||||||
async beginExtract() {
|
async beginExtract() {
|
||||||
this.libraryFolder = (await InitSettingsHandler.getSettings()).libraryPath
|
this.libraryFolder = (await GetSettingsHandler.getSettings()).libraryPath
|
||||||
const files = await readdir(this.sourceFolder)
|
const files = await readdir(this.sourceFolder)
|
||||||
if (this.isArchive) {
|
if (this.isArchive) {
|
||||||
this.extract(files[0])
|
this.extract(files[0])
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as url from 'url'
|
|||||||
// IPC Handlers
|
// IPC Handlers
|
||||||
import { getIPCInvokeHandlers, getIPCEmitHandlers, IPCEmitEvents } from './shared/IPCHandler'
|
import { getIPCInvokeHandlers, getIPCEmitHandlers, IPCEmitEvents } from './shared/IPCHandler'
|
||||||
import Database from './shared/Database'
|
import Database from './shared/Database'
|
||||||
|
import { GetSettingsHandler } from './ipc/SettingsHandler.ipc'
|
||||||
|
|
||||||
let mainWindow: BrowserWindow
|
let mainWindow: BrowserWindow
|
||||||
const args = process.argv.slice(1)
|
const args = process.argv.slice(1)
|
||||||
@@ -12,7 +13,10 @@ const isDevBuild = args.some(val => val == '--dev')
|
|||||||
|
|
||||||
restrictToSingleInstance()
|
restrictToSingleInstance()
|
||||||
handleOSXWindowClosed()
|
handleOSXWindowClosed()
|
||||||
app.on('ready', createBridgeWindow)
|
app.on('ready', () => {
|
||||||
|
// Load settings from file before the window is created
|
||||||
|
GetSettingsHandler.initSettings().then(createBridgeWindow)
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only allow a single Bridge window to be open at any one time.
|
* Only allow a single Bridge window to be open at any one time.
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ export default class Database {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys the database connection.
|
||||||
|
*/
|
||||||
static closeConnection() {
|
static closeConnection() {
|
||||||
if (this.database != undefined) {
|
if (this.database != undefined) {
|
||||||
this.database.conn.destroy()
|
this.database.conn.destroy()
|
||||||
@@ -57,10 +60,9 @@ export default class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends <query> to the database.
|
* Sends `query` to the database.
|
||||||
* @param query The query string to be sent.
|
* @param queryStatement The nth response statement to be returned. If undefined, the entire response is returned.
|
||||||
* @param queryStatement The nth response statement to be returned.
|
* @returns the selected response statement, or an empty array if the query fails.
|
||||||
* @returns one of the responses as type <ResponseType[]>, or an empty array if the query fails.
|
|
||||||
*/
|
*/
|
||||||
async sendQuery<ResponseType>(query: string, queryStatement?: number) {
|
async sendQuery<ResponseType>(query: string, queryStatement?: number) {
|
||||||
return new Promise<ResponseType[] | ResponseType>(resolve => {
|
return new Promise<ResponseType[] | ResponseType>(resolve => {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import InitSettingsHandler from '../ipc/InitSettingsHandler.ipc'
|
|
||||||
import { basename } from 'path'
|
import { basename } from 'path'
|
||||||
|
import { GetSettingsHandler } from '../ipc/SettingsHandler.ipc'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param absoluteFilepath The absolute filepath to a folder
|
* @returns The relative filepath from the library folder to `absoluteFilepath`.
|
||||||
* @returns The relative filepath from the scanned folder to <absoluteFilepath>
|
|
||||||
*/
|
*/
|
||||||
export async function getRelativeFilepath(absoluteFilepath: string) {
|
export function getRelativeFilepath(absoluteFilepath: string) {
|
||||||
const settings = await InitSettingsHandler.getSettings()
|
const settings = GetSettingsHandler.getSettings()
|
||||||
return basename(settings.libraryPath) + absoluteFilepath.substring(settings.libraryPath.length)
|
return basename(settings.libraryPath) + absoluteFilepath.substring(settings.libraryPath.length)
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,11 @@ import { VersionResult, AlbumArtResult } from './interfaces/songDetails.interfac
|
|||||||
import SearchHandler from '../ipc/SearchHandler.ipc'
|
import SearchHandler from '../ipc/SearchHandler.ipc'
|
||||||
import SongDetailsHandler from '../ipc/SongDetailsHandler.ipc'
|
import SongDetailsHandler from '../ipc/SongDetailsHandler.ipc'
|
||||||
import AlbumArtHandler from '../ipc/AlbumArtHandler.ipc'
|
import AlbumArtHandler from '../ipc/AlbumArtHandler.ipc'
|
||||||
import { Download, NewDownload, DownloadProgress } from './interfaces/download.interface'
|
import { Download, DownloadProgress } from './interfaces/download.interface'
|
||||||
import { DownloadHandler } from '../ipc/download/DownloadHandler'
|
import { DownloadHandler } from '../ipc/download/DownloadHandler'
|
||||||
import { Settings } from './Settings'
|
import { Settings } from './Settings'
|
||||||
import InitSettingsHandler from '../ipc/InitSettingsHandler.ipc'
|
|
||||||
import BatchSongDetailsHandler from '../ipc/BatchSongDetailsHandler.ipc'
|
import BatchSongDetailsHandler from '../ipc/BatchSongDetailsHandler.ipc'
|
||||||
import SaveSettingsHandler from '../ipc/SaveSettingsHandler.ipc'
|
import { GetSettingsHandler, SetSettingsHandler } from '../ipc/SettingsHandler.ipc'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To add a new IPC listener:
|
* To add a new IPC listener:
|
||||||
@@ -20,7 +19,7 @@ import SaveSettingsHandler from '../ipc/SaveSettingsHandler.ipc'
|
|||||||
|
|
||||||
export function getIPCInvokeHandlers(): IPCInvokeHandler<keyof IPCInvokeEvents>[] {
|
export function getIPCInvokeHandlers(): IPCInvokeHandler<keyof IPCInvokeEvents>[] {
|
||||||
return [
|
return [
|
||||||
new InitSettingsHandler(),
|
new GetSettingsHandler(),
|
||||||
new SearchHandler(),
|
new SearchHandler(),
|
||||||
new SongDetailsHandler(),
|
new SongDetailsHandler(),
|
||||||
new BatchSongDetailsHandler(),
|
new BatchSongDetailsHandler(),
|
||||||
@@ -28,8 +27,11 @@ export function getIPCInvokeHandlers(): IPCInvokeHandler<keyof IPCInvokeEvents>[
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of possible async IPC events that return values, mapped to their input and output types.
|
||||||
|
*/
|
||||||
export type IPCInvokeEvents = {
|
export type IPCInvokeEvents = {
|
||||||
'init-settings': {
|
'get-settings': {
|
||||||
input: undefined
|
input: undefined
|
||||||
output: Settings
|
output: Settings
|
||||||
}
|
}
|
||||||
@@ -51,24 +53,34 @@ export type IPCInvokeEvents = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes an object that handles the `E` async IPC event that will return a value.
|
||||||
|
*/
|
||||||
export interface IPCInvokeHandler<E extends keyof IPCInvokeEvents> {
|
export interface IPCInvokeHandler<E extends keyof IPCInvokeEvents> {
|
||||||
event: E
|
event: E
|
||||||
handler(data: IPCInvokeEvents[E]['input']): Promise<IPCInvokeEvents[E]['output']> | IPCInvokeEvents[E]['output']
|
handler(data: IPCInvokeEvents[E]['input']): Promise<IPCInvokeEvents[E]['output']> | IPCInvokeEvents[E]['output']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getIPCEmitHandlers(): IPCEmitHandler<keyof IPCEmitEvents>[] {
|
export function getIPCEmitHandlers(): IPCEmitHandler<keyof IPCEmitEvents>[] {
|
||||||
return [
|
return [
|
||||||
new DownloadHandler(),
|
new DownloadHandler(),
|
||||||
new SaveSettingsHandler()
|
new SetSettingsHandler()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of possible async IPC events that don't return values, mapped to their input types.
|
||||||
|
*/
|
||||||
export type IPCEmitEvents = {
|
export type IPCEmitEvents = {
|
||||||
'download': Download
|
'download': Download
|
||||||
'download-updated': DownloadProgress
|
'download-updated': DownloadProgress
|
||||||
'update-settings': Settings
|
'set-settings': Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes an object that handles the `E` async IPC event that will not return a value.
|
||||||
|
*/
|
||||||
export interface IPCEmitHandler<E extends keyof IPCEmitEvents> {
|
export interface IPCEmitHandler<E extends keyof IPCEmitEvents> {
|
||||||
event: E
|
event: E
|
||||||
handler(data: IPCEmitEvents[E]): void
|
handler(data: IPCEmitEvents[E]): void
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Represents Bridge's user settings.
|
||||||
|
*/
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
rateLimitDelay: number // Number of seconds to wait between each file download from Google servers
|
rateLimitDelay: number // Number of seconds to wait between each file download from Google servers
|
||||||
theme: string // The name of the currently enabled UI theme
|
theme: string // The name of the currently enabled UI theme
|
||||||
libraryPath: string // The path to the user's library
|
libraryPath: string // The path to the user's library
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridge's default user settings.
|
||||||
|
*/
|
||||||
export const defaultSettings: Settings = {
|
export const defaultSettings: Settings = {
|
||||||
rateLimitDelay: 31,
|
rateLimitDelay: 31,
|
||||||
theme: 'Default',
|
theme: 'Default',
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
let sanitize = require('sanitize-filename')
|
const sanitize = require('sanitize-filename')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns A random UUID
|
* @returns a random UUID
|
||||||
*/
|
*/
|
||||||
export function generateUUID() { // Public Domain/MIT
|
export function generateUUID() { // Public Domain/MIT
|
||||||
var d = new Date().getTime()//Timestamp
|
let d = new Date().getTime() // Timestamp
|
||||||
var d2 = Date.now() // Time in microseconds since page-load or 0 if unsupported
|
let d2 = Date.now() // Time in microseconds since page-load or 0 if unsupported
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
var r = Math.random() * 16//random number between 0 and 16
|
let r = Math.random() * 16 // Random number between 0 and 16
|
||||||
if (d > 0) { // Use timestamp until depleted
|
if (d > 0) { // Use timestamp until depleted
|
||||||
r = (d + r) % 16 | 0
|
r = (d + r) % 16 | 0
|
||||||
d = Math.floor(d / 16)
|
d = Math.floor(d / 16)
|
||||||
@@ -20,8 +20,7 @@ export function generateUUID() { // Public Domain/MIT
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitizes a filename of any characters that cannot be part of a windows filename.
|
* @returns `filename`, but with any invalid filename characters replaced with similar valid characters.
|
||||||
* @param filename The name of the file to sanitize.
|
|
||||||
*/
|
*/
|
||||||
export function sanitizeFilename(filename: string): string {
|
export function sanitizeFilename(filename: string): string {
|
||||||
const newName = sanitize(filename, {
|
const newName = sanitize(filename, {
|
||||||
@@ -38,16 +37,14 @@ export function sanitizeFilename(filename: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts <val> from the range (<fromA>, <fromB>) to the range (<toA>, <toB>).
|
* Converts `val` from the range (`fromA`, `fromB`) to the range (`toA`, `toB`).
|
||||||
*/
|
*/
|
||||||
export function interpolate(val: number, fromA: number, fromB: number, toA: number, toB: number) {
|
export function interpolate(val: number, fromA: number, fromB: number, toA: number, toB: number) {
|
||||||
return ((val - fromA) / (fromB - fromA)) * (toB - toA) + toA
|
return ((val - fromA) / (fromB - fromA)) * (toB - toA) + toA
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits <objectList> into multiple arrays, grouping by matching <key> values.
|
* @returns `objectList` split into multiple arrays, where each array contains the objects with matching `key` values.
|
||||||
* @param objectList A list of objects.
|
|
||||||
* @param key A key from the list of objects.
|
|
||||||
*/
|
*/
|
||||||
export function groupBy<T>(objectList: T[], key: keyof T) {
|
export function groupBy<T>(objectList: T[], key: keyof T) {
|
||||||
const results: T[][] = []
|
const results: T[][] = []
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Represents a user's request to interact with the download system.
|
||||||
|
*/
|
||||||
export interface Download {
|
export interface Download {
|
||||||
action: 'add' | 'retry' | 'continue' | 'cancel'
|
action: 'add' | 'retry' | 'continue' | 'cancel'
|
||||||
versionID: number
|
versionID: number
|
||||||
data ?: NewDownload
|
data?: NewDownload // Should be defined if action == 'add'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains the data required to start downloading a single chart
|
* Contains the data required to start downloading a single chart.
|
||||||
*/
|
*/
|
||||||
export interface NewDownload {
|
export interface NewDownload {
|
||||||
avTagName: string
|
avTagName: string
|
||||||
@@ -15,7 +18,7 @@ export interface NewDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the download progress of a single chart
|
* Represents the download progress of a single chart.
|
||||||
*/
|
*/
|
||||||
export interface DownloadProgress {
|
export interface DownloadProgress {
|
||||||
versionID: number
|
versionID: number
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Represents a user's song search query.
|
||||||
|
*/
|
||||||
export interface SongSearch {
|
export interface SongSearch {
|
||||||
query: string
|
query: string
|
||||||
type: SearchType
|
type: SearchType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of possible search categories.
|
||||||
|
*/
|
||||||
export enum SearchType {
|
export enum SearchType {
|
||||||
'Any', 'Name', 'Artist', 'Album', 'Genre', 'Year', 'Charter'
|
'Any', 'Name', 'Artist', 'Album', 'Genre', 'Year', 'Charter'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single song search result.
|
||||||
|
*/
|
||||||
export interface SongResult {
|
export interface SongResult {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* The image data for a song's album art.
|
||||||
|
*/
|
||||||
export interface AlbumArtResult {
|
export interface AlbumArtResult {
|
||||||
art: Buffer
|
art: Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single chart version.
|
||||||
|
*/
|
||||||
export interface VersionResult {
|
export interface VersionResult {
|
||||||
versionID: number
|
versionID: number
|
||||||
chartID: number
|
chartID: number
|
||||||
|
|||||||
Reference in New Issue
Block a user