Initial Browse UI and initial song search

This commit is contained in:
Geomitron
2020-02-06 13:21:35 -05:00
parent 8f20311f68
commit e5fd303c91
30 changed files with 616 additions and 44 deletions

View File

@@ -0,0 +1,31 @@
import { IPCHandler } from '../shared/IPCHandler'
import Database from '../shared/Database'
import { SongSearch, SearchType, SongResult } from '../shared/interfaces/search.interface'
import { escape } from 'mysql'
export default class SearchHandler implements IPCHandler<'song-search'> {
event: 'song-search' = 'song-search'
// TODO: add method documentation
async handler(search: SongSearch) {
const db = await Database.getInstance()
return db.sendQuery(this.getSearchQuery(search)) as Promise<SongResult[]>
}
private getSearchQuery(search: SongSearch) {
switch(search.type) {
case SearchType.Any: return this.getGeneralSearchQuery(search.query)
default: return '<<<ERROR>>>' // TODO: add more search types
}
}
private getGeneralSearchQuery(searchString: string) {
return `
SELECT id, name, artist, album, genre, year
FROM Song
WHERE MATCH (name,artist,album,genre) AGAINST (${escape(searchString)}) > 0
LIMIT ${20} OFFSET ${0};
` // TODO: add parameters for the limit and offset
}
}

View File

@@ -1,11 +0,0 @@
import { IPCHandler } from '../shared/IPCHandler'
import { TestInput } from '../shared/interfaces/test.interface'
export default class TestHandler implements IPCHandler<'test-event-A'> {
event = 'test-event-A' as 'test-event-A'
async handler(data: TestInput) {
await new Promise<void>((resolve) => setTimeout(() => resolve(), 3000))
return `Processed data with value1 = ${data.value1} and value2 + 5 = ${data.value2 + 5}`
}
}

View File

@@ -0,0 +1,74 @@
import { Connection, createConnection } from 'mysql'
import { failQuery } from './ErrorMessages'
export default class Database {
// Singleton
private static database: Database
private constructor() { }
static async getInstance() {
if (this.database == undefined) {
this.database = new Database()
}
await this.database.initDatabaseConnection()
return this.database
}
private conn: Connection
/**
* Constructs a database connection to the chartmanager database.
*/
private async initDatabaseConnection() {
this.conn = createConnection({
host: 'chartmanager.cdtrqnlcxz86.us-east-1.rds.amazonaws.com',
port: 3306,
user: 'standarduser',
password: 'E4OZXWDPiX9exUpMhcQq', // Note: this login is read-only
database: 'chartmanagerdatabase',
multipleStatements: true,
charset: 'utf8mb4',
typeCast: (field, next) => { // Convert 1/0 to true/false
if (field.type == 'TINY' && field.length == 1) {
return (field.string() == '1') // 1 = true, 0 = false
} else {
return next()
}
}
})
return new Promise<void>((resolve, reject) => {
this.conn.connect(err => {
if (err) {
reject(`Failed to connect to database: ${err}`)
return
} else {
resolve()
}
})
})
}
/**
* Sends <query> to the database.
* @param query The query string to be sent.
* @param queryStatement The nth response statement to be returned.
* @returns one of the responses as type <ResponseType[]>, or an empty array if the query fails.
*/
async sendQuery<ResponseType>(query: string, queryStatement?: number) {
return new Promise<ResponseType[]>(resolve => {
this.conn.query(query, (err, results) => {
if (err) {
failQuery(query, err)
resolve([])
return
}
if (queryStatement !== undefined) {
resolve(results[queryStatement - 1])
} else {
resolve(results)
}
})
})
}
}

View File

@@ -0,0 +1,124 @@
import { red } from 'cli-color'
import { getRelativeFilepath } from './UtilFunctions'
// TODO: add better error reporting (through the UI)
/**
* Displays an error message for reading files in the song folder
*/
export function failReadRelative(filepath: string, error: any) {
console.error(`${red('ERROR:')} Failed to read files inside song folder (${getRelativeFilepath(filepath)}):\n${error}`)
}
/**
* Displays an error message for reading files
*/
export function failRead(filepath: string, error: any) {
console.error(`${red('ERROR:')} Failed to read files inside song folder (${filepath}):\n${error}`)
}
/**
* Displays an error message for writing files
*/
export function failWrite(filepath: string, error: any) {
console.error(`${red('ERROR:')} Failed to write to file (${getRelativeFilepath(filepath)}):\n${error}`)
}
/**
* Displays an error message for opening files
*/
export function failOpen(filepath: string, error: any) {
console.error(`${red('ERROR:')} Failed to open file (${getRelativeFilepath(filepath)}):\n${error}`)
}
/**
* Displays an error message for deleting folders
*/
export function failDelete(filepath: string, error: any) {
console.error(`${red('ERROR:')} Failed to delete folder (${getRelativeFilepath(filepath)}):\n${error}`)
}
/**
* Displays an error message for opening text files
*/
export function failEncoding(filepath: string, error: any) {
console.error(`${red('ERROR:')} Failed to read text file (${getRelativeFilepath(filepath)
}):\nJavaScript cannot parse using the detected text encoding of (${error})`)
}
/**
* Displays an error message for failing to parse an .ini file
*/
export function failParse(filepath: string, error: any) {
console.error(`${red('ERROR:')} Failed to parse ini file (${getRelativeFilepath(filepath)}):\n${error}`)
}
/**
* Displays an error message for processing a query
*/
export function failQuery(query: string, error: any) {
console.error(`${red('ERROR:')} Failed to execute query:\n${query}\nWith error:\n${error}`)
}
/**
* Displays an error message for connecting to the database
*/
export function failDatabase(error: any) {
console.error(`${red('ERROR:')} Failed to connect to database:\n${error}`)
}
/**
* Displays an error message for connecting to the database
*/
export function failTimeout(type: string, url: string, maxAttempts: number) {
console.error(`${red('ERROR:')} Failed to connect to download server at (\n${url}) after ${maxAttempts} retry attempts. [type=${type}]`)
}
/**
* Displays an error message for connecting to the database
*/
export function failResponse(statusCode: string, url: string) {
console.error(`${red('ERROR:')} Failed to connect to download server at (\n${url}) [statusCode=${statusCode}]`)
}
/**
* Displays an error message for processing audio files
*/
export function failFFMPEG(audioFile: string, error: any) {
console.error(`${red('ERROR:')} Failed to process audio file (${getRelativeFilepath(audioFile)}):\n${error}`)
}
/**
* Displays an error message for failing to create multiple threads
*/
export function failMultithread(error: any) {
console.error(`${red('ERROR:')} Failed to create multiple threads:\n${error}`)
}
/**
* Displays an error message for replacing files
*/
export function failReplace(filepath: string, error: any) {
console.error(`${red('ERROR:')} Failed to rewrite file (${getRelativeFilepath(filepath)}):\n${error}`)
}
/**
* Displays an error message for downloading charts
*/
export function failDownload(error: any) {
console.error(`${red('ERROR:')} Failed to download chart:\n${error}`)
}
/**
* Displays an error message for reading files
*/
export function failScan(filepath: string) {
console.error(`${red('ERROR:')} The specified library folder contains no files (${filepath})`)
}
/**
* Displays an error message for failing to unzip an archived file
*/
export function failUnzip(filepath: string, error: any) {
console.error(`${red('ERROR:')} Failed to extract archive at (${filepath}):\n${error}`)
}

View File

@@ -1,5 +1,5 @@
import TestHandler from '../ipc/TestHandler.ipc'
import { TestInput } from './interfaces/test.interface'
import SearchHandler from '../ipc/SearchHandler.ipc'
import { SongSearch, SongResult } from './interfaces/search.interface'
/**
* To add a new IPC listener:
@@ -11,14 +11,14 @@ import { TestInput } from './interfaces/test.interface'
export function getIPCHandlers(): IPCHandler<keyof IPCEvents>[] {
return [
new TestHandler()
new SearchHandler()
]
}
export type IPCEvents = {
['test-event-A']: {
input: TestInput
output: string
['song-search']: {
input: SongSearch
output: SongResult[]
}
['test-event-B']: {
input: number

View File

@@ -0,0 +1,19 @@
export class Settings {
// Singleton
private constructor() { }
private static settings: Settings
static async getInstance() {
if (this.settings == undefined) {
this.settings = new Settings()
}
await this.settings.initSettings()
return this.settings
}
songsFolderPath: string
private async initSettings() {
// TODO: load settings from settings file or set defaults
}
}

View File

@@ -0,0 +1,15 @@
import { basename } from 'path'
import { Settings } from './Settings'
const settings = Settings.getInstance()
/**
* @param absoluteFilepath The absolute filepath to a folder
* @returns The relative filepath from the scanned folder to <absoluteFilepath>
*/
export function getRelativeFilepath(absoluteFilepath: string) {
// TODO: figure out how these functions should use <settings> (like an async initialization script that
// loads everything and connects to the database, etc...)
// return basename(scanSettings.songsFolderPath) + absoluteFilepath.substring(scanSettings.songsFolderPath.length)
return absoluteFilepath
}

View File

@@ -0,0 +1,17 @@
export interface SongSearch {
query: string
type: SearchType
}
export enum SearchType {
'Any', 'Name', 'Artist', 'Album', 'Genre', 'Year', 'Charter'
}
export interface SongResult {
id: number
name: string
artist: string
album: string
genre: string
year: string
}

View File

@@ -1,4 +0,0 @@
export interface TestInput {
value1: string
value2: number
}