diff --git a/.gitignore b/.gitignore index 1f1e3da..a391d89 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +extraResources todo conf.json src/Bot/conf.js diff --git a/.prettierignore b/.prettierignore index 132a4e8..c2dd120 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,5 +2,4 @@ dist build node_modules assets -config scripts \ No newline at end of file diff --git a/config/main.webpack.config.js b/config/main.webpack.config.js index 4bdee86..e18841c 100644 --- a/config/main.webpack.config.js +++ b/config/main.webpack.config.js @@ -1,58 +1,56 @@ +const path = require('path'); const paths = require('./paths'); const webpack = require('webpack'); const getClientEnvironment = require('./env'); - module.exports = mode => { const env = getClientEnvironment('/'); return { - target: "electron-main", + target: 'electron-main', mode, entry: { main: paths.electronIndexJs, osuSongsScan: paths.osuSongsScan, - osuIsRunning: paths.osuIsRunning + osuIsRunning: paths.osuIsRunning, }, output: { path: paths.appBuild, publicPath: paths.publicUrl, - filename: '[name].bundle.js' + filename: '[name].bundle.js', }, node: { - __dirname: false + __dirname: false, }, module: { - rules : [{ - test: /\.(js|mjs|jsx|ts|tsx)$/, - include: paths.appSrc, - loader: require.resolve('babel-loader'), - options: { - sourceType: 'unambiguous', - presets: [ - '@babel/preset-env' - ], - } - }, - { - test: /ipcMessages.js$/, - loader: 'string-replace-loader', - options:{ - multiple: [ - { - search: './processes/osuSongsScan.js', - replace: './osuSongsScan.bundle.js' - }, - { - search: './processes/osuIsRunning.js', - replace: './osuIsRunning.bundle.js' - } - ] - } - }] + rules: [ + { + test: /\.(js|mjs|jsx|ts|tsx)$/, + include: paths.appSrc, + loader: require.resolve('babel-loader'), + options: { + sourceType: 'unambiguous', + presets: ['@babel/preset-env'], + }, + }, + { + test: /threads.*\.js$/, + loader: 'string-replace-loader', + options: { + multiple: [ + { + search: './osuSongsScan.worker.js', + replace: '../../extraResources/osuSongsScan.bundle.js', + }, + { + search: './osuIsRunning.worker.js', + replace: '../../extraResources/osuIsRunning.bundle.js', + }, + ], + }, + }, + ], }, - plugins: [ - new webpack.DefinePlugin(env.stringified), - ], - } -}; \ No newline at end of file + plugins: [new webpack.DefinePlugin(env.stringified)], + }; +}; diff --git a/config/paths.js b/config/paths.js index 496716e..3e15e44 100644 --- a/config/paths.js +++ b/config/paths.js @@ -22,8 +22,7 @@ function ensureSlash(inputPath, needsSlash) { } } -const getPublicUrl = appPackageJson => - envPublicUrl || require(appPackageJson).homepage; +const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage; // We use `PUBLIC_URL` environment variable or "homepage" field to infer // "public path" at which the app is served. @@ -33,8 +32,7 @@ const getPublicUrl = appPackageJson => // like /todos/42/static/js/bundle.7289d.js. We have to know the root. function getServedPath(appPackageJson) { const publicUrl = getPublicUrl(appPackageJson); - const servedUrl = - envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/'); + const servedUrl = envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/'); return ensureSlash(servedUrl, true); } @@ -54,9 +52,7 @@ const moduleFileExtensions = [ // Resolve file paths in the same order as webpack const resolveModule = (resolveFn, filePath) => { - const extension = moduleFileExtensions.find(extension => - fs.existsSync(resolveFn(`${filePath}.${extension}`)) - ); + const extension = moduleFileExtensions.find(extension => fs.existsSync(resolveFn(`${filePath}.${extension}`))); if (extension) { return resolveFn(`${filePath}.${extension}`); @@ -85,10 +81,8 @@ module.exports = { publicUrl: getPublicUrl(resolveApp('package.json')), servedPath: getServedPath(resolveApp('package.json')), rendererWebpackConfig: resolveApp('config/renderer.webpack.config.js'), - osuSongsScan: resolveApp('./src/electron/processes/osuSongsScan.js'), - osuIsRunning: resolveApp('./src/electron/processes/osuIsRunning.js') + osuSongsScan: resolveApp('./src/electron/threads/osuSongsScan.worker.js'), + osuIsRunning: resolveApp('./src/electron/threads/osuIsRunning.worker.js'), }; - - module.exports.moduleFileExtensions = moduleFileExtensions; diff --git a/package.json b/package.json index 9650c15..bedee96 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,7 @@ "dev": "node scripts/start.js", "start": "electron ./build/main.bundle.js", "dev:win": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./scripts/start.ps1", - "prebuild": "node scripts/preBuild.js", - "build": "node scripts/build.js", + "build": "node scripts/preBuild.js && node scripts/build.js && node scripts/postBuild.js", "test": "node scripts/test.js", "lint": "yarn lint:prettier && yarn lint:es", "lint:fix": "yarn lint:fix:prettier && yarn lint:fix:es", @@ -46,6 +45,9 @@ }, "build": { "appId": "io.beatconnect.client", + "extraResources": [ + "./extraResources/**" + ], "publish": [ { "provider": "github", @@ -232,4 +234,4 @@ "react-app" ] } -} \ No newline at end of file +} diff --git a/scripts/postBuild.js b/scripts/postBuild.js new file mode 100644 index 0000000..f908c8f --- /dev/null +++ b/scripts/postBuild.js @@ -0,0 +1,16 @@ +const { join } = require('path') +const { copyFileSync, emptyDirSync } = require('fs-extra') +const paths = require('../config/paths'); +// Copy wallpaper binaries to public folder on build + + + +const sources = [ + '../build/osuIsRunning.bundle.js', + '../build/osuSongsScan.bundle.js', +] + +const destFolder = join(__dirname, '..', 'extraResources'); +emptyDirSync(destFolder); + +sources.forEach(src => copyFileSync(join(__dirname,src), join(destFolder, src.split('/').pop()))) \ No newline at end of file diff --git a/src/App/modules/Settings/utils/useScanOsuSongs.js b/src/App/modules/Settings/utils/useScanOsuSongs.js index 8d2dddd..f0c9075 100644 --- a/src/App/modules/Settings/utils/useScanOsuSongs.js +++ b/src/App/modules/Settings/utils/useScanOsuSongs.js @@ -1,3 +1,5 @@ +/* eslint-disable no-alert */ +/* eslint-disable no-console */ import { ipcRenderer } from 'electron'; import { error } from 'electron-log'; import { useEffect, useState } from 'react'; @@ -11,38 +13,28 @@ import { scanOsuCollection } from './scanOsuCollections'; export const useOsuDbScan = () => { const osuSongsPath = useSelector(getOsuSongPath); const osuPath = useSelector(getOsuPath); - const { add: addTask, update, terminate } = useTasks(); + const { add: addTask, terminate } = useTasks(); const history = useDownloadHistory(); const [isScanning, setIsScanning] = useState(false); - const scanOsuSongs = () => { + const scanOsuSongs = async () => { if (isScanning) return; if (!osuPath && !osuSongsPath) { - return alert('You need to select your osu! or songs folder before performing a scan'); + alert('You need to select your osu! or songs folder before performing a scan'); + return; } setIsScanning(true); addTask({ name: 'Scanning beatmaps', status: 'running', description: '', section: 'Settings' }); - ipcRenderer.send('osuSongsScan', { osuPath, osuSongsPath, allowLegacy: true }); // User osu folder path - ipcRenderer.on('osuSongsScanStatus', (e, args) => { - update({ - name: 'Scanning beatmaps', - description: `${Math.round(args * 100)}%`, - }); - }); - ipcRenderer.on('osuSongsScanResults', (e, args) => { - terminate('Scanning beatmaps'); - setIsScanning(false); - if (args.err) error(`Error while scannings song: ${args.err}`); - else { - history.set(args); - setLastScan({ date: Date.now(), beatmaps: Object.keys(args.beatmaps).length }); - } - }); - ipcRenderer.on('osuSongsScanError', (e, args) => { - terminate('Scanning beatmaps'); - setIsScanning(false); - alert('Failed to scan beatmaps, check your songs and osu! path in settings section'); - }); + const result = await ipcRenderer.invoke('osuSongsScan', { osuPath }); + + terminate('Scanning beatmaps'); + setIsScanning(false); + if (result.error) { + throw new Error(`Error while scannings song: ${result.error}`); + } else { + history.set(result); + setLastScan({ date: Date.now(), beatmaps: Object.keys(result.beatmaps).length }); + } }; return scanOsuSongs; @@ -54,8 +46,13 @@ export const useOsuDbAutoScan = () => { const osuPath = useSelector(getOsuPath); useEffect(() => { if (osuPath && osuSongsPath !== '') { - osuDbScan(); - scanOsuCollection(osuPath).then(console.log); + osuDbScan() + .then(() => console.log('Osu db scan success!')) + .catch(err => { + error(`Error while scannings song: ${err.message}`); + alert('Failed to scan beatmaps, check your songs and osu! path in settings section'); + }); + scanOsuCollection(osuPath).then(() => console.log('Collection scan success!')); } }, []); }; diff --git a/src/electron/ipcMessages.js b/src/electron/ipcMessages.js index 2e261b5..1c21f1a 100644 --- a/src/electron/ipcMessages.js +++ b/src/electron/ipcMessages.js @@ -2,27 +2,18 @@ const log = require('electron-log'); const { error } = require('electron-log'); const { ipcMain, dialog, shell } = require('electron'); const { join } = require('path'); -const { fork } = require('child_process'); const { downloadAndSetWallpaper } = require('./wallpaper'); const { readCollectionDB } = require('./helpers/osuCollections/collections.utils'); +const startPullingOsuState = require('./threads/osuIsRunning'); +const scanOsuDb = require('./threads/osuSongsScan'); -ipcMain.on('osuSongsScan', (event, options) => { - // TODO Replace with osu-db-parser module - const osuSongsScanProcess = fork(join(__dirname, './processes/osuSongsScan.js'), null, { silent: true }); - osuSongsScanProcess.stdout.pipe(process.stdout); - osuSongsScanProcess.send(JSON.stringify({ msg: 'start', ...options })); - osuSongsScanProcess.on('message', msg => { - const { results, status, err, overallDuration, overallUnplayedCount } = JSON.parse(msg); - if (results) { - event.reply('osuSongsScanResults', { beatmaps: results, overallDuration, overallUnplayedCount }); - osuSongsScanProcess.kill('SIGTERM'); - } - if (status) event.reply('osuSongsScanStatus', status); - if (err) { - event.reply('osuSongsScanError', err); - osuSongsScanProcess.kill('SIGTERM'); - } - }); +ipcMain.handle('osuSongsScan', async (event, { osuPath }) => { + try { + const [beatmaps, overallDuration, overallUnplayedCount] = await scanOsuDb(`${osuPath}/osu!.db`); + return { beatmaps, overallDuration, overallUnplayedCount }; + } catch (e) { + return { error: e.message }; + } }); ipcMain.on('set-wallpaper', (event, bgUri) => { @@ -44,11 +35,10 @@ ipcMain.on('set-wallpaper', (event, bgUri) => { ipcMain.on('start-osu', (event, osuPath) => shell.openPath(join(osuPath, 'osu!.exe')).catch(error)); ipcMain.once('start-pulling-osu-state', event => { - const osuIsRunningChecker = fork(join(__dirname, './processes/osuIsRunning.js')); - osuIsRunningChecker.on('message', msg => { - event.reply('osu-is-running', !!msg); - }); - osuIsRunningChecker.send('start'); + const osuStateHandler = isRunning => { + event.reply('osu-is-running', isRunning); + }; + startPullingOsuState(osuStateHandler); }); ipcMain.handle('scan-osu-collections', async (event, osuPath) => { diff --git a/src/electron/processes/osuDbReader.re b/src/electron/processes/osuDbReader.re deleted file mode 100644 index 6b58213..0000000 --- a/src/electron/processes/osuDbReader.re +++ /dev/null @@ -1,31 +0,0 @@ -type callback('a) = (Js.Nullable.t(Js.Exn.t), 'a) => unit; - -[@bs.module "fs"] -external readFile: (string, callback(Buffer.t)) => unit = "readFile"; -[@bs.module "fs"] -external writeFile: (string, Buffer.t, callback(string)) => unit = - "writeFile"; -[@bs.module "process"] external on: (string, string => unit) => unit = "on"; -[@bs.module "process"] external send: string => unit = "send"; - -let osuDb = - readFile("/Users/yannis/Downloads/osu!.db", (err, buffer) => - switch (Js.Nullable.toOption(err)) { - | Some(err) => Js.log(err) - | None => Js.log(OsuDbParser.read(buffer)) - } - ); - -// let handleMessage = (message) => -// switch (message) { -// | "start" => "Yep" -// | _ => -// }; - -// on("message") - -// TODO Handle incoming messages from main -// Send messages to main -// Handle those messages in main -// Send back datas from main to renderer -// All of this on reason via shared channel polymorphic variants and JSON converters \ No newline at end of file diff --git a/src/electron/processes/osuIsRunning.js b/src/electron/processes/osuIsRunning.js deleted file mode 100644 index c4b6c45..0000000 --- a/src/electron/processes/osuIsRunning.js +++ /dev/null @@ -1,27 +0,0 @@ -const { lookup } = require('ps-node'); - -const osuIsRuning = () => { - lookup( - { - command: 'osu!.exe', - }, - (err, resultList) => { - if (err) { - throw new Error(err); - } - process.send(resultList.length); - }, - ); -}; - -let pullIntervalId = null; -process.on('message', msg => { - if (msg === 'start') { - pullIntervalId = setInterval(osuIsRuning, 15000); - } -}); - -process.once('SIGTERM', () => { - process.removeAllListeners(); - if (pullIntervalId) clearInterval(pullIntervalId); -}); diff --git a/src/electron/processes/osuSongsScan.js b/src/electron/processes/osuSongsScan.js deleted file mode 100644 index 1ebaf6b..0000000 --- a/src/electron/processes/osuSongsScan.js +++ /dev/null @@ -1,94 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const { join } = require('path'); -const { log } = require('electron-log'); -const parser = require('../helpers/beatmapParser'); -const { readOsuDB, winTickToMs } = require('../helpers/osudb'); - -const osuSongsScan = songsDirectoryPath => - new Promise((resolve, reject) => { - try { - const output = {}; - const beatmaps = fs.readdirSync(songsDirectoryPath); - const beatmapsCount = beatmaps.length; - beatmaps.forEach((beatmap, i) => { - if (i % 50 === 0) { - const progress = (i / beatmapsCount).toFixed(2); - process.send(JSON.stringify({ status: progress })); - } - const beatmapPath = path.join(songsDirectoryPath, beatmap); - const dirStats = fs.lstatSync(beatmapPath); - const isDirExists = fs.existsSync(beatmapPath) && dirStats.isDirectory(); - if (isDirExists) { - const date = dirStats.mtimeMs; - const assets = fs.readdirSync(beatmapPath); - for (let j = 0; j < assets.length; j++) { - if (assets[j].split('.').pop() === 'osu') { - const data = fs.readFileSync(path.join(beatmapPath, assets[j]), 'utf8'); - const { Metadata } = parser(data); - if (!(typeof Metadata === 'undefined')) { - const { BeatmapSetID, Title, Artist } = Metadata; - if (BeatmapSetID && BeatmapSetID !== '-1' && BeatmapSetID !== '0') - output[BeatmapSetID] = { id: BeatmapSetID, name: `${Title} | ${Artist}`, date }; - break; - } - } - } - } - }); - resolve(output); - } catch (err) { - reject(err); - } - }); - -const osuDbScan = osuPath => { - const re = readOsuDB(`${osuPath}/osu!.db`); - const out = {}; - let overallDuration = 0; - let overallUnplayedCount = 0; - re.beatmaps.forEach(beatmap => { - if (beatmap.beatmapset_id === -1) return; - if (out[beatmap.beatmapset_id]) return; - if (beatmap.unplayed) overallUnplayedCount += 1; - overallDuration += beatmap.total_time; - out[beatmap.beatmapset_id] = { - id: beatmap.beatmapset_id, - date: winTickToMs(beatmap.last_modification_time), - title: beatmap.song_title, - artist: beatmap.artist_name, - creator: beatmap.creator_name, - isUnplayed: beatmap.unplayed, - md5: beatmap.md5, - audioPath: join(beatmap.folder_name, beatmap.audio_file_name), - previewOffset: beatmap.preview_offset, - }; - }); - - return [out, overallDuration, overallUnplayedCount]; -}; - -process.on('message', async data => { - const { msg, osuPath, osuSongsPath, allowLegacy } = JSON.parse(data); - let beatmaps = []; - let overallDuration = 0; - let overallUnplayedCount = 0; - switch (msg) { - case 'start': - try { - if (osuPath) { - [beatmaps, overallDuration, overallUnplayedCount] = osuDbScan(osuPath); - } - // Fallback to direcrory scan if failed to read osu db - if (!Object.keys(beatmaps).length && allowLegacy) beatmaps = await osuSongsScan(osuSongsPath); - } catch (err) { - log.error(`OsuSongScan: ${JSON.stringify(err.message)}`); - process.send(JSON.stringify({ err: err.message })); - throw err; - } - process.send(JSON.stringify({ results: beatmaps, overallDuration, overallUnplayedCount })); - break; - default: - break; - } -}); diff --git a/src/electron/threads/osuIsRunning.js b/src/electron/threads/osuIsRunning.js new file mode 100644 index 0000000..7ed8809 --- /dev/null +++ b/src/electron/threads/osuIsRunning.js @@ -0,0 +1,34 @@ +const { Worker } = require('worker_threads'); +const { error } = require('electron-log'); +const { join } = require('path'); + +let worker; +let pullIntervalId; +const startPullingOsuState = handler => { + if (worker && pullIntervalId) { + throw new Error('startPullingOsuState was already called and is started to end it call the returned stop function'); + } + worker = new Worker(join(__dirname, './osuIsRunning.worker.js')); + pullIntervalId = setInterval(() => worker.postMessage('check-osu-state'), 10000); + worker.on('message', data => { + switch (data[0]) { + case 'result': + handler(data[1]); + break; + case 'error': + error(`[osuIsRunning thread]: ${data[1]}`); + break; + default: + break; + } + }); + const stopPullingOsuState = () => { + clearInterval(pullIntervalId); + worker.removeAllListeners(); + worker.terminate(); + }; + + return stopPullingOsuState; +}; + +module.exports = startPullingOsuState; diff --git a/src/electron/threads/osuIsRunning.worker.js b/src/electron/threads/osuIsRunning.worker.js new file mode 100644 index 0000000..b4aedf6 --- /dev/null +++ b/src/electron/threads/osuIsRunning.worker.js @@ -0,0 +1,22 @@ +const { parentPort } = require('worker_threads'); +const { lookup } = require('ps-node'); + +let isBusy = false; +parentPort.on('message', data => { + if (data === 'check-osu-state' && !isBusy) { + isBusy = true; + lookup( + { + command: 'osu!.exe', + }, + (err, resultList) => { + if (err) { + parentPort.postMessage(['error', err.message]); + } else { + parentPort.postMessage(['result', !!resultList.length]); + } + isBusy = false; + }, + ); + } +}); diff --git a/src/electron/threads/osuSongsScan.js b/src/electron/threads/osuSongsScan.js new file mode 100644 index 0000000..91cae35 --- /dev/null +++ b/src/electron/threads/osuSongsScan.js @@ -0,0 +1,31 @@ +const { Worker } = require('worker_threads'); +const { join } = require('path'); +const { error } = require('electron-log'); + +const scanOsuDb = osuDbPath => + new Promise((resolve, reject) => { + const worker = new Worker(join(__dirname, './osuSongsScan.worker.js')); + const terminate = () => { + worker.removeAllListeners(); + worker.terminate(); + }; + worker.on('message', data => { + switch (data[0]) { + case 'result': + terminate(); + resolve(data[1]); + break; + case 'error': + terminate(); + error(`[scanOsuDb thread]: ${data[1]}`); + reject(data[1]); + break; + default: + terminate(); + break; + } + }); + worker.postMessage(osuDbPath); + }); + +module.exports = scanOsuDb; diff --git a/src/electron/threads/osuSongsScan.worker.js b/src/electron/threads/osuSongsScan.worker.js new file mode 100644 index 0000000..3089922 --- /dev/null +++ b/src/electron/threads/osuSongsScan.worker.js @@ -0,0 +1,34 @@ +const { parentPort } = require('worker_threads'); +const { join } = require('path'); +const { readOsuDB, winTickToMs } = require('../helpers/osudb'); + +parentPort.on('message', osuDbPath => { + if (osuDbPath) { + try { + const re = readOsuDB(osuDbPath); + const beatmaps = {}; + let overallDuration = 0; + let overallUnplayedCount = 0; + re.beatmaps.forEach(beatmap => { + if (beatmap.beatmapset_id === -1) return; + if (beatmaps[beatmap.beatmapset_id]) return; + if (beatmap.unplayed) overallUnplayedCount += 1; + overallDuration += beatmap.total_time; + beatmaps[beatmap.beatmapset_id] = { + id: beatmap.beatmapset_id, + date: winTickToMs(beatmap.last_modification_time), + title: beatmap.song_title, + artist: beatmap.artist_name, + creator: beatmap.creator_name, + isUnplayed: beatmap.unplayed, + md5: beatmap.md5, + audioPath: join(beatmap.folder_name, beatmap.audio_file_name), + previewOffset: beatmap.preview_offset, + }; + }); + parentPort.postMessage(['result', [beatmaps, overallDuration, overallUnplayedCount]]); + } catch (e) { + parentPort.postMessage(['error', e.message]); + } + } +});