diff --git a/.gitignore b/.gitignore index c6bba59..326fed0 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Miscellaneous +.mp3 diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml new file mode 100644 index 0000000..d1e3d40 --- /dev/null +++ b/.idea/git_toolbox_prj.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..d23208f --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace-LifeIsARace.xml b/.idea/workspace-LifeIsARace.xml new file mode 100644 index 0000000..ec27889 --- /dev/null +++ b/.idea/workspace-LifeIsARace.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + { + "lastFilter": { + "state": "OPEN", + "assignee": "xtrll" + } +} + + + + + + + + { + "associatedIndex": 4 +} + + + + + + + { + "keyToString": { + "ASKED_ADD_EXTERNAL_FILES": "true", + "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true", + "Node.js.app.js.executor": "Run", + "Node.js.audioController.js.executor": "Run", + "RunOnceActivity.OpenProjectViewOnStart": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "git-widget-placeholder": "main", + "ignore.virus.scanning.warn.message": "true", + "javascript.nodejs.core.library.configured.version": "21.6.2", + "javascript.nodejs.core.library.typings.version": "20.12.7", + "last_opened_file_path": "C:/Users/XTRLL/OneDrive/Projects/WebstormProjects/MusicMetaFinder", + "node.js.detected.package.eslint": "true", + "node.js.detected.package.tslint": "true", + "node.js.selected.package.eslint": "(autodetect)", + "node.js.selected.package.tslint": "(autodetect)", + "nodejs_package_manager_path": "npm", + "settings.editor.selected.configurable": "terminal", + "vue.rearranger.settings.migration": "true" + } +} + + + + + + + + + + 1713646863779 + + + + + + + + + + + \ No newline at end of file diff --git a/app.js b/app.js index fe98baa..3f74b69 100644 --- a/app.js +++ b/app.js @@ -1,18 +1,49 @@ #!/usr/bin/env node -import audioController from './src/controllers/audioController.js'; +import fetchFiles from './src/utils/filesFetcher.js'; +import { validateAudioFiles } from './src/controllers/fileController.js'; +import { recognizeAudioFiles } from './src/controllers/recognitionController.js'; +// import metadataController from './src/controllers/metadataController.js'; +import { checkEnvVariables } from './src/utils/checkEnvVariables.js'; +import { checkInputPath } from './src/utils/checkInputPath.js'; -const { AUDD_API_TOKEN } = process.env; -const inputPath = process.argv[[2]]; +const { ACOUSTID_API_TOKEN, SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET } = process.env; -if (!AUDD_API_TOKEN) { - console.error('Please set up the AUDD_API_TOKEN in your .env file.'); - process.exit(1); +async function main() { + try { + // Check for required environment variables + checkEnvVariables(); + // Check for the input path + const inputPath = process.argv[[2]]; + checkInputPath(inputPath); + + // Resolve the input path to get array of file paths (handles one or more files) + const files = await fetchFiles(inputPath); + // Process the resolved paths to confirm and prepare the audio file for metadata recognition + const audioFiles = await validateAudioFiles(files); + // Recognize the content of the audio file and obtain the corresponding Spotify track ID + const recognizedAudioFiles = await recognizeAudioFiles(audioFiles); + // Fetch the audio metadata from Spotify using the recognized track IDs + // const audioMetadata = await metadataController.fetchFromSpotify(recognizedAudioFiles); + // Write the fetched metadata into the audio file + // const processedAudioFiles = await fileController.writeMetadata(audioMetadata, audioFiles); + } catch (e) { + console.error('An error occurred inside app.js:', e); + process.exit(1); + } } -if (!inputPath) { - console.error('Please provide the path to an audio file.'); +main().catch((error) => { + console.error(`Execution error: ${error.message}`); process.exit(1); -} +}); -audioController(inputPath); +process.on('uncaughtException', (error) => { + console.error('Uncaught Exception:', error); + process.exit(1); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); + process.exit(1); +}); diff --git a/package-lock.json b/package-lock.json index 0948092..ea769b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,20 @@ { - "name": "MusicMetaFinder", + "name": "music-meta-finder", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "music-meta-finder", + "version": "1.0.0", "dependencies": { "axios": "^1.6.8", - "commander": "^12.0.0", + "chromaprint": "^0.1.0", "dotenv": "^16.4.5", - "music-metadata": "^7.14.0", - "node-id3": "^0.2.6" + "qs": "^6.12.1" + }, + "bin": { + "analyze-audio": "node --env-file .env ./app.js" }, "devDependencies": { "@eslint/eslintrc": "^3.0.2", @@ -177,11 +182,6 @@ "node": ">= 8" } }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -425,7 +425,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -465,6 +464,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chromaprint": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chromaprint/-/chromaprint-0.1.0.tgz", + "integrity": "sha512-VWsjVpoPPNtDd9KDNNkr1MM7QiTQmAtbPlOG+I0uvilkHWjo1OBgHzmRQidO20HssYVFbxwP0eqT+YJzosD3eQ==" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -494,14 +498,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", - "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", - "engines": { - "node": ">=18" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -514,14 +510,6 @@ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", "dev": true }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -591,6 +579,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -613,7 +602,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -738,7 +726,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -750,7 +737,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -1193,22 +1179,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-type": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", - "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", - "dependencies": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1296,7 +1266,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1332,7 +1301,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -1427,7 +1395,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -1463,7 +1430,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -1475,7 +1441,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -1487,7 +1452,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -1514,7 +1478,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -1522,36 +1485,6 @@ "node": ">= 0.4" } }, - "node_modules/iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -1599,7 +1532,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/internal-slot": { "version": "1.0.7", @@ -1955,14 +1889,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2006,28 +1932,8 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/music-metadata": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-7.14.0.tgz", - "integrity": "sha512-xrm3w7SV0Wk+OythZcSbaI8mcr/KHd0knJieu8bVpaPfMv/Agz5EooCAPz3OR5hbYMiUG6dgAPKZKnMzV+3amA==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "content-type": "^1.0.5", - "debug": "^4.3.4", - "file-type": "^16.5.4", - "media-typer": "^1.1.0", - "strtok3": "^6.3.0", - "token-types": "^4.2.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -2035,19 +1941,10 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/node-id3": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.6.tgz", - "integrity": "sha512-w8GuKXLlPpDjTxLowCt/uYMhRQzED3cg2GdSG1i6RSGKeDzPvxlXeLQuQInKljahPZ0aDnmyX7FX8BbJOM7REg==", - "dependencies": { - "iconv-lite": "0.6.2" - } - }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2243,18 +2140,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "node_modules/peek-readable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", - "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -2287,6 +2172,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2307,34 +2206,6 @@ } ] }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -2445,25 +2316,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -2481,11 +2333,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -2499,7 +2346,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -2552,7 +2398,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -2566,14 +2411,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -2656,22 +2493,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strtok3": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", - "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^4.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2702,22 +2523,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/token-types": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", - "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -2851,11 +2656,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 60ebbdb..c5bcd78 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,8 @@ }, "dependencies": { "axios": "^1.6.8", - "commander": "^12.0.0", + "chromaprint": "^0.1.0", "dotenv": "^16.4.5", - "fluent-ffmpeg": "^2.1.2", - "music-metadata": "^7.14.0", - "node-fetch": "^3.3.2", - "node-id3": "^0.2.6", "qs": "^6.12.1" }, "devDependencies": { diff --git a/src/api/audioRecognition.js b/src/api/audioRecognition.js index cc63010..e703df3 100644 --- a/src/api/audioRecognition.js +++ b/src/api/audioRecognition.js @@ -1,45 +1,12 @@ import axios from 'axios'; -import fs from 'fs'; -import processApiError from "../errors/ApiError.js"; -import truncateAudioStream from "../utils/truncateAudioStream"; +import chromaprint from 'chromaprint' +import processApiError from "../errors/apiError.js"; -/** - * Recognizes audio via the AudD API. - * - * @param {string} filePath The local path to the audio file to recognize. - * @param {string} AUDD_API_TOKEN The token for authentication with the AudD API. - * @return {Promise} A promise that resolves with the recognition data from AudD API. - */ -export default async function audioRecognition(filePath, AUDD_API_TOKEN) { - try { - // Create a read stream for the audio file. - const stream = fs.createReadStream(filePath); - // Truncate the audio stream to 25 seconds. - const truncatedAudioBuffer = await truncateAudioStream(stream); +export default async function audioRecognition(filePath) { - // Convert the truncated audio buffer to a base64-encoded string. - const base64audio = truncatedAudioBuffer.toString('base64'); - // Construct the request body with necessary parameters - const body = new URLSearchParams({ - api_token: AUDD_API_TOKEN, - audio: base64Audio, - return: 'spotify', // Asking the API to return data in a format that can be used to reference Spotify tracks - }); - - // Define request options, including a 10-second timeout - const requestOptions = { - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - timeout: 10000, - }; - } catch (e) { - console.error('Error during setup of audio recognition:', e.message, filePath); - throw e; - } - - // Send a POST request to the AudD API with the audio data for recognition - return axios.post('https://api.audd.io/', new URLSearchParams(body), requestOptions) + return axios.post('http://api.acoustid.org/v2/lookup', new URLSearchParams(body), requestOptions) .then((response) => { const { data } = response; // Destructure the data from the response object if (!data) throw { code: 'NO_DATA' }; // Throw an error if no data is returned diff --git a/src/api/spotifyAuth.js b/src/api/spotifyAuth.js index 9a77ac1..c05c19c 100644 --- a/src/api/spotifyAuth.js +++ b/src/api/spotifyAuth.js @@ -10,7 +10,7 @@ export default function getSpotifyAccessToken() { url: 'https://accounts.spotify.com/api/token', headers: { Authorization: `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString('base64')}`, - 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Type': 'applicaion/x-www-form-urlencoded', }, data: qs.stringify({ grant_type: 'client_credentials', diff --git a/src/controllers/audioController.js b/src/controllers/audioController.js deleted file mode 100644 index 67171b3..0000000 --- a/src/controllers/audioController.js +++ /dev/null @@ -1,42 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import recognizeAudio from '../api/audioRecognition.js'; -import fetchFilesFromDirectory from '../utils/directoryFileFetcher.js'; -import validateAudioFile from '../services/audioFileValidator.js'; - -export default async function audioController(inputPath) { - let stats; - - try { - stats = await fs.promises.stat(inputPath); - } catch (e) { - console.error('Error accessing provided path', e); - return; - } - - if (stats.isDirectory()) { - try { - const files = await fetchFilesFromDirectory(inputPath); - for (const file of files) { - await handleFile(file); - } - } catch (e) { - console.error(`Error extracting files from ${inputPath}:`, e); - throw e; - } - } else if (stats.isFile()) { - await handleFile(inputPath); - } else { - console.error('The provided path is neither a file nor a directory.'); - } -} - -async function handleFile(filePath) { - try { - const audioFile = await validateAudioFile(filePath); - const result = await recognizeAudio(audioFile, process.env.AUDD_API_TOKEN); - console.log(`Results for file ${path.basename(filePath)}:`, result); - } catch (e) { - console.error(`An error occurred processing the file ${path.basename(filePath)}:`, e); - } -} diff --git a/src/controllers/fileController.js b/src/controllers/fileController.js new file mode 100644 index 0000000..4a90dd3 --- /dev/null +++ b/src/controllers/fileController.js @@ -0,0 +1,20 @@ +import validateAudioFile from '../services/audioFileValidator.js'; + +/** + * Processes an array of file paths and returns an array containing only valid audio file paths. + * + * @param {string[]} filePaths - The array of file paths to process. + * @returns {Promise} A promise that resolves to an array of valid audio file paths. + */ +export async function validateAudioFiles(filePaths) { + // Validate input type + if (!filePaths instanceof Array) { + throw new TypeError('Input must be an array of file paths (strings).'); + } + // Create a Promise for each file to validate it as an audio file + const validationPromises = filePaths.map((filePath) => validateAudioFile(filePath)); + // Wait for all the validation promises to resolve + const validationResults = await Promise.all(validationPromises); + // Filter out any non-audio file paths (represented as null from validateAudioFile) and return resulting array + return validationResults.filter((result) => result.type !== null); +} diff --git a/src/controllers/metadataController.js b/src/controllers/metadataController.js new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/pathController.js b/src/controllers/pathController.js new file mode 100644 index 0000000..9ff992e --- /dev/null +++ b/src/controllers/pathController.js @@ -0,0 +1,15 @@ +import filesFetcher from '../utils/filesFetcher.js'; + +/** + * Handle the input path provided by the user to resolve file paths + * using the appropriate path handling utility. This controller function + * is responsible for initiating the file fetching process and ensuring + * that the paths returned comply with the expected format and conditions. + * + * @param {string} inputPath - The initial file or directory path provided by the user. + * @returns {Promise} - A promise that resolves to an array of file paths, + * or rejects with an error if the operation fails. + */ +export async function fetchFiles(inputPath) { + return filesFetcher(inputPath); +} \ No newline at end of file diff --git a/src/controllers/recognitionController.js b/src/controllers/recognitionController.js new file mode 100644 index 0000000..991397f --- /dev/null +++ b/src/controllers/recognitionController.js @@ -0,0 +1,24 @@ +import audioRecognition from '../api/audioRecognition.js'; + +/** + * Recognizes a list of audio files. + * + * @param {string[]} audioFiles An array of file paths of the audio files. + * @return {Promise} A promise that resolves to an array of recognition results. + */ +export async function recognizeAudioFiles(audioFiles) { + const recognitionPromises = audioFiles.map(filePath => { + audioRecognition(filePath) + .catch(error => { + // Log the error and return null + // This prevents one failed recognition from stopping the whole process + console.error(`Recognition failed for file ${filePath}:`, error); + return null; + }) + }); + + // Wait for all recognitions to resolve. This will be an array of results or null values + const recognizedAudioFiles = await Promise.all(recognitionPromises); + // Filter out unsuccessful recognitions + return recognizedAudioFiles.filter(result => result !== null); +} \ No newline at end of file diff --git a/src/errors/ApiError.js b/src/errors/ApiError.js deleted file mode 100644 index 0f5a72b..0000000 --- a/src/errors/ApiError.js +++ /dev/null @@ -1,21 +0,0 @@ -const errorMessages = { - NO_DATA: 'No data received from AudD API', - NO_RESPONSE: 'No response received from AudD API', - SETUP_ERROR: 'Error setting up the request to AudD API', - UNKNOWN_ERROR: 'An unknown error occurred with the AudD API. Contact us in this case.', - 901: 'No API token passed, and the limit was reached (you need to obtain an API token).', - 900: 'Wrong API token (check the api_token parameter).', - 600: 'Incorrect audio URL.', - 700: 'You haven\'t sent a file for recognition (or we didn\'t receive it). If you use the POST HTTP method, check the Content-Type header: it should be multipart/form-data; also check the URL you\'re sending requests to: it should start with https:// (http:// requests get redirected and we don\'t receive any data from you when your code follows the redirect).', - 500: 'Incorrect audio file.', - 400: 'Too big audio file. 10MB or 25 seconds is the maximum. We recommend recording no more than 20 seconds (usually, it takes less than one megabyte). If you need to recognize larger audio files, use the enterprise endpoint instead, it supports even days-long files.', - 300: 'Fingerprinting error: there was a problem with audio decoding or with the neural network. Possibly, the audio file is too small.', - 100: 'An unknown error.' -}; - -export default function processApiError(error) { - let errorMessage = errorMessages[error.code] || errorMessages['UNKNOWN_ERROR']; - - console.error(errorMessage); - throw new Error(errorMessage); -} diff --git a/src/errors/apiError.js b/src/errors/apiError.js new file mode 100644 index 0000000..fa43273 --- /dev/null +++ b/src/errors/apiError.js @@ -0,0 +1,13 @@ +const errorMessages = { + NO_DATA: 'No data received', + NO_RESPONSE: 'No response received', + SETUP_ERROR: 'Error setting up the request', + UNKNOWN_ERROR: 'An unknown error occurred', +}; + +export default function processApiError(error) { + let errorMessage = errorMessages[error.code] || errorMessages['UNKNOWN_ERROR']; + + console.error(errorMessage); + throw new Error(errorMessage); +} diff --git a/src/services/audioFileValidator.js b/src/services/audioFileValidator.js index d4429a4..7294fb5 100644 --- a/src/services/audioFileValidator.js +++ b/src/services/audioFileValidator.js @@ -1,16 +1,34 @@ import fs from 'fs/promises'; import path from 'path'; +/** + * Validates whether a given file path points to a supported audio file. + * Logs an error for each file that is not an audio file or not a file at all and ignores it. + * + * @param {string} filePath - The absolute path to the file to validate. + * @returns {Promise} - The file path if it is a supported audio file, otherwise null. + */ export default async function validateAudioFile(filePath) { - const stats = await fs.lstat(filePath); - if (!stats.isFile()) { - throw new Error(`The path ${filePath} is not a file.`); - } + try { + // Ensure that the path points to a file + const stats = await fs.lstat(filePath); + if (!stats.isFile()) { + console.error(`The path ${filePath} is not an audio file and is ignored.`); + return null; // Stop further checks and return null + } - const fileExtension = path.extname(filePath).toLowerCase(); - const audioExtensions = ['.mp3', '.wav', '.aac', '.flac', '.ogg', '.aiff', '.m4a']; - if (audioExtensions.includes(fileExtension)) { - return filePath; + // List of supported audio file extensions + const audioExtensions = ['.mp3', '.wav', '.aac', '.flac', '.ogg', '.aiff', '.m4a']; + + // Check if the file extension is in the list of supported audio formats + const fileExtension = path.extname(filePath).toLowerCase(); + if (!audioExtensions.includes(fileExtension)) { + console.error(`File ${path.basename(filePath)} is not an audio file and is ignored.`); + return null; // Not an audio file, return null + } + + return filePath; // The file is a supported audio file + } catch (e) { + console.error(`Error validating file ${filePath}: ${e}`); } - console.error(`File ${path.basename(filePath)} is not an audio file and is ignored.`); } diff --git a/src/utils/checkEnvVariables.js b/src/utils/checkEnvVariables.js new file mode 100644 index 0000000..aa6c8b3 --- /dev/null +++ b/src/utils/checkEnvVariables.js @@ -0,0 +1,5 @@ +export function checkEnvVariables() { + if (!process.env.ACOUSTID_API_TOKEN || !process.env.SPOTIFY_CLIENT_ID || !process.env.SPOTIFY_CLIENT_SECRET) { + throw new Error('Please set up ACOUSTID_API_TOKEN, SPOTIFY_CLIENT_ID, and SPOTIFY_CLIENT_SECRET in your .env file.'); + } +} \ No newline at end of file diff --git a/src/utils/checkInputPath.js b/src/utils/checkInputPath.js new file mode 100644 index 0000000..e84138d --- /dev/null +++ b/src/utils/checkInputPath.js @@ -0,0 +1,5 @@ +export function checkInputPath(inputPath) { + if (!inputPath) { + throw new Error('Please provide the path to an audio file or directory.'); + } +} \ No newline at end of file diff --git a/src/utils/directoryFileFetcher.js b/src/utils/directoryFileFetcher.js deleted file mode 100644 index 7b0e2f1..0000000 --- a/src/utils/directoryFileFetcher.js +++ /dev/null @@ -1,25 +0,0 @@ -import fs from 'fs/promises'; -import path from 'path'; - -export default async function directoryFileFetcher(folderPath) { - async function enumerateFilesInDirectory(dirPath) { - let fileList = []; - const entries = await fs.readdir(dirPath, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name); - if (entry.isDirectory()) { - fileList = fileList.concat(await enumerateFilesInDirectory(fullPath)); - } else { - fileList.push(fullPath); - } - } - return fileList; - } - - try { - return await enumerateFilesInDirectory(folderPath); // return array of files - } catch (e) { - console.error(`Error enumerating files in directory ${folderPath}:`, e); - throw e; - } -} diff --git a/src/utils/filesFetcher.js b/src/utils/filesFetcher.js new file mode 100644 index 0000000..baaba7b --- /dev/null +++ b/src/utils/filesFetcher.js @@ -0,0 +1,67 @@ +import fs from 'fs'; +import path from 'path'; + +/** + * Fetches files from a given input path. If the path is a directory, + * it recursively fetches all files within it, including in subdirectories. + * If the path is an individual file, it returns an array with just + * that file path. This function handles both files and directories, + * making it versatile for various file fetching needs. + * + * @param {string} inputPath - The path to the file or directory to fetch files from. + * @returns {Promise} A promise that resolves with an array of file paths. + * @throws {Error} If the inputPath does not refer to an existing file or directory. + */ +export default async function fetchFiles(inputPath) { + try { + const stats = await fs.promises.stat(inputPath); + let files = []; + if (stats.isDirectory()) { + // Recursive case: inputPath is a directory. + files = await fetchFilesFromDirectory(inputPath); + } else if (stats.isFile()) { + // Base case: inputPath is a file. + files = [inputPath]; + } else { + throw new Error('Invalid path: not a file or directory'); + } + return files; + } catch (e) { + console.error('Error resolving path', e); + throw e; + } +} + +/** + * A private helper function that recursively fetches all files within a directory. + * It will traverse all subdirectories and return a flat array of file paths. + * + * @param {string} folderPath - The directory path to start the file search from. + * @returns {Promise} A promise that resolves with an array of file paths. + * @throws {Error} If an error occurs while reading the directory. + */ +async function fetchFilesFromDirectory(folderPath) { + async function enumerateFilesInDirectory(dirPath) { + let fileList = []; + const entries = await fs.readdir(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + // Recurse into subdirectories. + fileList = fileList.concat(await enumerateFilesInDirectory(fullPath)); + } else { + // Add file path to the list. + fileList.push(fullPath); + } + } + return fileList; + } + + try { + // Initiate recursive file listing from the root folder path. + return await enumerateFilesInDirectory(folderPath); + } catch (e) { + console.error(`Error enumerating files in directory ${folderPath}:`, e); + throw e; + } +} diff --git a/src/utils/truncateAudioStream.js b/src/utils/truncateAudioStream.js deleted file mode 100644 index c30eec2..0000000 --- a/src/utils/truncateAudioStream.js +++ /dev/null @@ -1,29 +0,0 @@ -import ffmpeg from 'fluent-ffmpeg'; - -/** - * Truncates an audio stream to the desired length - * - * @param {ReadStream} inputStream The readable stream for the audio file. - * @param {number} duration The duration in seconds to which the audio is to be truncated. - * @returns {Promise} A promise that resolves with the truncated audio as a buffer. - */ - -export default function truncateAudioStream(inputStream, duration = 25) { - return new Promise((resolve, reject) => { - let audioBuffer = Buffer.from([]); - - ffmpeg(inputStream) - .setDuration(duration) - .toFormat('mp3') - .on('end', () => { - resolve(audioBuffer) - }) - .on('error', (err) => { - reject(err) - }) - .on('data', (chunk) => { - audioBuffer = Buffer.concat([audioBuffer, chunk]); - }) - .run(); - }) -} \ No newline at end of file diff --git a/track.mp3 b/track.mp3 new file mode 100644 index 0000000..098b4f6 Binary files /dev/null and b/track.mp3 differ