Add configurable library path with settings API and UI
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ node_modules/
|
|||||||
audio_library/
|
audio_library/
|
||||||
uploads/
|
uploads/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
config.json
|
||||||
|
|||||||
3158
package-lock.json
generated
Normal file
3158
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ const API_BASE = window.location.origin;
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
updateSubsonicUrl();
|
updateSubsonicUrl();
|
||||||
|
loadSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Switch between tabs
|
// Switch between tabs
|
||||||
@@ -296,3 +297,48 @@ function updateSubsonicUrl() {
|
|||||||
const url = `${window.location.protocol}//${window.location.hostname}:${window.location.port}/rest`;
|
const url = `${window.location.protocol}//${window.location.hostname}:${window.location.port}/rest`;
|
||||||
document.getElementById('subsonicUrl').value = url;
|
document.getElementById('subsonicUrl').value = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load settings
|
||||||
|
async function loadSettings() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/api/settings`);
|
||||||
|
const settings = await res.json();
|
||||||
|
document.getElementById('libraryPath').value = settings.libraryPath;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading settings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save library path
|
||||||
|
async function saveLibraryPath() {
|
||||||
|
const libraryPath = document.getElementById('libraryPath').value;
|
||||||
|
|
||||||
|
if (!libraryPath) {
|
||||||
|
alert('Please enter a library path');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/api/settings`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ libraryPath })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
alert('Library path updated! Please restart the server for changes to take effect.');
|
||||||
|
} else {
|
||||||
|
alert(`Error: ${data.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving settings:', error);
|
||||||
|
alert('Failed to save settings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browse for folder (note: web browsers can't actually browse filesystem)
|
||||||
|
function browseFolder() {
|
||||||
|
alert('Enter the full path to your audio library folder.\n\nExamples:\n- /Users/username/Music\n- /home/user/music\n- C:\\Users\\username\\Music\n- ./audio_library (relative to server)');
|
||||||
|
}
|
||||||
|
|||||||
@@ -358,10 +358,16 @@
|
|||||||
<h2>Library Settings</h2>
|
<h2>Library Settings</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Library Path</label>
|
<label>Library Path</label>
|
||||||
<input type="text" id="libraryPath" value="./audio_library" readonly>
|
<div style="display: flex; gap: 10px;">
|
||||||
<small style="color: #86868b;">Audio files location on server</small>
|
<input type="text" id="libraryPath" placeholder="/path/to/your/music" style="flex: 1; margin-bottom: 0;">
|
||||||
|
<button class="btn btn-secondary" onclick="browseFolder()" style="white-space: nowrap;">💡 Help</button>
|
||||||
|
</div>
|
||||||
|
<small style="color: #86868b;">Full path or relative path to your audio files</small>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<button class="btn btn-primary" onclick="saveLibraryPath()">Save Path</button>
|
||||||
|
<button class="btn btn-secondary" onclick="scanLibrary()">Scan Library</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="scanLibrary()">Scan Library Now</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
64
src/routes/settings.js
Normal file
64
src/routes/settings.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const CONFIG_FILE = path.join(process.cwd(), 'config.json');
|
||||||
|
|
||||||
|
// Default config
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
libraryPath: './audio_library',
|
||||||
|
port: 3000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(CONFIG_FILE, 'utf-8');
|
||||||
|
return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
|
||||||
|
} catch (error) {
|
||||||
|
return DEFAULT_CONFIG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save config
|
||||||
|
async function saveConfig(config) {
|
||||||
|
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get settings
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const config = await loadConfig();
|
||||||
|
res.json(config);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update settings
|
||||||
|
router.patch('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const config = await loadConfig();
|
||||||
|
const { libraryPath } = req.body;
|
||||||
|
|
||||||
|
if (libraryPath) {
|
||||||
|
// Validate path exists
|
||||||
|
try {
|
||||||
|
await fs.access(libraryPath);
|
||||||
|
config.libraryPath = libraryPath;
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(400).json({ error: 'Library path does not exist' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveConfig(config);
|
||||||
|
res.json({ success: true, config });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
export { loadConfig };
|
||||||
@@ -7,13 +7,18 @@ import playlistsRouter from './routes/playlists.js';
|
|||||||
import albumsRouter from './routes/albums.js';
|
import albumsRouter from './routes/albums.js';
|
||||||
import uploadRouter from './routes/upload.js';
|
import uploadRouter from './routes/upload.js';
|
||||||
import subsonicRouter from './routes/subsonic.js';
|
import subsonicRouter from './routes/subsonic.js';
|
||||||
|
import settingsRouter from './routes/settings.js';
|
||||||
import { initDatabase } from './database/db.js';
|
import { initDatabase } from './database/db.js';
|
||||||
|
import { loadConfig } from './routes/settings.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
|
||||||
|
// Load config
|
||||||
|
const config = await loadConfig();
|
||||||
|
const PORT = process.env.PORT || config.port || 3000;
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
@@ -27,6 +32,7 @@ app.use('/api/tracks', tracksRouter);
|
|||||||
app.use('/api/playlists', playlistsRouter);
|
app.use('/api/playlists', playlistsRouter);
|
||||||
app.use('/api/albums', albumsRouter);
|
app.use('/api/albums', albumsRouter);
|
||||||
app.use('/api/upload', uploadRouter);
|
app.use('/api/upload', uploadRouter);
|
||||||
|
app.use('/api/settings', settingsRouter);
|
||||||
|
|
||||||
// Subsonic API
|
// Subsonic API
|
||||||
app.use('/rest', subsonicRouter);
|
app.use('/rest', subsonicRouter);
|
||||||
@@ -34,8 +40,11 @@ app.use('/rest', subsonicRouter);
|
|||||||
// Serve static files
|
// Serve static files
|
||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
|
|
||||||
// Serve audio files
|
// Serve audio files (use configured path)
|
||||||
app.use('/audio', express.static(path.join(process.cwd(), 'audio_library')));
|
const libraryPath = path.isAbsolute(config.libraryPath)
|
||||||
|
? config.libraryPath
|
||||||
|
: path.join(process.cwd(), config.libraryPath);
|
||||||
|
app.use('/audio', express.static(libraryPath));
|
||||||
app.use('/artwork', express.static(path.join(process.cwd(), 'artwork')));
|
app.use('/artwork', express.static(path.join(process.cwd(), 'artwork')));
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
@@ -45,5 +54,6 @@ app.get('/health', (req, res) => {
|
|||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`🎵 CarPlay Audio Server running on port ${PORT}`);
|
console.log(`🎵 CarPlay Audio Server running on port ${PORT}`);
|
||||||
console.log(`📁 Audio library: ${path.join(process.cwd(), 'audio_library')}`);
|
console.log(`📁 Audio library: ${libraryPath}`);
|
||||||
|
console.log(`🌐 Admin panel: http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user