Does anyone know of a simple web api where a user can upload a file using the file picker and it will return the songs duration in minutes. I swear I have searched the entire internet up and down and found nothing. Im okay with temporarily uploading the file to cloudinary if I need to use the file link. Thanks.
See this topic: Measuring incoming base64 sound files and also How to retrieve audio information like length of audio file from Cloudinary file? Rails App UPDATED - Stack Overflow
Thanks so much for the response,
Ill take a look at these, thanks.
Update:
After many trial and error I was finally able to find a solution that succesfully returned the duration of a mp3 file.
The way I did it was using Node.js and Vercel to run a server which allowed me to program a JavaScript file and upload it to my own domain which is where I had the following file:
const axios = require('axios');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const mm = require('music-metadata');
export default async function handler(req, res) {
try {
// Log the entire request object
console.log('Received request:', req);
// Handle CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
if (req.method === 'OPTIONS') {
// Pre-flight request, respond successfully
console.log('Pre-flight request received');
res.status(200).end();
return;
}
// Parse the request body as JSON
let requestBody;
try {
requestBody = JSON.parse(req.body);
} catch (error) {
console.error('Error parsing request body as JSON:', error);
res.status(400).json({ error: 'Error parsing request body as JSON' });
return;
}
// Log the entire request body
console.log('Request body:', requestBody);
// Check if the request has a body
if (!requestBody || typeof requestBody !== 'object' || !requestBody.fileLink) {
console.error('Invalid request. No fileLink found in the request body.');
res.status(400).json({ error: 'Invalid request. No fileLink found in the request body.' });
return;
}
// Get the file link from the request body
const { fileLink } = requestBody;
console.log('File link:', fileLink);
// Download the file using axios
const response = await axios.get(fileLink, { responseType: 'arraybuffer' });
const fileBuffer = Buffer.from(response.data);
// Write the buffer to a temporary file
const tempFilePath = path.join(os.tmpdir(), 'temp_audio_file.mp3');
await fs.writeFile(tempFilePath, fileBuffer);
// Get Duration using music-metadata on the temporary file
const metadata = await mm.parseFile(tempFilePath, { duration: true });
const durationInSeconds = metadata.format.duration;
// Delete the temporary file after processing
await fs.unlink(tempFilePath);
res.status(200).json({ duration: durationInSeconds });
} catch (error) {
console.error('Server error:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
}
So currently the user is able to upload a file to cloudinary, and it will return the duration of the file. One major issue here is that overtime my Cloudinary will be filled with unused files as the song itself is not needed in Cloudinary after the duration is returned. I can’t seem to find a way to delete the file after the duration is returned.
This might help: Upload API Reference | Cloudinary
Thank you, I was finally able to get it to work properly after many many different attempts. If anyone ever wants to return an audio files duration and then delete the file after using this method(im sure there are better methods out there) then here is how I did it:
- Download Node.js
- Create a Node.js project in the terminal and create a package.json file by running the following command:
npm init
- create a .js file inside a subdirectory of your node js project(ex: myproj/api/duration.js)
- Paste the following code into the file:
const axios = require('axios');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const mm = require('music-metadata');
const cloudinary = require('cloudinary').v2;
export default async function handler(req, res) {
try {
// Log the entire request object
console.log('Received request:', req);
// Handle CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
if (req.method === 'OPTIONS') {
// Pre-flight request, respond successfully
console.log('Pre-flight request received');
res.status(200).end();
return;
}
// Parse the request body as JSON
let requestBody;
try {
requestBody = JSON.parse(req.body);
} catch (error) {
console.error('Error parsing request body as JSON:', error);
res.status(400).json({ error: 'Error parsing request body as JSON' });
return;
}
// Log the entire request body
console.log('Request body:', requestBody);
// Check if the request has a body
if (!requestBody || typeof requestBody !== 'object' || !requestBody.fileLink) {
console.error('Invalid request. No fileLink found in the request body.');
res.status(400).json({ error: 'Invalid request. No fileLink found in the request body.' });
return;
}
// Get the file link from the request body
const { fileLink } = requestBody;
console.log('File link:', fileLink);
// Extract public ID from fileLink using a regular expression
const publicIdMatch = fileLink.match(/\/([^\/]+)\.[a-zA-Z0-9]+$/);
const publicId = publicIdMatch ? publicIdMatch[1] : null;
if (!publicId) {
console.error('Unable to extract public ID from fileLink.');
res.status(400).json({ error: 'Unable to extract public ID from fileLink.' });
return;
}
console.log('Public ID:', publicId);
const fileTypeMatch = fileLink.match(/\.(mp3|mp4|mov|avi|wmv|flv)$/i);
const resourceType = fileTypeMatch ? 'video' : 'image'; // Assume 'image' if not audio/video
console.log('Resource Type:', resourceType);
// Download the file using axios
const response = await axios.get(fileLink, { responseType: 'arraybuffer' });
const fileBuffer = Buffer.from(response.data);
// Write the buffer to a temporary file
const tempFilePath = path.join(os.tmpdir(), 'temp_audio_file.mp3');
await fs.writeFile(tempFilePath, fileBuffer);
// Get Duration using music-metadata on the temporary file
const metadata = await mm.parseFile(tempFilePath, { duration: true });
const durationInSeconds = metadata.format.duration;
// Delete the temporary file after processing
await fs.unlink(tempFilePath);
// Delete the file from Cloudinary
const options = {
resource_type: resourceType, // Change to 'audio' if using audio files
invalidate: true,
api_key: 'your_cloudinary_api_key', // Directly pass the API key here
cloud_name: 'your_cloudinary_cloud_name' // Replace with your actual Cloudinary cloud name
};
// Generate Cloudinary signature
const timestamp = Math.floor(Date.now() / 1000);
const signature = cloudinary.utils.api_sign_request(
{ public_id: publicId, timestamp: timestamp, invalidate: true },
'your_cloudinary_api_secret'
);
// Make the Cloudinary request
const cloudinaryResponse = await cloudinary.uploader.destroy(publicId, { ...options, public_id: publicId, timestamp, signature });
console.log('Cloudinary response:', cloudinaryResponse);
console.log('API Key:', options.api_key);
res.status(200).json({ duration: durationInSeconds });
} catch (error) {
console.error('Server error:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
}
- Create a Vercel account, then install vercel in the Node.js terminal
npm install -g vercel
- run vercel by doing the following command in the terminal
vercel
- You will need to modify your package.json file in your main directory to the following:
{
"name": "your Vercel project name",
"version": "1.0.0",
"description": "",
"main": "duration.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.24.0",
"cloudinary": "^2.0.1",
"music-metadata": "^5.4.3"
}
}
- after you setup the vercel project, you want to type
vercel --prod
when this is done you want to login to vercel on your web browser and click on your project, you should see a domain that is similar to: projectname.vercel.app
- Now you want to setup the blocks in Thunkable, create a new webapi component and set the url to the “domain” plus the duration.js file directory. Ex: https://yourproject.vercel.app/api/duration.js. Then you want to setup the post as follows:
After all that(assuming I didn’t miss any steps, I did do alot of trial and error while figuring this out so I made this tutorial off of memory, I will edit it accordingly as I see fit.) it should return something like this: “{“duration”:duration in seconds}”
Again this is my first ever step by step tutorial for thunkable so please let me know if I missed any steps or if anyone is having any issues, I spent over 10 hours trying to figure this out.
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.