Skip to content

Commit abc4f51

Browse files
committed
Replace http-server with fastify
Communicate directory listings in JSON. Switch module from CommonJs to ECMAScript, consistent with front-end code.
1 parent a6b6e3e commit abc4f51

File tree

5 files changed

+159
-107
lines changed

5 files changed

+159
-107
lines changed

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55
"author": "Henrique Vianna <[email protected]> (https://henriquevianna.com)",
66
"license": "AGPL-3.0",
77
"description": "Media player and real-time audio spectrum analyzer",
8+
"type": "module",
89
"scripts": {
910
"build": "webpack",
10-
"start": "http-server",
11+
"server": "node server/server.js",
12+
"start": "node run server",
1113
"dev": "webpack serve --config webpack.dev.js"
1214
},
1315
"devDependencies": {
16+
"@fastify/static": "^8.2.0",
1417
"audiomotion-analyzer": "^4.5.1",
1518
"buffer": "^6.0.3",
1619
"css-loader": "^7.1.2",
1720
"css-minimizer-webpack-plugin": "^7.0.0",
18-
"http-server": "^14.1.1",
21+
"fastify": "^5.4.0",
1922
"idb-keyval": "^6.2.1",
2023
"mini-css-extract-plugin": "^2.9.0",
2124
"music-metadata-browser": "^2.5.10",

server/server.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// server/index.js
2+
import Fastify from 'fastify';
3+
import fastifyStatic from '@fastify/static';
4+
import fs from 'fs/promises';
5+
import path from 'path';
6+
import { fileURLToPath } from 'url';
7+
8+
// ESM __dirname workaround
9+
const __filename = fileURLToPath(import.meta.url);
10+
const __dirname = path.dirname(__filename);
11+
12+
// Paths
13+
const rootDir = path.resolve(__dirname, '..');
14+
const publicDir = path.join(rootDir, 'public');
15+
const musicDir = path.join(publicDir, 'music');
16+
17+
const app = Fastify({ logger: true });
18+
19+
const fileExtensionFilter = {
20+
covers: /\.(jpg|jpeg|webp|avif|png|gif|bmp)$/i,
21+
subs: /\.vtt$/i,
22+
file: /.*/,
23+
}
24+
25+
// Serve entire /public as root
26+
app.register(fastifyStatic, {
27+
root: publicDir,
28+
prefix: '/', // makes index.html available at /
29+
index: ['index.html'],
30+
});
31+
32+
app.get('/music/*', async (request, reply) => {
33+
try {
34+
// Extract requested path
35+
const rawPath = request.params['*'] || '';
36+
const decodedPath = decodeURIComponent(rawPath);
37+
const safeSubPath = path.normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, '');
38+
39+
const targetPath = path.join(musicDir, safeSubPath);
40+
const stat = await fs.stat(targetPath);
41+
42+
if (stat.isFile()) {
43+
// Send the file (with correct headers)
44+
return reply.sendFile(safeSubPath, musicDir); // Fastify Static will stream the file
45+
}
46+
47+
if (!stat.isDirectory()) {
48+
return reply.code(404).send({ error: 'Not a file or directory' });
49+
}
50+
51+
// Handle directory listing
52+
const entries = await fs.readdir(targetPath, { withFileTypes: true });
53+
54+
const dirs = [], files = [], imgs = [], subs = [];
55+
56+
for (const entry of entries) {
57+
const name = entry.name;
58+
if (entry.isDirectory()) {
59+
dirs.push({ name });
60+
} else if (entry.isFile()) {
61+
if (name.match(fileExtensionFilter.covers)) imgs.push({ name });
62+
else if (name.match(fileExtensionFilter.subs)) subs.push({ name });
63+
if (name.match(fileExtensionFilter.file)) files.push({ name });
64+
}
65+
}
66+
67+
reply.type('application/json').send({ dirs, files, imgs, subs });
68+
69+
} catch (err) {
70+
if (err.code === 'ENOENT') {
71+
reply.code(404).send({ error: 'Not found' });
72+
} else {
73+
app.log.error(err);
74+
reply.code(500).send({ error: 'Internal server error' });
75+
}
76+
}
77+
});
78+
79+
// 3. Start server
80+
app.listen({ port: 8080 }, err => {
81+
if (err) {
82+
app.log.error(err);
83+
process.exit(1);
84+
}
85+
});

src/file-explorer.js

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ export async function getDirectoryContents( path, dirHandle ) {
268268
else {
269269
// Web server
270270
const response = await fetch( path );
271-
content = response.ok ? await response.text() : false;
271+
content = response.ok ? await response.json() : false;
272272
}
273273
}
274274
catch( e ) {
@@ -365,58 +365,7 @@ export function parseWebIndex( content ) {
365365
*/
366366
export function parseDirectory( content, path ) {
367367

368-
// NOTE: `path` is currently used only to generate the correct `src` for subs files when
369-
// reading an arbitrary directory. For all other files, only the filename is included.
370-
//
371-
// TO-DO: add an extra `path` or `src` property to *all* entries in the returned object,
372-
// so we don't have to deal with this everywhere else!
373-
374-
const coverExtensions = /\.(jpg|jpeg|webp|avif|png|gif|bmp)$/i,
375-
subsExtensions = /\.vtt$/i;
376-
377-
let dirs = [],
378-
files = [],
379-
imgs = [],
380-
subs = [];
381-
382-
// helper function
383-
const findImg = ( arr, pattern ) => {
384-
const regexp = new RegExp( `${pattern}.*${coverExtensions.source}`, 'i' );
385-
return arr.find( el => ( el.name || el ).match( regexp ) );
386-
}
387-
388-
if ( Array.isArray( content ) && supportsFileSystemAPI ) {
389-
// File System entries
390-
for ( const fileObj of content ) {
391-
const { name, handle, dirHandle } = fileObj;
392-
if ( handle instanceof FileSystemDirectoryHandle )
393-
dirs.push( fileObj );
394-
else if ( handle instanceof FileSystemFileHandle ) {
395-
if ( name.match( coverExtensions ) )
396-
imgs.push( fileObj );
397-
else if ( name.match( subsExtensions ) )
398-
subs.push( fileObj );
399-
if ( name.match( fileExtensions ) )
400-
files.push( fileObj );
401-
}
402-
}
403-
}
404-
else {
405-
// Web server HTML content
406-
for ( const { url, file } of parseWebIndex( content ) ) {
407-
const fileObj = { name: file };
408-
if ( url.slice( -1 ) == '/' )
409-
dirs.push( fileObj );
410-
else {
411-
if ( file.match( coverExtensions ) )
412-
imgs.push( fileObj );
413-
else if ( file.match( subsExtensions ) )
414-
subs.push( fileObj );
415-
if ( file.match( fileExtensions ) )
416-
files.push( fileObj );
417-
}
418-
}
419-
}
368+
let {dirs, files, imgs, subs} = content;
420369

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

380+
// helper function
381+
const findImg = (arr, pattern) => {
382+
const regexp = new RegExp(pattern, 'i');
383+
return arr.find(el => (el.name || el).match(regexp));
384+
};
385+
431386
const cover = findImg( imgs, 'cover' ) || findImg( imgs, 'folder' ) || findImg( imgs, 'front' ) || imgs[0];
432387

433388
// case-insensitive sorting with international charset support - thanks https://stackoverflow.com/a/40390844/2370385

webpack.config.js

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,54 @@
1-
const webpack = require('webpack');
2-
const path = require('path');
3-
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
4-
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
1+
import path from 'path';
2+
import { fileURLToPath } from 'url';
3+
import webpack from 'webpack';
4+
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
5+
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
56

6-
module.exports = {
7-
mode: 'production',
8-
entry: './src/index.js',
9-
module: {
10-
rules: [
11-
{
12-
test: /\.css$/,
13-
use: [
14-
MiniCssExtractPlugin.loader,
15-
{
16-
loader: 'css-loader',
17-
options: { url: false }
18-
}
19-
]
20-
}
21-
]
22-
},
23-
optimization: {
24-
minimizer: [ `...`, new CssMinimizerPlugin() ],
25-
splitChunks: {
26-
cacheGroups: {
27-
vendor: {
28-
test: /[\\/]node_modules[\\/]/,
29-
name: 'vendors',
30-
chunks: 'all',
31-
},
32-
},
33-
},
34-
},
35-
plugins: [
36-
new MiniCssExtractPlugin({
37-
filename: 'styles.css',
38-
}),
39-
new webpack.ProvidePlugin({
40-
Buffer: ['buffer', 'Buffer'],
41-
process: 'process/browser.js',
42-
}),
43-
],
44-
output: {
45-
filename: pathData => {
46-
return pathData.chunk.name === 'main' ? 'audioMotion.js' : '[name].js';
47-
},
48-
path: path.resolve( __dirname, 'public' )
49-
}
7+
// ESM __dirname workaround
8+
const __filename = fileURLToPath(import.meta.url);
9+
const __dirname = path.dirname(__filename);
10+
11+
export default {
12+
mode: 'production',
13+
entry: './src/index.js',
14+
module: {
15+
rules: [
16+
{
17+
test: /\.css$/,
18+
use: [
19+
MiniCssExtractPlugin.loader,
20+
{
21+
loader: 'css-loader',
22+
options: { url: false },
23+
},
24+
],
25+
},
26+
],
27+
},
28+
optimization: {
29+
minimizer: ['...', new CssMinimizerPlugin()],
30+
splitChunks: {
31+
cacheGroups: {
32+
vendor: {
33+
test: /[\\/]node_modules[\\/]/,
34+
name: 'vendors',
35+
chunks: 'all',
36+
},
37+
},
38+
},
39+
},
40+
plugins: [
41+
new MiniCssExtractPlugin({
42+
filename: 'styles.css',
43+
}),
44+
new webpack.ProvidePlugin({
45+
Buffer: ['buffer', 'Buffer'],
46+
process: 'process/browser.js',
47+
}),
48+
],
49+
output: {
50+
filename: pathData =>
51+
pathData.chunk.name === 'main' ? 'audioMotion.js' : '[name].js',
52+
path: path.resolve(__dirname, 'public'),
53+
},
5054
};

webpack.dev.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
const path = require('path');
2-
const webpack = require('webpack');
1+
import path from 'path';
2+
import { fileURLToPath } from 'url';
3+
import webpack from 'webpack';
34

4-
module.exports = {
5+
// ESM __dirname workaround
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
9+
export default {
510
mode: 'development',
611
entry: './src/index.js',
712
devtool: 'inline-source-map',
@@ -19,7 +24,7 @@ module.exports = {
1924
{
2025
test: /\.css$/,
2126
use: [
22-
'style-loader', // Use style-loader instead of MiniCssExtractPlugin in dev
27+
'style-loader',
2328
{
2429
loader: 'css-loader',
2530
options: { url: false },

0 commit comments

Comments
 (0)