Skip to content

Replace http-server with fastify #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
"author": "Henrique Vianna <[email protected]> (https://henriquevianna.com)",
"license": "AGPL-3.0",
"description": "Media player and real-time audio spectrum analyzer",
"type": "module",
"scripts": {
"build": "webpack",
"start": "http-server",
"server": "node server/server.js",
"start": "node run server",
"dev": "webpack serve --config webpack.dev.js"
},
"devDependencies": {
"@fastify/static": "^8.2.0",
"audiomotion-analyzer": "^4.5.1",
"buffer": "^6.0.3",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
"http-server": "^14.1.1",
"fastify": "^5.4.0",
"idb-keyval": "^6.2.1",
"js-yaml": "^4.1.0",
"mini-css-extract-plugin": "^2.9.0",
Expand Down
3 changes: 3 additions & 0 deletions public/config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ enableLocalAccess: true
# Initial state of the Media Panel (“close” expands the analyzer area)
# open | close
frontPanel: open

# Location to shared media folder
mediaFolder: public\music
105 changes: 105 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// server/index.js
import Fastify from 'fastify';
import fastifyStatic from '@fastify/static';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';

// ESM __dirname workaround
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Paths
const rootDir = path.resolve(__dirname, '..');
const publicDir = path.join(rootDir, 'public');
const configPath = path.join(publicDir, 'config.yaml');

const app = Fastify({ logger: true });

const fileExtensionFilter = {
covers: /\.(jpg|jpeg|webp|avif|png|gif|bmp)$/i,
subs: /\.vtt$/i,
file: /.*/,
}

// 1. Load config.yaml and resolve mediaFolder
let musicDir = path.join(publicDir, 'music'); // fallback default

async function loadConfig() {
try {
const yamlRaw = await fs.readFile(configPath, 'utf8');
const config = loadYaml(yamlRaw);

if (config && typeof config.mediaFolder === 'string') {
// Resolve mediaFolder relative to rootDir
musicDir = path.resolve(rootDir, config.mediaFolder.replace(/^\/+/, ''));
app.log.info(`mediaFolder set to: ${musicDir}`);
}
} catch (err) {
app.log.warn(`Failed to load config.yaml: ${err.message}`);
}
}

await loadConfig();

// Serve entire /public as root
app.register(fastifyStatic, {
root: publicDir,
prefix: '/', // makes index.html available at /
index: ['index.html'],
});

app.get('/music/*', async (request, reply) => {
try {
// Extract requested path
const rawPath = request.params['*'] || '';
const decodedPath = decodeURIComponent(rawPath);
const safeSubPath = path.normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, '');

const targetPath = path.join(musicDir, safeSubPath);
const stat = await fs.stat(targetPath);

if (stat.isFile()) {
// Send the file (with correct headers)
return reply.sendFile(safeSubPath, musicDir); // Fastify Static will stream the file
}

if (!stat.isDirectory()) {
return reply.code(404).send({ error: 'Not a file or directory' });
}

// Handle directory listing
const entries = await fs.readdir(targetPath, { withFileTypes: true });

const dirs = [], files = [], imgs = [], subs = [];

for (const entry of entries) {
const name = entry.name;
if (entry.isDirectory()) {
dirs.push({ name });
} else if (entry.isFile()) {
if (name.match(fileExtensionFilter.covers)) imgs.push({ name });
else if (name.match(fileExtensionFilter.subs)) subs.push({ name });
if (name.match(fileExtensionFilter.file) && !name.startsWith('.')) files.push({ name });
}
}

reply.type('application/json').send({ dirs, files, imgs, subs });

} catch (err) {
if (err.code === 'ENOENT') {
reply.code(404).send({ error: 'Not found' });
} else {
app.log.error(err);
reply.code(500).send({ error: 'Internal server error' });
}
}
});

// Start server
app.listen({ port: 8080 }, err => {
if (err) {
app.log.error(err);
process.exit(1);
}
});
61 changes: 8 additions & 53 deletions src/file-explorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ export async function getDirectoryContents( path, dirHandle ) {
else {
// Web server
const response = await fetch( path );
content = response.ok ? await response.text() : false;
content = response.ok ? await response.json() : false;
}
}
catch( e ) {
Expand Down Expand Up @@ -365,58 +365,7 @@ export function parseWebIndex( content ) {
*/
export function parseDirectory( content, path ) {

// NOTE: `path` is currently used only to generate the correct `src` for subs files when
// reading an arbitrary directory. For all other files, only the filename is included.
//
// TO-DO: add an extra `path` or `src` property to *all* entries in the returned object,
// so we don't have to deal with this everywhere else!

const coverExtensions = /\.(jpg|jpeg|webp|avif|png|gif|bmp)$/i,
subsExtensions = /\.vtt$/i;

let dirs = [],
files = [],
imgs = [],
subs = [];

// helper function
const findImg = ( arr, pattern ) => {
const regexp = new RegExp( `${pattern}.*${coverExtensions.source}`, 'i' );
return arr.find( el => ( el.name || el ).match( regexp ) );
}

if ( Array.isArray( content ) && supportsFileSystemAPI ) {
// File System entries
for ( const fileObj of content ) {
const { name, handle, dirHandle } = fileObj;
if ( handle instanceof FileSystemDirectoryHandle )
dirs.push( fileObj );
else if ( handle instanceof FileSystemFileHandle ) {
if ( name.match( coverExtensions ) )
imgs.push( fileObj );
else if ( name.match( subsExtensions ) )
subs.push( fileObj );
if ( name.match( fileExtensions ) )
files.push( fileObj );
}
}
}
else {
// Web server HTML content
for ( const { url, file } of parseWebIndex( content ) ) {
const fileObj = { name: file };
if ( url.slice( -1 ) == '/' )
dirs.push( fileObj );
else {
if ( file.match( coverExtensions ) )
imgs.push( fileObj );
else if ( file.match( subsExtensions ) )
subs.push( fileObj );
if ( file.match( fileExtensions ) )
files.push( fileObj );
}
}
}
let {dirs, files, imgs, subs} = content;

// attach subtitle entries to their respective media files
for ( const sub of subs ) {
Expand All @@ -428,6 +377,12 @@ export function parseDirectory( content, path ) {
fileEntry.subs = { src: path ? path + name : makePath( name ), lang, handle };
}

// helper function
const findImg = (arr, pattern) => {
const regexp = new RegExp(pattern, 'i');
return arr.find(el => (el.name || el).match(regexp));
};

const cover = findImg( imgs, 'cover' ) || findImg( imgs, 'folder' ) || findImg( imgs, 'front' ) || imgs[0];

// case-insensitive sorting with international charset support - thanks https://stackoverflow.com/a/40390844/2370385
Expand Down
100 changes: 52 additions & 48 deletions webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,54 @@
const webpack = require('webpack');
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
import path from 'path';
import { fileURLToPath } from 'url';
import webpack from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';

module.exports = {
mode: 'production',
entry: './src/index.js',
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: { url: false }
}
]
}
]
},
optimization: {
minimizer: [ `...`, new CssMinimizerPlugin() ],
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
plugins: [
new MiniCssExtractPlugin({
filename: 'styles.css',
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser.js',
}),
],
output: {
filename: pathData => {
return pathData.chunk.name === 'main' ? 'audioMotion.js' : '[name].js';
},
path: path.resolve( __dirname, 'public' )
}
// ESM __dirname workaround
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
mode: 'production',
entry: './src/index.js',
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: { url: false },
},
],
},
],
},
optimization: {
minimizer: ['...', new CssMinimizerPlugin()],
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
plugins: [
new MiniCssExtractPlugin({
filename: 'styles.css',
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser.js',
}),
],
output: {
filename: pathData =>
pathData.chunk.name === 'main' ? 'audioMotion.js' : '[name].js',
path: path.resolve(__dirname, 'public'),
},
};
13 changes: 9 additions & 4 deletions webpack.dev.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
const path = require('path');
const webpack = require('webpack');
import path from 'path';
import { fileURLToPath } from 'url';
import webpack from 'webpack';

module.exports = {
// ESM __dirname workaround
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
mode: 'development',
entry: './src/index.js',
devtool: 'inline-source-map',
Expand All @@ -24,7 +29,7 @@ module.exports = {
{
test: /\.css$/,
use: [
'style-loader', // Use style-loader instead of MiniCssExtractPlugin in dev
'style-loader',
{
loader: 'css-loader',
options: { url: false },
Expand Down