refactor: remove AuDD references and unused project files

Removed all files, dependencies, and mentions related to the AuDD API. Cleaned up the src directory, removing all non-developed or unused items, as part of a major cleanup. This change is due to the strategic shift towards using audio fingerprinting for song recognition, as opposed to sending the raw audio file itself.
This commit is contained in:
xtrullor73
2024-05-09 23:40:46 -07:00
parent 8bb93cafd6
commit a97736abee
24 changed files with 418 additions and 412 deletions

View File

@@ -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<Object>} 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

View File

@@ -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',

View File

@@ -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);
}
}

View File

@@ -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<string[]>} 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);
}

View File

View File

@@ -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<string[]>} - 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);
}

View File

@@ -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<Object[]>} 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);
}

View File

@@ -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);
}

13
src/errors/apiError.js Normal file
View File

@@ -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);
}

View File

@@ -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<string|null>} - 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.`);
}

View File

@@ -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.');
}
}

View File

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

View File

@@ -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;
}
}

67
src/utils/filesFetcher.js Normal file
View File

@@ -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<string[]>} 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<string[]>} 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;
}
}

View File

@@ -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<Buffer>} 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();
})
}