diff --git a/.gitignore b/.gitignore index 271c1c0bb7..c95bd90f3d 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,4 @@ build-dir #Travis/IDE stuff .travis.yml .vs/ +target/* \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..5b8adf3843 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,284 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "bitflags" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "id3" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9389dd9c8c4671b1e4b2878a6329bccb573f9c24a75bc91c641c451ce5436501" +dependencies = [ + "bitflags", + "byteorder", + "flate2", +] + +[[package]] +name = "libc" +version = "0.2.146" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[package]] +name = "libloading" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "neon" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28e15415261d880aed48122e917a45e87bb82cf0260bb6db48bbab44b7464373" +dependencies = [ + "neon-build", + "neon-macros", + "neon-runtime", + "semver", + "smallvec", +] + +[[package]] +name = "neon-build" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bac98a702e71804af3dacfde41edde4a16076a7bbe889ae61e56e18c5b1c811" + +[[package]] +name = "neon-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7288eac8b54af7913c60e0eb0e2a7683020dffa342ab3fd15e28f035ba897cf" +dependencies = [ + "quote", + "syn", + "syn-mid", +] + +[[package]] +name = "neon-runtime" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676720fa8bb32c64c3d9f49c47a47289239ec46b4bdb66d0913cc512cb0daca" +dependencies = [ + "cfg-if", + "libloading", + "smallvec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "scanner" +version = "0.1.0" +dependencies = [ + "id3", + "neon", + "uuid", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-mid" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa8e7560a164edb1621a55d18a0c59abf49d360f47aa7b821061dd7eea7fac9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "uuid" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" +dependencies = [ + "getrandom", + "rand", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..aeae537396 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["packages/scanner"] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1cd9698d16..c3070177d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4969,6 +4969,10 @@ "resolved": "packages/main", "link": true }, + "node_modules/@nuclear/scanner": { + "resolved": "packages/scanner", + "link": true + }, "node_modules/@nuclear/ui": { "resolved": "packages/ui", "link": true @@ -16815,6 +16819,15 @@ } ] }, + "node_modules/cargo-cp-artifact": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.8.tgz", + "integrity": "sha512-3j4DaoTrsCD1MRkTF2Soacii0Nx7UHCce0EwUf4fHnggwiE4fbmF2AbnfzayR36DF8KGadfh7M/Yfy625kgPlA==", + "dev": true, + "bin": { + "cargo-cp-artifact": "bin/cargo-cp-artifact.js" + } + }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", @@ -46373,6 +46386,7 @@ "license": "AGPL-3.0", "dependencies": { "@nuclear/core": "^0.6.30", + "@nuclear/scanner": "^0.6.21", "autobind-decorator": "^2.4.0", "body-parser": "^1.19.0", "concat-stream": "^2.0.0", @@ -46463,6 +46477,14 @@ "uuid": "bin/uuid" } }, + "packages/scanner": { + "version": "0.6.21", + "hasInstallScript": true, + "license": "AGPL-3.0", + "devDependencies": { + "cargo-cp-artifact": "^0.1" + } + }, "packages/ui": { "name": "@nuclear/ui", "version": "0.6.30", @@ -50122,7 +50144,7 @@ "react-full-screen": "^1.0.1", "react-hifi": "^2.2.1", "react-hls-player": "^3.0.1", - "react-hot-loader": "^4.12", + "react-hot-loader": "^4.13.1", "react-i18next": "^11.12.0", "react-image": "^2.2.2", "react-list": "^0.8.13", @@ -50211,6 +50233,7 @@ "version": "file:packages/main", "requires": { "@nuclear/core": "^0.6.30", + "@nuclear/scanner": "^0.6.21", "@types/body-parser": "^1.17.1", "@types/concat-stream": "^1.6.0", "@types/cors": "^2.8.6", @@ -50295,6 +50318,12 @@ } } }, + "@nuclear/scanner": { + "version": "file:packages/scanner", + "requires": { + "cargo-cp-artifact": "^0.1" + } + }, "@nuclear/ui": { "version": "file:packages/ui", "requires": { @@ -59609,6 +59638,12 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz", "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==" }, + "cargo-cp-artifact": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.8.tgz", + "integrity": "sha512-3j4DaoTrsCD1MRkTF2Soacii0Nx7UHCce0EwUf4fHnggwiE4fbmF2AbnfzayR36DF8KGadfh7M/Yfy625kgPlA==", + "dev": true + }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", diff --git a/package.json b/package.json index 654976f0c4..21ad36e460 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "homepage": "https://github.com/nukeop/nuclear#readme", "scripts": { - "postinstall": "electron-builder install-app-deps", + "postinstall": "electron-builder install-app-deps && lerna run build --scope @nuclear/scanner", "start": "lerna run start --stream", "build": "shx rm -rf dist && lerna run build && npm run pack", "test": "lerna run test", diff --git a/packages/main/package.json b/packages/main/package.json index de4c1c78c9..bf52389973 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -21,6 +21,7 @@ "homepage": "https://github.com/nukeop/nuclear#readme", "dependencies": { "@nuclear/core": "^0.6.30", + "@nuclear/scanner": "^0.6.21", "autobind-decorator": "^2.4.0", "body-parser": "^1.19.0", "concat-stream": "^2.0.0", diff --git a/packages/main/src/controllers/local-library.ts b/packages/main/src/controllers/local-library.ts index a4ca27dffa..84b98eb28b 100644 --- a/packages/main/src/controllers/local-library.ts +++ b/packages/main/src/controllers/local-library.ts @@ -1,12 +1,13 @@ import { inject } from 'inversify'; import { IpcMessageEvent } from 'electron'; +import { IpcEvents } from '@nuclear/core'; +import {scanFolders} from '@nuclear/scanner'; import LocalLibrary from '../services/local-library'; import { ipcController, ipcEvent } from '../utils/decorators'; import LocalLibraryDb from '../services/local-library/db'; import Platform from '../services/platform'; import Window from '../services/window'; -import { IpcEvents } from '@nuclear/core'; @ipcController() class LocalIpcCtrl { @@ -48,12 +49,12 @@ class LocalIpcCtrl { */ @ipcEvent(IpcEvents.LOCALFOLDERS_SET) async setLocalFolders(event: IpcMessageEvent, directories: string[]) { - const localFolders = await Promise.all( + await Promise.all( directories .map(folder => this.localLibraryDb.addFolder(this.normalizeFolderPath(folder))) ); - const cache = await this.localLibrary.scanFoldersAndGetMeta(localFolders, (scanProgress, scanTotal) => { + const cache = await scanFolders(directories, ['mp3'], (scanProgress, scanTotal) => { this.window.send(IpcEvents.LOCAL_FILES_PROGRESS, {scanProgress, scanTotal}); }); @@ -82,12 +83,10 @@ class LocalIpcCtrl { async onRefreshLocalFolders() { try { const folders = await this.localLibraryDb.getLocalFolders(); - const cache = await this.localLibrary.scanFoldersAndGetMeta( - folders, - (scanProgress, scanTotal) => { - this.window.send(IpcEvents.LOCAL_FILES_PROGRESS, {scanProgress, scanTotal}); - } - ); + + const cache = await scanFolders(folders.map(folder => folder.path), ['mp3'], (scanProgress, scanTotal) => { + this.window.send(IpcEvents.LOCAL_FILES_PROGRESS, {scanProgress, scanTotal}); + }); this.window.send(IpcEvents.LOCAL_FILES, cache); } catch (err) { diff --git a/packages/main/src/services/library-scanner/library-scanner.test.ts b/packages/main/src/services/library-scanner/library-scanner.test.ts new file mode 100644 index 0000000000..a803a3e732 --- /dev/null +++ b/packages/main/src/services/library-scanner/library-scanner.test.ts @@ -0,0 +1,10 @@ +import {scanFolders} from '@nuclear/scanner'; + +describe('Local library scanner', () => { + it('scans folders', async () => { + const result = await scanFolders([''], ['mp3'], (progress, total, lastScanned) => { + // console.log({progress, total, lastScanned}); + }); + expect(result).toBe({}); + }); +}); diff --git a/packages/main/tsconfig.json b/packages/main/tsconfig.json index fa6acfa1cb..b56c78a0c9 100644 --- a/packages/main/tsconfig.json +++ b/packages/main/tsconfig.json @@ -15,7 +15,8 @@ "src", "typings", "node_modules/@nuclear", - "../core/src" + "../core/src", + "../scanner" ], "exclude": [ "node_modules/@nuclear/*/test", diff --git a/packages/main/webpack.config.ts b/packages/main/webpack.config.ts index 28bb387932..4f3a02cec1 100644 --- a/packages/main/webpack.config.ts +++ b/packages/main/webpack.config.ts @@ -16,6 +16,8 @@ const osMapper: Record = { const MAIN_DIR = path.resolve(__dirname, 'src'); const CORE_DIR = path.resolve(__dirname, '..', '..', 'node_modules', '@nuclear', 'core', 'src'); +const SCANNER_DIR = path.resolve(__dirname, '..', '..', 'node_modules', '@nuclear', 'scanner'); +const SCANNER_DIR_SYMLINKED = path.resolve(__dirname, 'node_modules', '@nuclear', 'scanner'); module.exports = (env: BuildEnv): webpack.Configuration => { if (!env.TARGET) { @@ -28,7 +30,7 @@ module.exports = (env: BuildEnv): webpack.Configuration => { return { entry: './src/main.ts', resolve: { - extensions: ['.ts', '.js', '.json'], + extensions: ['.ts', '.js', '.json', '.node'], alias: { jsbi: path.resolve(__dirname, '..', '..', 'node_modules', 'jsbi', 'dist', 'jsbi-cjs.js') }, @@ -60,7 +62,8 @@ module.exports = (env: BuildEnv): webpack.Configuration => { }, { test: /\.node$/, - use: 'node-loader' + use: 'node-loader', + include: [MAIN_DIR, SCANNER_DIR, SCANNER_DIR_SYMLINKED] } ] }, diff --git a/packages/scanner/.gitignore b/packages/scanner/.gitignore new file mode 100644 index 0000000000..6ca71fb5fc --- /dev/null +++ b/packages/scanner/.gitignore @@ -0,0 +1,5 @@ +target +index.node +**/node_modules +**/.DS_Store +npm-debug.log* diff --git a/packages/scanner/Cargo.lock b/packages/scanner/Cargo.lock new file mode 100644 index 0000000000..ba3ac02eea --- /dev/null +++ b/packages/scanner/Cargo.lock @@ -0,0 +1,156 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "libloading" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "neon" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28e15415261d880aed48122e917a45e87bb82cf0260bb6db48bbab44b7464373" +dependencies = [ + "neon-build", + "neon-macros", + "neon-runtime", + "semver", + "smallvec", +] + +[[package]] +name = "neon-build" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bac98a702e71804af3dacfde41edde4a16076a7bbe889ae61e56e18c5b1c811" + +[[package]] +name = "neon-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7288eac8b54af7913c60e0eb0e2a7683020dffa342ab3fd15e28f035ba897cf" +dependencies = [ + "quote", + "syn", + "syn-mid", +] + +[[package]] +name = "neon-runtime" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676720fa8bb32c64c3d9f49c47a47289239ec46b4bdb66d0913cc512cb0daca" +dependencies = [ + "cfg-if", + "libloading", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "scanner" +version = "0.1.0" +dependencies = [ + "neon", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-mid" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa8e7560a164edb1621a55d18a0c59abf49d360f47aa7b821061dd7eea7fac9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/packages/scanner/Cargo.toml b/packages/scanner/Cargo.toml new file mode 100644 index 0000000000..31c9810799 --- /dev/null +++ b/packages/scanner/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "scanner" +version = "0.1.0" +license = "AGPL-3.0" +edition = "2018" +exclude = ["index.node"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +id3 = "1.7.0" + +[dependencies.uuid] +version = "1.3.4" +features = [ + "v4", + "fast-rng" +] + +[dependencies.neon] +version = "0.10" +default-features = false +features = ["napi-6"] diff --git a/packages/scanner/README.md b/packages/scanner/README.md new file mode 100644 index 0000000000..d005526e48 --- /dev/null +++ b/packages/scanner/README.md @@ -0,0 +1,119 @@ +# scanner + +This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon). + +## Installing scanner + +Installing scanner requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support). + +You can install the project with npm. In the project directory, run: + +```sh +$ npm install +``` + +This fully installs the project, including installing any dependencies and running the build. + +## Building scanner + +If you have already installed the project and only want to run the build, run: + +```sh +$ npm run build +``` + +This command uses the [cargo-cp-artifact](https://github.com/neon-bindings/cargo-cp-artifact) utility to run the Rust build and copy the built library into `./index.node`. + +## Exploring scanner + +After building scanner, you can explore its exports at the Node REPL: + +```sh +$ npm install +$ node +> require('.').hello() +"hello node" +``` + +## Available Scripts + +In the project directory, you can run: + +### `npm install` + +Installs the project, including running `npm run build`. + +### `npm build` + +Builds the Node addon (`index.node`) from source. + +Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm build` and `npm build-*` commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html): + +``` +npm run build -- --feature=beetle +``` + +#### `npm build-debug` + +Alias for `npm build`. + +#### `npm build-release` + +Same as [`npm build`](#npm-build) but, builds the module with the [`release`](https://doc.rust-lang.org/cargo/reference/profiles.html#release) profile. Release builds will compile slower, but run faster. + +### `npm test` + +Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/). + +## Project Layout + +The directory structure of this project is: + +``` +scanner/ +├── Cargo.toml +├── README.md +├── index.node +├── package.json +├── src/ +| └── lib.rs +└── target/ +``` + +### Cargo.toml + +The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command. + +### README.md + +This file. + +### index.node + +The Node addon—i.e., a binary Node module—generated by building the project. This is the main module for this package, as dictated by the `"main"` key in `package.json`. + +Under the hood, a [Node addon](https://nodejs.org/api/addons.html) is a [dynamically-linked shared object](https://en.wikipedia.org/wiki/Library_(computing)#Shared_libraries). The `"build"` script produces this file by copying it from within the `target/` directory, which is where the Rust build produces the shared object. + +### package.json + +The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. + +### src/ + +The directory tree containing the Rust source code for the project. + +### src/lib.rs + +The Rust library's main module. + +### target/ + +Binary artifacts generated by the Rust build. + +## Learn More + +To learn more about Neon, see the [Neon documentation](https://neon-bindings.com). + +To learn more about Rust, see the [Rust documentation](https://www.rust-lang.org). + +To learn more about Node, see the [Node documentation](https://nodejs.org). diff --git a/packages/scanner/index.d.ts b/packages/scanner/index.d.ts new file mode 100644 index 0000000000..c435528467 --- /dev/null +++ b/packages/scanner/index.d.ts @@ -0,0 +1,16 @@ +type LocalTrack = { + uuid: string; + artist?: string; + title?: string; + album?: string; + duration?: number; + position?: number; + year?: string; + + filename: string; + path: string; + local: true; +} + +declare const scanFolders = (folders: string[], supportedFormats: string[], onProgress: (progress: number, total: number, lastScanned?: string) => void) => Promise; +export { scanFolders }; diff --git a/packages/scanner/package-lock.json b/packages/scanner/package-lock.json new file mode 100644 index 0000000000..772be0c1e9 --- /dev/null +++ b/packages/scanner/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "@nuclear/scanner", + "version": "0.6.21", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@nuclear/scanner", + "version": "0.6.21", + "license": "AGPL-3.0", + "devDependencies": { + "cargo-cp-artifact": "^0.1" + } + }, + "node_modules/cargo-cp-artifact": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.8.tgz", + "integrity": "sha512-3j4DaoTrsCD1MRkTF2Soacii0Nx7UHCce0EwUf4fHnggwiE4fbmF2AbnfzayR36DF8KGadfh7M/Yfy625kgPlA==", + "dev": true, + "bin": { + "cargo-cp-artifact": "bin/cargo-cp-artifact.js" + } + } + } +} diff --git a/packages/scanner/package.json b/packages/scanner/package.json new file mode 100644 index 0000000000..79e3939034 --- /dev/null +++ b/packages/scanner/package.json @@ -0,0 +1,27 @@ +{ + "name": "@nuclear/scanner", + "version": "0.6.21", + "description": "Local library scanner", + "main": "index.node", + "types": "index.d.ts", + "scripts": { + "build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", + "build-debug": "npm run build --", + "build-release": "npm run build -- --release", + "install": "npm run build-release", + "test": "cargo test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nukeop/nuclear.git" + }, + "author": "nukeop ", + "license": "AGPL-3.0", + "bugs": { + "url": "https://github.com/nukeop/nuclear/issues" + }, + "homepage": "https://github.com/nukeop/nuclear#readme", + "devDependencies": { + "cargo-cp-artifact": "^0.1" + } +} \ No newline at end of file diff --git a/packages/scanner/src/error.rs b/packages/scanner/src/error.rs new file mode 100644 index 0000000000..30c9d6a73a --- /dev/null +++ b/packages/scanner/src/error.rs @@ -0,0 +1,15 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub struct ScannerError { + pub message: String, +} + +impl Error for ScannerError {} + +impl fmt::Display for ScannerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ScannerError: {}", self.message) + } +} diff --git a/packages/scanner/src/js.rs b/packages/scanner/src/js.rs new file mode 100644 index 0000000000..2f4f70d256 --- /dev/null +++ b/packages/scanner/src/js.rs @@ -0,0 +1,37 @@ +use neon::prelude::*; + +pub fn set_optional_field_str( + cx: &mut FunctionContext, + obj: &mut Handle, + field_name: &str, + value: Option, +) { + match value { + Some(v) => { + let field_value = cx.string(&v); + obj.set(cx, field_name, field_value).unwrap(); + } + None => { + let undefined = cx.undefined(); + obj.set(cx, field_name, undefined).unwrap(); + } + } +} + +pub fn set_optional_field_u32( + cx: &mut FunctionContext, + obj: &mut Handle, + field_name: &str, + value: Option, +) { + match value { + Some(v) => { + let field_value = cx.number(v as f64); + obj.set(cx, field_name, field_value).unwrap(); + } + None => { + let undefined = cx.undefined(); + obj.set(cx, field_name, undefined).unwrap(); + } + } +} diff --git a/packages/scanner/src/lib.rs b/packages/scanner/src/lib.rs new file mode 100644 index 0000000000..fe9376dcdd --- /dev/null +++ b/packages/scanner/src/lib.rs @@ -0,0 +1,156 @@ +mod error; +mod js; +mod local_track; +use error::ScannerError; +use id3::{Tag, TagLike}; +use js::{set_optional_field_str, set_optional_field_u32}; +use neon::prelude::*; +use std::collections::LinkedList; +use uuid::Uuid; + +use local_track::LocalTrack; + +fn visitFile(path: String) -> Result { + let tag = Tag::read_from_path(&path); + + match tag { + Ok(tag) => Ok(LocalTrack { + uuid: Uuid::new_v4().to_string(), + artist: tag.artist().map(|s| s.to_string()), + title: tag.title().map(|s| s.to_string()), + album: tag.album().map(|s| s.to_string()), + duration: tag.duration().unwrap_or(0), + position: tag.track(), + year: tag.year().map(|s| s as u32), + filename: path.split("/").last().map(|s| s.to_string()).unwrap(), + path: path.clone(), + }), + Err(e) => Err(ScannerError { + message: format!("Error reading file: {}", e), + }), + } +} + +fn visitDirectory( + path: String, + supportedFormats: Vec, + dirsToScanQueue: &mut LinkedList, + filesToScanQueue: &mut LinkedList, +) { + // Read the contents of the directory + let dir = std::fs::read_dir(path.clone()).unwrap(); + for entry in dir { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + // Add the directory to the queue + dirsToScanQueue.push_back(path.to_str().unwrap().to_string()); + } else if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { + // Add the file to the queue, if it's a supported format + if supportedFormats.contains(&extension.to_string()) { + filesToScanQueue.push_back(path.to_str().unwrap().to_string()); + } + } + } +} + +fn scanFolders(mut cx: FunctionContext) -> JsResult { + let folders: Handle = cx.argument(0)?; + let supported_formats: Handle = cx.argument(1)?; + let onProgressCallback: Handle = cx.argument(2)?; + let result: Handle = cx.empty_array(); + + // Copy all the starting folders to a queue, which holds all the folders left to scan + let supported_formats_vec = supported_formats + .to_vec(&mut cx)? + .into_iter() + .map(|format| format.to_string(&mut cx).unwrap().value(&mut cx)) + .collect::>(); + let folders_vec = folders.to_vec(&mut cx)?; + let mut dirs_to_scan_queue: LinkedList = LinkedList::new(); + let mut files_to_scan_queue: LinkedList = LinkedList::new(); + let mut total_files_to_scan_num; + for folder in folders_vec { + let folder_string = folder.to_string(&mut cx)?.value(&mut cx); + dirs_to_scan_queue.push_back(folder_string); + } + + // While there are still folders left to scan + while !dirs_to_scan_queue.is_empty() { + // Get the next folder to scan + let folder = dirs_to_scan_queue.pop_front().unwrap(); + + // Scan the folder + visitDirectory( + folder.clone(), + supported_formats_vec.clone(), + &mut dirs_to_scan_queue, + &mut files_to_scan_queue, + ); + + // Call the progress callback + let this = cx.undefined(); + let args = vec![ + cx.number(0).upcast(), + cx.number(files_to_scan_queue.len() as f64).upcast(), + cx.string(folder.clone()).upcast(), + ]; + onProgressCallback.call(&mut cx, this, args)?; + } + + // All folders have been scanned, now scan the files + total_files_to_scan_num = files_to_scan_queue.len(); + while !files_to_scan_queue.is_empty() { + // Get the next file to scan + let file = files_to_scan_queue.pop_front().unwrap(); + + // Scan the file + let track = visitFile(file.clone()).unwrap(); + let len = result.len(&mut cx); + let mut track_js_object = JsObject::new(&mut cx); + let track_uuid_js_string = cx.string(track.uuid); + track_js_object.set(&mut cx, "uuid", track_uuid_js_string)?; + + set_optional_field_str(&mut cx, &mut track_js_object, "artist", track.artist); + + set_optional_field_str(&mut cx, &mut track_js_object, "title", track.title); + + set_optional_field_str(&mut cx, &mut track_js_object, "album", track.album); + + let track_duration_js_number = cx.number(track.duration); + track_js_object.set(&mut cx, "duration", track_duration_js_number)?; + + set_optional_field_u32(&mut cx, &mut track_js_object, "position", track.position); + + set_optional_field_u32(&mut cx, &mut track_js_object, "year", track.year); + + let track_filename_js_string = cx.string(track.filename); + track_js_object.set(&mut cx, "filename", track_filename_js_string)?; + + let track_path_js_string = cx.string(track.path); + track_js_object.set(&mut cx, "path", track_path_js_string)?; + + let track_local = cx.boolean(true); + track_js_object.set(&mut cx, "local", track_local)?; + + result.set(&mut cx, len, track_js_object)?; + + // Call the progress callback + let this = cx.undefined(); + let args = vec![ + cx.number((total_files_to_scan_num - files_to_scan_queue.len()) as f64) + .upcast(), + cx.number(total_files_to_scan_num as f64).upcast(), + cx.string(file.clone()).upcast(), + ]; + onProgressCallback.call(&mut cx, this, args)?; + } + + Ok(result) +} + +#[neon::main] +fn main(mut cx: ModuleContext) -> NeonResult<()> { + cx.export_function("scanFolders", scanFolders)?; + Ok(()) +} diff --git a/packages/scanner/src/local_track.rs b/packages/scanner/src/local_track.rs new file mode 100644 index 0000000000..5d8aebd932 --- /dev/null +++ b/packages/scanner/src/local_track.rs @@ -0,0 +1,13 @@ +#[derive(Debug, Clone)] +pub struct LocalTrack { + pub uuid: String, + pub artist: Option, + pub title: Option, + pub album: Option, + pub duration: u32, + pub position: Option, + pub year: Option, + + pub filename: String, + pub path: String, +}