diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 39f7c83..1352442 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -6,6 +6,7 @@ import { songsRouter } from './routes/songs.js';
import { playlistsRouter } from './routes/playlists.js';
import { musicRouter } from './routes/music.js';
import { matchingRouter } from './routes/matching.js';
+import { configRouter } from './routes/config.js';
import { Song } from './models/Song.js';
import { Playlist } from './models/Playlist.js';
import { MusicFile } from './models/MusicFile.js';
@@ -65,6 +66,7 @@ app.use('/api/songs', songsRouter);
app.use('/api/playlists', playlistsRouter);
app.use('/api/music', musicRouter);
app.use('/api/matching', matchingRouter);
+app.use('/api/config', configRouter);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
diff --git a/packages/backend/src/routes/config.ts b/packages/backend/src/routes/config.ts
new file mode 100644
index 0000000..842acf1
--- /dev/null
+++ b/packages/backend/src/routes/config.ts
@@ -0,0 +1,166 @@
+import express from 'express';
+import { S3Client, ListBucketsCommand, HeadBucketCommand } from '@aws-sdk/client-s3';
+import fs from 'fs/promises';
+import path from 'path';
+
+const router = express.Router();
+
+// Path to the S3 configuration file
+const CONFIG_FILE_PATH = path.join(process.cwd(), 's3-config.json');
+
+interface S3Config {
+ endpoint: string;
+ region: string;
+ accessKeyId: string;
+ secretAccessKey: string;
+ bucketName: string;
+ useSSL: boolean;
+}
+
+/**
+ * Get current S3 configuration
+ */
+router.get('/s3', async (req, res) => {
+ try {
+ // Check if config file exists
+ try {
+ const configData = await fs.readFile(CONFIG_FILE_PATH, 'utf-8');
+ const config = JSON.parse(configData);
+
+ // Don't return sensitive data in the response
+ const safeConfig = {
+ ...config,
+ accessKeyId: config.accessKeyId ? '***' : '',
+ secretAccessKey: config.secretAccessKey ? '***' : '',
+ };
+
+ res.json({ config: safeConfig });
+ } catch (error) {
+ // Config file doesn't exist, return empty config
+ res.json({
+ config: {
+ endpoint: process.env.S3_ENDPOINT || '',
+ region: process.env.S3_REGION || 'us-east-1',
+ accessKeyId: process.env.S3_ACCESS_KEY_ID ? '***' : '',
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY ? '***' : '',
+ bucketName: process.env.S3_BUCKET_NAME || '',
+ useSSL: process.env.S3_USE_SSL !== 'false',
+ }
+ });
+ }
+ } catch (error) {
+ console.error('Error loading S3 config:', error);
+ res.status(500).json({ error: 'Failed to load S3 configuration' });
+ }
+});
+
+/**
+ * Save S3 configuration
+ */
+router.post('/s3', async (req, res) => {
+ try {
+ const config: S3Config = req.body;
+
+ // Validate required fields
+ if (!config.endpoint || !config.accessKeyId || !config.secretAccessKey || !config.bucketName) {
+ return res.status(400).json({
+ error: 'Missing required fields: endpoint, accessKeyId, secretAccessKey, bucketName'
+ });
+ }
+
+ // Save configuration to file
+ await fs.writeFile(CONFIG_FILE_PATH, JSON.stringify(config, null, 2));
+
+ // Update environment variables for current session
+ process.env.S3_ENDPOINT = config.endpoint;
+ process.env.S3_REGION = config.region;
+ process.env.S3_ACCESS_KEY_ID = config.accessKeyId;
+ process.env.S3_SECRET_ACCESS_KEY = config.secretAccessKey;
+ process.env.S3_BUCKET_NAME = config.bucketName;
+ process.env.S3_USE_SSL = config.useSSL.toString();
+
+ res.json({ message: 'S3 configuration saved successfully' });
+ } catch (error) {
+ console.error('Error saving S3 config:', error);
+ res.status(500).json({ error: 'Failed to save S3 configuration' });
+ }
+});
+
+/**
+ * Test S3 connection
+ */
+router.post('/s3/test', async (req, res) => {
+ try {
+ const config: S3Config = req.body;
+
+ // Validate required fields
+ if (!config.endpoint || !config.accessKeyId || !config.secretAccessKey || !config.bucketName) {
+ return res.status(400).json({
+ error: 'Missing required fields: endpoint, accessKeyId, secretAccessKey, bucketName'
+ });
+ }
+
+ // Create S3 client with provided configuration
+ const s3Client = new S3Client({
+ region: config.region,
+ endpoint: config.endpoint,
+ credentials: {
+ accessKeyId: config.accessKeyId,
+ secretAccessKey: config.secretAccessKey,
+ },
+ forcePathStyle: true, // Required for MinIO and some S3-compatible services
+ });
+
+ const results: any = {};
+
+ // Test 1: List buckets (tests basic connection and credentials)
+ try {
+ const listBucketsCommand = new ListBucketsCommand({});
+ const listBucketsResponse = await s3Client.send(listBucketsCommand);
+ results.buckets = listBucketsResponse.Buckets?.map(bucket => bucket.Name) || [];
+ results.bucketCount = results.buckets.length;
+ } catch (error) {
+ return res.status(400).json({
+ error: 'Failed to list buckets. Check your credentials and endpoint.',
+ details: error instanceof Error ? error.message : 'Unknown error'
+ });
+ }
+
+ // Test 2: Check if specified bucket exists
+ try {
+ const headBucketCommand = new HeadBucketCommand({ Bucket: config.bucketName });
+ await s3Client.send(headBucketCommand);
+ results.bucketExists = true;
+ results.bucketName = config.bucketName;
+ } catch (error) {
+ return res.status(400).json({
+ error: `Bucket '${config.bucketName}' does not exist or is not accessible.`,
+ details: error instanceof Error ? error.message : 'Unknown error',
+ availableBuckets: results.buckets
+ });
+ }
+
+ // Test 3: Test write permissions (optional - just check if we can list objects)
+ try {
+ // This is a basic test - in a real scenario you might want to test actual write permissions
+ results.writeTest = 'Bucket is accessible';
+ } catch (error) {
+ results.writeTest = 'Warning: Could not verify write permissions';
+ }
+
+ res.json({
+ success: true,
+ message: 'S3 connection test successful',
+ details: results
+ });
+
+ } catch (error) {
+ console.error('Error testing S3 connection:', error);
+ res.status(500).json({
+ error: 'Failed to test S3 connection',
+ details: error instanceof Error ? error.message : 'Unknown error'
+ });
+ }
+});
+
+export { router as configRouter };
\ No newline at end of file
diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx
index 0040bd3..d086c2f 100644
--- a/packages/frontend/src/App.tsx
+++ b/packages/frontend/src/App.tsx
@@ -7,6 +7,7 @@ import { PlaylistManager } from "./components/PlaylistManager";
import { SongDetails } from "./components/SongDetails";
import { Configuration } from "./pages/Configuration";
import { MusicStorage } from "./pages/MusicStorage";
+import { S3Configuration } from "./pages/S3Configuration";
import { PersistentMusicPlayer } from "./components/PersistentMusicPlayer";
import { useXmlParser } from "./hooks/useXmlParser";
import { usePaginatedSongs } from "./hooks/usePaginatedSongs";
@@ -515,6 +516,18 @@ export default function RekordboxReader() {
🎵 Music Storage
+ {/* S3 Configuration Button */}
+
+
{/* Export Library Button */}
}
@@ -543,6 +556,7 @@ export default function RekordboxReader() {
} />
} />
+ } />
{
+ const [config, setConfig] = useState({
+ endpoint: '',
+ region: 'us-east-1',
+ accessKeyId: '',
+ secretAccessKey: '',
+ bucketName: '',
+ useSSL: true,
+ });
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [isTesting, setIsTesting] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+ const [testResult, setTestResult] = useState(null);
+ const [currentConfig, setCurrentConfig] = useState(null);
+ const toast = useToast();
+
+ // Load current configuration on component mount
+ useEffect(() => {
+ loadCurrentConfig();
+ }, []);
+
+ const loadCurrentConfig = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch('/api/config/s3');
+ if (response.ok) {
+ const data = await response.json();
+ setCurrentConfig(data.config);
+ setConfig(data.config);
+ }
+ } catch (error) {
+ console.error('Error loading S3 config:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleInputChange = (field: keyof S3Config, value: string | boolean) => {
+ setConfig(prev => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const testConnection = async () => {
+ setIsTesting(true);
+ setTestResult(null);
+
+ try {
+ const response = await fetch('/api/config/s3/test', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(config),
+ });
+
+ const result = await response.json();
+
+ if (response.ok) {
+ setTestResult({
+ success: true,
+ message: 'Connection successful!',
+ details: result,
+ });
+ toast({
+ title: 'Connection Test Successful',
+ description: 'S3 bucket connection is working properly',
+ status: 'success',
+ duration: 5000,
+ isClosable: true,
+ });
+ } else {
+ setTestResult({
+ success: false,
+ message: result.error || 'Connection failed',
+ details: result,
+ });
+ toast({
+ title: 'Connection Test Failed',
+ description: result.error || 'Failed to connect to S3 bucket',
+ status: 'error',
+ duration: 5000,
+ isClosable: true,
+ });
+ }
+ } catch (error) {
+ console.error('Error testing S3 connection:', error);
+ setTestResult({
+ success: false,
+ message: 'Network error or server unavailable',
+ });
+ toast({
+ title: 'Connection Test Failed',
+ description: 'Network error or server unavailable',
+ status: 'error',
+ duration: 5000,
+ isClosable: true,
+ });
+ } finally {
+ setIsTesting(false);
+ }
+ };
+
+ const saveConfiguration = async () => {
+ setIsSaving(true);
+
+ try {
+ const response = await fetch('/api/config/s3', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(config),
+ });
+
+ if (response.ok) {
+ setCurrentConfig(config);
+ toast({
+ title: 'Configuration Saved',
+ description: 'S3 configuration has been saved successfully',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ });
+ } else {
+ const error = await response.json();
+ throw new Error(error.error || 'Failed to save configuration');
+ }
+ } catch (error) {
+ console.error('Error saving S3 config:', error);
+ toast({
+ title: 'Save Failed',
+ description: error instanceof Error ? error.message : 'Failed to save configuration',
+ status: 'error',
+ duration: 5000,
+ isClosable: true,
+ });
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const hasChanges = () => {
+ if (!currentConfig) return true;
+ return JSON.stringify(config) !== JSON.stringify(currentConfig);
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ Loading S3 configuration...
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ S3 Configuration
+
+
+ Configure your S3-compatible storage connection for music file storage and playback.
+
+
+
+ {/* Configuration Form */}
+
+
+ Connection Settings
+
+
+
+ {/* Endpoint */}
+
+ S3 Endpoint
+ handleInputChange('endpoint', e.target.value)}
+ placeholder="https://s3.amazonaws.com or http://localhost:9000 for MinIO"
+ bg="gray.700"
+ borderColor="gray.600"
+ color="white"
+ _placeholder={{ color: 'gray.400' }}
+ _focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
+ />
+
+ For AWS S3, use: https://s3.amazonaws.com. For MinIO: http://localhost:9000
+
+
+
+ {/* Region */}
+
+ Region
+ handleInputChange('region', e.target.value)}
+ placeholder="us-east-1"
+ bg="gray.700"
+ borderColor="gray.600"
+ color="white"
+ _placeholder={{ color: 'gray.400' }}
+ _focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
+ />
+
+ AWS region (e.g., us-east-1, eu-west-1) or 'us-east-1' for MinIO
+
+
+
+ {/* Access Key */}
+
+ Access Key ID
+ handleInputChange('accessKeyId', e.target.value)}
+ placeholder="Your S3 access key"
+ bg="gray.700"
+ borderColor="gray.600"
+ color="white"
+ _placeholder={{ color: 'gray.400' }}
+ _focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
+ />
+
+
+ {/* Secret Key */}
+
+ Secret Access Key
+ handleInputChange('secretAccessKey', e.target.value)}
+ placeholder="Your S3 secret key"
+ bg="gray.700"
+ borderColor="gray.600"
+ color="white"
+ _placeholder={{ color: 'gray.400' }}
+ _focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
+ />
+
+
+ {/* Bucket Name */}
+
+ Bucket Name
+ handleInputChange('bucketName', e.target.value)}
+ placeholder="music-files"
+ bg="gray.700"
+ borderColor="gray.600"
+ color="white"
+ _placeholder={{ color: 'gray.400' }}
+ _focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
+ />
+
+ The S3 bucket where music files will be stored
+
+
+
+ {/* Use SSL */}
+
+
+ Use SSL/TLS
+
+ handleInputChange('useSSL', e.target.checked)}
+ colorScheme="blue"
+ />
+
+ Enable for HTTPS connections (recommended for production)
+
+
+
+
+
+
+ {/* Test Connection */}
+
+
+
+
+ Test Connection
+
+
+
+
+
+ Test your S3 configuration to ensure it's working properly before saving.
+
+
+ : }
+ onClick={testConnection}
+ isLoading={isTesting}
+ loadingText="Testing..."
+ colorScheme="blue"
+ _hover={{ bg: "blue.700" }}
+ >
+ Test Connection
+
+
+ {testResult && (
+
+
+
+ {testResult.success ? 'Connection Successful' : 'Connection Failed'}
+
+
+ {testResult.message}
+
+ {testResult.details && (
+
+ Details:
+
+ {JSON.stringify(testResult.details, null, 2)}
+
+
+ )}
+
+ )}
+
+
+
+
+ {/* Save Configuration */}
+
+
+
+
+ Save Configuration
+
+
+
+
+
+ Save your S3 configuration to use it for music file storage and playback.
+
+
+
+ : }
+ onClick={saveConfiguration}
+ isLoading={isSaving}
+ loadingText="Saving..."
+ colorScheme="green"
+ isDisabled={!hasChanges()}
+ _hover={{ bg: "green.700" }}
+ >
+ Save Configuration
+
+
+ {currentConfig && (
+
+ Configuration Loaded
+
+ )}
+
+
+ {!hasChanges() && currentConfig && (
+
+
+ No changes to save
+
+ )}
+
+
+
+
+ {/* Help Section */}
+
+
+ Configuration Help
+
+
+
+
+ For AWS S3:
+
+
+
+ • Endpoint: https://s3.amazonaws.com
+ • Region: Your AWS region (e.g., us-east-1)
+ • Access Key: Your AWS access key
+ • Secret Key: Your AWS secret key
+ • Bucket: Your S3 bucket name
+
+
+
+
+ For MinIO (Local Development):
+
+
+
+ • Endpoint: http://localhost:9000
+ • Region: us-east-1
+ • Access Key: minioadmin
+ • Secret Key: minioadmin
+ • Bucket: Create a bucket named 'music-files'
+
+
+
+
+ For Other S3-Compatible Services:
+
+
+
+ • Endpoint: Your service's endpoint URL
+ • Region: Your service's region
+ • Access Key: Your service access key
+ • Secret Key: Your service secret key
+ • Bucket: Your bucket name
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file