feat: Refactor project structure for modularity and expandability

- Implemented the service and adapter layers instead of controllers to simplify the integration of new audio recognition and metadata fetching APIs.
- Unified code styling and practices throughout the project to ensure consistency.
This commit is contained in:
xtrullor73
2024-05-13 22:11:08 -07:00
parent bcdc66ca18
commit d8683cb8ad
33 changed files with 398 additions and 243 deletions

1
.gitignore vendored
View File

@@ -129,5 +129,4 @@ dist
.yarn/install-state.gz
.pnp.*
# Miscellaneous
*.mp3

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="showEditorInlineBlameOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@@ -1,138 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="d4160043-cf6e-432e-a9d1-4e77ea95ecd3" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/src/controllers/recognitionController.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/errors/ApiError.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app.js" beforeDir="false" afterPath="$PROJECT_DIR$/app.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/api/audioRecognition.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/api/audioRecognition.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/controllers/audioController.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/controllers/audioController.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/services/audioFileValidator.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/services/audioFileValidator.js" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="JavaScript File" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitHubPullRequestSearchHistory">{
&quot;lastFilter&quot;: {
&quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;xtrll&quot;
}
}</component>
<component name="GitToolBoxStore">
<option name="projectConfigVersion" value="5" />
</component>
<component name="GithubPullRequestsUISettings">
<option name="selectedUrlAndAccountId">
<UrlAndAccount>
<option name="accountId" value="2dd8b0a6-0bdb-4445-9876-eac917c9314a" />
<option name="url" value="https://github.com/xtrll/MusicMetaFinder.git" />
</UrlAndAccount>
</option>
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 4
}</component>
<component name="ProjectId" id="2fNgHgn8pMO0IODj8huilr8qzvv" />
<component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;,
&quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;Node.js.app.js.executor&quot;: &quot;Run&quot;,
&quot;Node.js.audioController.js.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;javascript.nodejs.core.library.configured.version&quot;: &quot;21.6.2&quot;,
&quot;javascript.nodejs.core.library.typings.version&quot;: &quot;20.12.7&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/XTRLL/OneDrive/Projects/WebstormProjects/MusicMetaFinder&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;terminal&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\XTRLL\OneDrive\Projects\WebstormProjects\MusicMetaFinder\src\utils" />
</key>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="d4160043-cf6e-432e-a9d1-4e77ea95ecd3" name="Changes" comment="" />
<created>1713646863779</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1713646863779</updated>
<workItem from="1713646864896" duration="8000" />
<workItem from="1713719339108" duration="1794000" />
<workItem from="1713732443312" duration="323000" />
<workItem from="1713732777028" duration="116000" />
<workItem from="1713732899783" duration="2161000" />
<workItem from="1713735288664" duration="15843000" />
<workItem from="1713813884367" duration="1663000" />
<workItem from="1713833652605" duration="5256000" />
<workItem from="1713934593236" duration="4520000" />
<workItem from="1714279427712" duration="806000" />
<workItem from="1714435371913" duration="660000" />
<workItem from="1714497169161" duration="4293000" />
<workItem from="1714586158180" duration="2418000" />
<workItem from="1714602276496" duration="10134000" />
<workItem from="1714686118351" duration="10531000" />
<workItem from="1714756397102" duration="5253000" />
<workItem from="1714850771448" duration="8908000" />
<workItem from="1714941636860" duration="3548000" />
<workItem from="1715013121597" duration="1377000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
</component>
</project>

View File

@@ -1,34 +1,36 @@
#!/usr/bin/env node
import { checkEnvVariables } from './src/services/checkEnvVariables.js';
import { checkInputPath } from './src/services/checkInputPath.js';
import fetchFiles from './src/utils/filesFetcher.js';
import { validateAudioFiles } from './src/controllers/fileController.js';
import { recognizeAudioFiles } from './src/controllers/recognitionController.js';
import { retrieveMetadata } from './src/controllers/metadataController.js';
import checkEnvVariables from './src/utils/checkEnvVariables.js';
import checkInputPath from './src/utils/checkInputPath.js';
import fetchFiles from './src/utils/fetchFiles.js';
import validateAudioFiles from './src/services/fileService.js';
import recognizeAudioFiles from './src/services/musicRecognitionService.js';
import retrieveMetadata from './src/services/metadataRetrievalService.js';
const { ACOUSTID_API_KEY, SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET } = process.env;
async function main() {
try {
// Check for required environment variables
checkEnvVariables();
// Check for the input path
// checkEnvVariables();
// Check for required input path
const inputPath = process.argv[[2]];
checkInputPath(inputPath);
// Preferred service can be provided
const service = process.argv[[3]] || undefined;
// 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 recordingIds = await recognizeAudioFiles(audioFiles);
const audioIds = await recognizeAudioFiles(audioFiles, service);
// Fetch the audio metadata from Spotify using the recognized track IDs
const audioMetadata = await retrieveMetadata(recordingIds);
const audioMetadata = await retrieveMetadata(audioIds, service);
console.log(audioMetadata);
// 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);
console.error('An error occurred inside cli.js:', e);
process.exit(1);
}
}

157
package-lock.json generated
View File

@@ -10,14 +10,17 @@
"dependencies": {
"axios": "^1.6.8",
"axios-retry": "^4.1.0",
"command-exists": "^1.2.9",
"commander": "^12.0.0",
"dotenv": "^16.4.5",
"fluent-ffmpeg": "^2.1.2",
"fpcalc": "^1.3.0",
"music-metadata": "^7.14.0",
"node-fetch": "^3.3.2",
"node-id3": "^0.2.6",
"qs": "^6.12.1"
},
"bin": {
"analyze-audio": "node --env-file .env ./app.js"
"analyze-audio": "node --env-file .env ./cli.js"
},
"devDependencies": {
"@eslint/eslintrc": "^3.0.2",
@@ -383,6 +386,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -517,10 +525,13 @@
"node": ">= 0.8"
}
},
"node_modules/command-exists": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
"integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="
"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",
@@ -607,6 +618,14 @@
"node": ">= 8"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"engines": {
"node": ">= 12"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
@@ -1268,6 +1287,28 @@
"reusify": "^1.0.4"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -1332,6 +1373,29 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
"node_modules/fluent-ffmpeg": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz",
"integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==",
"dependencies": {
"async": ">=0.2.9",
"which": "^1.1.1"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/fluent-ffmpeg/node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"which": "bin/which"
}
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
@@ -1373,6 +1437,17 @@
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/fpcalc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fpcalc/-/fpcalc-1.3.0.tgz",
@@ -1619,6 +1694,17 @@
"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",
@@ -1964,8 +2050,7 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
@@ -2137,6 +2222,49 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"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",
@@ -2600,6 +2728,11 @@
"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",
@@ -3014,6 +3147,14 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"engines": {
"node": ">= 8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -3,15 +3,18 @@
"version": "1.0.0",
"description": "CLI utility for music enthusiasts to automatically recognize tracks and enrich file metadata based on the fetched information.",
"bin": {
"analyze-audio": "node --env-file .env ./app.js"
"analyze-audio": "node --env-file .env ./cli.js"
},
"dependencies": {
"axios": "^1.6.8",
"axios-retry": "^4.1.0",
"command-exists": "^1.2.9",
"commander": "^12.0.0",
"dotenv": "^16.4.5",
"fluent-ffmpeg": "^2.1.2",
"fpcalc": "^1.3.0",
"music-metadata": "^7.14.0",
"node-fetch": "^3.3.2",
"node-id3": "^0.2.6",
"qs": "^6.12.1"
},
"devDependencies": {
@@ -24,7 +27,7 @@
},
"scripts": {
"lint": "eslint . --fix",
"analyze": "node --env-file .env app.js"
"analyze": "node --env-file .env cli.js"
},
"type": "module"
}

View File

@@ -1,22 +1,22 @@
import getTrackMetadata from '../api/metadataRetrieval.js';
import getLyrics from '../api/lyricRetrieval.js';
import {getAlbumArt} from "../api/imageRetrieval.js";
import requestMetadata from '../../api/metadata/musicBrainzApi.js';
import getAlbumArt from "../../api/metadata/coverArtArchiveApi.js";
import getLyrics from '../../api/metadata/lyricOvhApi.js';
/**
* Controller to handle retrieval of metadata for an array of MusicBrainz recording IDs.
* Adapter to handle retrieval of metadata for an array of MusicBrainz recording IDs.
* @function
* @param {string[]} recordingIds - An array of MusicBrainz recording IDs.
* @returns {Promise<Object[]>} A promise that resolves with an array of metadata objects.
* @returns {Promise<Object[]>} A promise that resolves with an object of metadata.
*/
export async function retrieveMetadata(recordingIds) {
export default async function getMetadata(recordingIds) {
try {
if (!Array.isArray(recordingIds)) {
throw new Error('MetadataController expects an array of recordingIds');
throw new Error('musicBrainzAdapter expects an array of recordingIds');
}
// Prepare an array to hold the metadata results initialized as an array of Promises
// Prepare an object to hold the metadata results initialized as an array of Promises
const metadataPromises = recordingIds.map(async (recordingId) => {
const trackMetadata = await getTrackMetadata(recordingId);
const trackMetadata = await requestMetadata(recordingId);
const artist = trackMetadata['artist-credit']?.[0]?.name;
const title = trackMetadata.title;
const albumId = trackMetadata.releases[0].id
@@ -35,7 +35,7 @@ export async function retrieveMetadata(recordingIds) {
return await Promise.all(metadataPromises);
} catch (e) {
console.error('Error retrieving metadata from MusicBrainz or lyrics:', e);
console.error('Error retrieving metadata using MetaBrainz:', e);
throw e;
}
}

View File

@@ -0,0 +1,36 @@
import getSpotifyAccessToken from '../../api/spotifyAuthApi.js';
import requestMetadata from '../../api/metadata/spotifyApi.js';
import getLyrics from '../../api/metadata/lyricOvhApi.js';
/**
* Adapter to handle retrieval of metadata for an array of Spotify track IDs.
* @function
* @param {string[]} trackIds - An array of spotify track IDs.
* @returns {Promise<Object[]>} A promise that resolves with an object of metadata.
*/
export default async function getMetadata(trackIds) {
try {
if (!Array.isArray(trackIds)) {
throw new Error('spotifyAdapter expects an array of trackIds');
}
const accessToken = await getSpotifyAccessToken();
const metadataPromises = trackIds.map(async (trackId) => {
const trackMetadata = await requestMetadata(trackId, accessToken);
// const lyrics = await getLyrics(artist, title);
// Combine the track metadata with the lyrics
return {
...trackMetadata,
// lyrics
};
});
return await Promise.all(metadataPromises);
} catch (e) {
console.error('Error retrieving metadata using Spotify:', e);
throw e;
}
}

View File

@@ -1,4 +1,4 @@
import audioRecognition from '../api/audioRecognition.js';
import recognizeAudio from '../../api/recognition/acoustidApi.js';
/**
* Recognizes a list of audio files.
@@ -6,8 +6,8 @@ import audioRecognition from '../api/audioRecognition.js';
* @param {string[]} audioFiles An array of file paths of the audio files.
* @return {Promise<Object[]>} A promise that resolves to an array of recognition results.
*/
export async function recognizeAudioFiles(audioFiles) {
const recognitionPromises = audioFiles.map((filePath) => audioRecognition(filePath)
export default async function recognizeAudioFiles(audioFiles) {
const recognitionPromises = audioFiles.map((filePath) => recognizeAudio(filePath)
.catch((error) => {
// Log the error and return null
// This prevents one failed recognition from stopping the whole process

View File

@@ -0,0 +1,22 @@
import recognizeAudio from '../../api/recognition/auddApi.js';
/**
* Recognizes a list of audio files.
*
* @param {string[]} audioFiles An array of file paths of the audio files.
* @return {Promise<Object[]>} A promise that resolves to an array of recognition results.
*/
export default async function recognizeAudioFiles(audioFiles) {
const recognitionPromises = audioFiles.map((filePath) => recognizeAudio(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);
}

View File

@@ -1,4 +1,4 @@
import axios from 'axios';
import axiosRetry from '../../utils/retryAxios.js';
/**
* Retrieves the album art image URL for a given album ID.
@@ -7,10 +7,10 @@ import axios from 'axios';
* @param {string} albumId - The unique identifier for the album whose art is being retrieved.
* @returns {Promise<string|null>} - A promise that resolves to the image URL or null if not found or in case of an error.
*/
export const getAlbumArt = (albumId) => {
export default async function getAlbumArt(albumId) {
const endpoint = `http://coverartarchive.org/release/${albumId}/front`;
return axios.get(endpoint, { maxRedirects: 0 })
return axiosRetry.get(endpoint, { maxRedirects: 0 })
.then((response) => {
// If the status code is 200, the image URL should be in the responseURL
if (response.status === 200) {
@@ -30,4 +30,4 @@ export const getAlbumArt = (albumId) => {
console.error('Error retrieving cover art:', error);
return null;
});
};
}

View File

@@ -1,5 +1,5 @@
import axiosRetry from '../services/retryAxios.js';
import { handleError } from '../errors/generalApiErrorHandler.js';
import axiosRetry from '../../utils/retryAxios.js';
import handleError from '../../errors/generalApiErrorHandler.js';
/**
* Fetches lyrics for a specific song using the provided artist name and title.
@@ -9,7 +9,7 @@ import { handleError } from '../errors/generalApiErrorHandler.js';
* @param {string} title - The title of the song.
* @returns {Promise<string|null>} Promise object representing the lyrics for the song or null if not found or in case of an error.
*/
export default function fetchLyrics(artist, title) {
export default async function getLyrics(artist, title) {
const endpoint = `https://api.lyrics.ovh/v1/${encodeURIComponent(artist)}/${encodeURIComponent(title)}`;
return axiosRetry.get(endpoint)

View File

@@ -1,13 +1,13 @@
import axiosRetry from '../services/retryAxios.js';
import { handleError } from '../errors/generalApiErrorHandler.js';
import axiosRetry from '../../utils/retryAxios.js';
import handleError from '../../errors/generalApiErrorHandler.js';
/**
* Retrieves the metadata for a recording from MusicBrainz.
*
* @param {string} recordingId - The MusicBrainz recording ID.
* @param {string} recordingId - The MusicBrainz recording ID (can be obtained from acoustid).
* @returns {Promise<Object>} - A promise resolving to the track metadata.
*/
export default async function getAudioMetadata(recordingId) {
export default async function getMetadata(recordingId) {
const baseUrl = 'https://musicbrainz.org';
const query = `/ws/2/recording/${recordingId}?fmt=json&inc=artists+releases+release-groups+isrcs+url-rels+discids+media+artist-credits+aliases+tags+ratings+genres`;
@@ -37,6 +37,7 @@ export default async function getAudioMetadata(recordingId) {
console.error(`No metadata found for recording ID: ${recordingId}`);
return null;
}
handleError(error, recordingId);
const errorMessage = handleError(error, recordingId);
throw new Error(errorMessage);
});
}

View File

@@ -0,0 +1,19 @@
import axiosRetry from '../../utils/retryAxios.js';
import handleError from '../../errors/generalApiErrorHandler.js';
export default function getMetadata(trackId, accessToken) {
const endpoint = `https://api.spotify.com/v1/tracks/${trackId}`;
const requestOptions = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
return axiosRetry.get(endpoint, requestOptions)
.then((response) => response.data)
.catch((error) => {
const errorMessage = handleError(error, trackId);
throw new Error(errorMessage);
});
}

View File

@@ -1,14 +1,14 @@
import fpcalc from 'fpcalc'; // Chromaprint MUST be present inside %PATH% as well. https://github.com/acoustid/chromaprint/releases
import axiosRetry from '../services/retryAxios.js';
import { handleError } from '../errors/acoustidApiErrorHandler.js';
import axiosRetry from '../../utils/retryAxios.js';
import handleError from '../../errors/acoustidApiErrorHandler.js';
/**
* Identifies an audio file using the AcoustID API.
*
* @param {string} filePath - The path to the audio file to be recognized.
* @returns {Promise<Object>} - A promise resolving to the recognition result.
* @returns {Promise<Object>} - A promise resolving to the recording id.
*/
export default async function audioRecognition(filePath) {
export default async function acoustdIdAudioRecognition(filePath) {
const { duration, fingerprint } = await new Promise((resolve, reject) => {
fpcalc(filePath, (err, result) => {
if (err) reject(err);

View File

@@ -0,0 +1,34 @@
import fs from 'fs';
import axios from 'axios';
import axiosRetry from '../../utils/retryAxios.js';
import handleError from '../../errors/generalApiErrorHandler.js';
export default async function auddAudioRecognition(filePath) {
const audioData = fs.readFileSync(filePath);
const base64Audio = Buffer.from(audioData).toString('base64');
const body = new URLSearchParams({
api_token: process.env.AUDD_API_TOKEN,
audio: base64Audio,
return: 'spotify',
});
const requestOptions = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
};
return axios.post('https://api.audd.io/', new URLSearchParams(body), requestOptions)
.then((response) => {
const { data } = response;
if (!data) throw new Error('No data received from Audd API');
if (data.error) throw new Error(`Audd API Error: ${JSON.stringify(data.error)}`);
console.log('Recognition successful for:', filePath);
return data;
})
.catch((error) => {
console.error('Error recognizing audio');
const errorMessage = handleError(error, filePath);
throw new Error(errorMessage);
});
}

View File

@@ -1,31 +0,0 @@
import axios from 'axios';
import qs from 'qs';
const client_id = process.env.SPOTIFY_CLIENT_ID;
const client_secret = process.env.SPOTIFY_CLIENT_SECRET;
export default function getSpotifyAccessToken() {
const authOptions = {
method: 'POST',
url: 'https://accounts.spotify.com/api/token',
headers: {
Authorization: `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString('base64')}`,
'Content-Type': 'applicaion/x-www-form-urlencoded',
},
data: qs.stringify({
grant_type: 'client_credentials',
}),
};
return axios(authOptions)
.then((response) => {
if (response.status === 200) {
return response.data.access_token;
}
return Promise.reject(new Error(`Could not get access token for ${response.status}`));
})
.catch((error) => {
console.error('Auth Error:', error);
throw error;
});
}

32
src/api/spotifyAuthApi.js Normal file
View File

@@ -0,0 +1,32 @@
import qs from 'qs';
import axiosRetry from '../utils/retryAxios.js';
import handleError from '../errors/generalApiErrorHandler.js';
export default async function getSpotifyAccessToken() {
const { client_id, client_secret } = process.env;
const authOptions = {
method: 'POST',
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',
},
data: qs.stringify({
grant_type: 'client_credentials',
}),
};
return axiosRetry(authOptions)
.then((response) => {
if (response.status === 200) {
return response.data.access_token;
}
throw new Error(`Could not get access token for ${response.status}`);
})
.catch((error) => {
console.error('Spotify authentication error:', error);
const errorMessage = handleError(error);
throw new Error(errorMessage);
});
}

View File

@@ -1,4 +1,4 @@
export function handleError(error, filePath) {
export default function handleError(error, filePath) {
if (error.response) { // Detailed error information when the API responds with an error status
console.error(`Request failed with status: ${error.response.status}`);
console.error('Headers:', error.response.headers);

View File

@@ -1,4 +1,4 @@
export function handleError(error, identifier) {
export default function handleError(error, identifier) {
let message = '';
// Handler when the API responds with an error status.

View File

@@ -1,4 +1,4 @@
import validateAudioFile from '../services/audioFileValidator.js';
import validateAudioFile from '../utils/validateAudioFiles.js';
/**
* Processes an array of file paths and returns an array containing only valid audio file paths.
@@ -6,7 +6,7 @@ import validateAudioFile from '../services/audioFileValidator.js';
* @param {string[]} filePaths - The array of file paths to process.
* @returns {Promise<string[]>} A promise that resolves to an array of valid audio file paths.
*/
export async function validateAudioFiles(filePaths) {
export default async function validateAudioFiles(filePaths) {
// Validate input type
if (!filePaths instanceof Array) {
throw new TypeError('Input must be an array of file paths (strings).');

View File

@@ -0,0 +1,14 @@
function normalize(metadata) {
const normalized = {
title: metadata.title || metadata.name || '',
artist: metadata.artist || '',
album: metadata.album || metadata.collection || '',
year: metadata.year || metadata.releaseDate && metadata.releaseDate.substring(0, 4) || '',
artwork: metadata.albumArt || metadata.artwork || metadata.image || '',
lyrics: metadata.lyrics || '',
genre: metadata.genre || (metadata.tags && metadata.tags.join(', ')) || '',
// ... add any other fields necessary for normalization
};
return normalized;
}

View File

@@ -0,0 +1,18 @@
import getMetadataUsingSpotify from '../adapters/metadata/spotifyAdapter.js';
import getMetadataUsingMusicBrainz from '../adapters/metadata/musicBrainzAdapter.js';
// List of supported services
const serviceMap = {
audd: getMetadataUsingSpotify,
acoustid: getMetadataUsingMusicBrainz,
};
export default async function recognizeAudio(audioIds, source) {
let recognitionService = serviceMap[source];
if (!recognitionService) {
console.error('Recognition service unknown or not provided, using default configuration');
recognitionService = getMetadataUsingMusicBrainz; // Default service
}
return recognitionService(audioIds);
}

View File

@@ -0,0 +1,18 @@
import recognizeUsingAudd from '../adapters/recognition/auddAdapter.js';
import recognizeUsingAcoustid from '../adapters/recognition/acoustidAdapter.js';
// List of supported services
const serviceMap = {
audd: recognizeUsingAudd,
acoustid: recognizeUsingAcoustid,
};
export default async function recognizeAudio(filePaths, source) {
let recognitionService = serviceMap[source];
if (!recognitionService) {
console.error('Recognition service unknown or not provided, using default configuration');
recognitionService = recognizeUsingAcoustid; // Default service
}
return recognitionService(filePaths);
}

View File

@@ -1,4 +1,4 @@
import filesFetcher from '../utils/filesFetcher.js';
import filesFetcher from '../utils/fetchFiles.js';
/**
* Handle the input path provided by the user to resolve file paths

View File

@@ -1,4 +1,4 @@
export function checkEnvVariables() {
export default function checkEnvVariables() {
if (!process.env.ACOUSTID_API_KEY || !process.env.SPOTIFY_CLIENT_ID || !process.env.SPOTIFY_CLIENT_SECRET) {
throw new Error('Please set up ACOUSTID_API_KEY, SPOTIFY_CLIENT_ID, and SPOTIFY_CLIENT_SECRET in your .env file.');
}

View File

@@ -1,4 +1,4 @@
export function checkInputPath(inputPath) {
export default function checkInputPath(inputPath) {
if (!inputPath) {
throw new Error('Please provide the path to an audio file or directory.');
}

BIN
track.mp3

Binary file not shown.