Connor McCutcheon
/ Music
sample-server.mjs
mjs
#!/usr/bin/env node
import cowsay from 'cowsay';
import { createReadStream, existsSync, writeFileSync } from 'fs';
import { readdir } from 'fs/promises';
import http from 'http';
import { join, resolve, sep } from 'path';
import readline from 'readline';
import os from 'os';
const LOG = !!process.env.LOG || false;
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
const isAudioFile = (f) => {
  const ext = f.split('.').slice(-1)[0].toLowerCase();
  return VALID_AUDIO_EXTENSIONS.includes(ext);
};
async function getFilesInDirectory(directory) {
  let files = [];
  const dirents = await readdir(directory, { withFileTypes: true });
  for (const dirent of dirents) {
    const fullPath = join(directory, dirent.name);
    if (dirent.isDirectory()) {
      if (dirent.name.startsWith('.')) {
        LOG && console.warn(`ignore hidden folder: ${fullPath}`);
        continue;
      }
      try {
        const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile);
        files = files.concat(subFiles);
        LOG && console.log(`${dirent.name} (${subFiles.length})`);
      } catch (err) {
        LOG && console.warn(`skipped due to error: ${fullPath}`);
      }
    } else {
      isAudioFile(fullPath) && files.push(fullPath);
    }
  }
  return files;
}
async function getBanks(directory, flat = false) {
  let files = await getFilesInDirectory(directory);
  let banks = {};
  directory = directory.split(sep).join('/');
  files = files.map((path) => {
    path = path.split(sep).join('/');
    const subDir = path.replace(directory, '');
    const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore
    const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension
    let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0];
    banks[bank] = banks[bank] || [];
    banks[bank].push(subDir);
    return subDir;
  });
  banks._base = `http://localhost:5432`;
  return { banks, files };
}
const args = process.argv.slice(2);
function getArgValue(flag) {
  const i = args.indexOf(flag);
  if (i !== -1) {
    const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
    if (nextIsFlag) return true;
    return args[i + 1];
  }
}
function getInput(query) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
  return new Promise((resolve) =>
    rl.question(query, (response) => {
      rl.close();
      resolve(response);
    }),
  );
}
let directory = getArgValue('--dir') || process.cwd();
directory = resolve(directory);
if (args.includes('--json')) {
  const { banks } = await getBanks(directory, getArgValue('--flat'));
  const json = JSON.stringify(banks);
  const outFile = resolve(directory, 'strudel.json');
  if (existsSync(outFile)) {
    const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `);
    if (answer.toLowerCase() !== 'y') {
      console.log('Aborted.');
      process.exit(0);
    }
  }
  writeFileSync(outFile, json, 'utf8');
  console.log(`Wrote json to ${outFile}`);
}
console.log(
  cowsay.say({
    text: 'welcome to @strudel/sampler',
    e: 'oO',
    T: 'U ',
  }),
);
const server = http.createServer(async (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  const { banks, files } = await getBanks(directory, getArgValue('--flat'));
  if (req.url === '/') {
    res.setHeader('Content-Type', 'application/json');
    return res.end(JSON.stringify(banks));
  }
  let subpath = decodeURIComponent(req.url);
  const filePath = join(directory, subpath.split('/').join(sep));
  // console.log('GET:', filePath);
  const isFound = existsSync(filePath);
  if (!isFound) {
    res.statusCode = 404;
    res.end('File not found');
    return;
  }
  const readStream = createReadStream(filePath);
  readStream.on('error', (err) => {
    res.statusCode = 500;
    res.end('Internal server error');
    console.error(err);
  });
  readStream.pipe(res);
});
// eslint-disable-next-line
const PORT = process.env.PORT || 5432;
const IP_ADDRESS = '0.0.0.0';
let IP;
const networkInterfaces = os.networkInterfaces();
Object.keys(networkInterfaces).forEach((key) => {
  networkInterfaces[key].forEach((networkInterface) => {
    if (networkInterface.family === 'IPv4' && !networkInterface.internal) {
      IP = networkInterface.address;
    }
  });
});
server.listen(PORT, IP_ADDRESS, () => {
  console.log(`@strudel/sampler is now serving audio files from:
 ${directory}
To use them in the Strudel REPL, run:
 samples('http://localhost:${PORT}')
Or on a machine in the same network:
 ${IP ? `samples('http://${IP}:${PORT}')` : `Unable to determine server's IP address.`}
`);
});
No comments yet.