This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
build
|
build
|
||||||
output
|
output
|
||||||
server
|
node_modules
|
||||||
|
|
||||||
# Object files
|
# Object files
|
||||||
*.o
|
*.o
|
||||||
|
|||||||
57
server/README.md
Normal file
57
server/README.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# mice Download Server
|
||||||
|
|
||||||
|
Simple Node.js server to host mice 3DS homebrew files for easy download via QR code.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Install dependencies:**
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build mice first:****
|
||||||
|
```bash
|
||||||
|
cd ..
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the server:**
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Access the download page:**
|
||||||
|
- Open browser to `http://localhost:3000`
|
||||||
|
- Use QR codes to download on 3DS browser
|
||||||
|
- Or find your local IP and access from any device on the network
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📱 QR codes for easy 3DS browser access
|
||||||
|
- 📦 Automatic detection of build files (.3dsx, .cia, .3ds, .elf)
|
||||||
|
- 🎮 Installation instructions included
|
||||||
|
- 📊 File size information
|
||||||
|
- 🌐 Network accessible from any device
|
||||||
|
|
||||||
|
## Usage on 3DS
|
||||||
|
|
||||||
|
1. Connect your 3DS to the same WiFi network as your computer
|
||||||
|
2. Open the Internet Browser on your 3DS
|
||||||
|
3. Scan the QR code or manually enter the URL
|
||||||
|
4. Download the .3dsx or .cia file
|
||||||
|
5. Install according to the instructions on the page
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # Uses nodemon for auto-restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Types
|
||||||
|
|
||||||
|
- **.3dsx** - Homebrew Launcher format (copy to `/3ds/` folder)
|
||||||
|
- **.cia** - Installable format (use FBI or similar installer)
|
||||||
|
- **.3ds** - 3DS ROM format
|
||||||
|
- **.elf** - Debug/development format
|
||||||
1465
server/package-lock.json
generated
Normal file
1465
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
server/package.json
Normal file
20
server/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "mice-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Simple file server for mice 3DS homebrew downloads",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"qrcode": "^1.5.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.2"
|
||||||
|
},
|
||||||
|
"keywords": ["3ds", "homebrew", "file-server"],
|
||||||
|
"author": "3DS Dev",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
346
server/server.js
Normal file
346
server/server.js
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const QRCode = require('qrcode');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Function to read current version from main.h
|
||||||
|
function getCurrentVersion() {
|
||||||
|
try {
|
||||||
|
const mainHPath = path.join(__dirname, '../include/main.h');
|
||||||
|
const content = fs.readFileSync(mainHPath, 'utf8');
|
||||||
|
const versionMatch = content.match(/#define\s+MICE_VERSION\s+"([^"]+)"/);
|
||||||
|
return versionMatch ? versionMatch[1] : 'unknown';
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Could not read version:', error.message);
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get build timestamp
|
||||||
|
function getBuildTimestamp() {
|
||||||
|
try {
|
||||||
|
const outputDir = path.join(__dirname, '../output');
|
||||||
|
if (fs.existsSync(outputDir)) {
|
||||||
|
const stats = fs.statSync(outputDir);
|
||||||
|
return stats.mtime.toLocaleString();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Could not read build timestamp:', error.message);
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve static files from the output directory
|
||||||
|
app.use('/downloads', express.static(path.join(__dirname, '../output')));
|
||||||
|
|
||||||
|
// Basic styling for the web interface with dark mode support
|
||||||
|
const CSS = `
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #f5f5f5;
|
||||||
|
--container-bg: white;
|
||||||
|
--text-color: #333;
|
||||||
|
--border-color: #ddd;
|
||||||
|
--section-bg: #fafafa;
|
||||||
|
--accent-color: #0066cc;
|
||||||
|
--button-hover: #0052a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg-color: #1a1a1a;
|
||||||
|
--container-bg: #2d2d2d;
|
||||||
|
--text-color: #e0e0e0;
|
||||||
|
--border-color: #555;
|
||||||
|
--section-bg: #3a3a3a;
|
||||||
|
--accent-color: #4da6ff;
|
||||||
|
--button-hover: #3d8bff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: var(--container-bg);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 20px rgba(0,0,0,0.3);
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: var(--text-color);
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 3px solid var(--accent-color);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.version-info {
|
||||||
|
text-align: center;
|
||||||
|
margin: 10px 0 20px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--section-bg);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
.download-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--section-bg);
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
.download-link {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 10px 10px 0;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
.download-link:hover {
|
||||||
|
background-color: var(--button-hover);
|
||||||
|
}
|
||||||
|
.download-link:hover {
|
||||||
|
background-color: #0052a3;
|
||||||
|
}
|
||||||
|
.qr-code {
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.file-info {
|
||||||
|
background-color: #e8f4fd;
|
||||||
|
padding: 15px;
|
||||||
|
border-left: 4px solid #0066cc;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
.installation-guide {
|
||||||
|
background-color: #fff8dc;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.file-info {
|
||||||
|
background-color: #1a2940;
|
||||||
|
border-left-color: #2d5aa0;
|
||||||
|
}
|
||||||
|
.installation-guide {
|
||||||
|
background-color: #2a3b5c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
|
||||||
|
// Main page
|
||||||
|
app.get('/', async (req, res) => {
|
||||||
|
const outputDir = path.join(__dirname, '../output');
|
||||||
|
|
||||||
|
// Use the client's actual request URL
|
||||||
|
const protocol = req.get('x-forwarded-proto') || req.protocol || 'http';
|
||||||
|
const host = req.get('x-forwarded-host') || req.get('host');
|
||||||
|
const baseUrl = `${protocol}://${host}`;
|
||||||
|
|
||||||
|
// Check if build files exist
|
||||||
|
const files = [];
|
||||||
|
const fileExtensions = ['.3dsx', '.cia', '.3ds', '.elf'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = fs.readdirSync(outputDir, { recursive: true });
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = path.join(outputDir, item);
|
||||||
|
const stats = fs.statSync(itemPath);
|
||||||
|
|
||||||
|
if (stats.isFile()) {
|
||||||
|
const ext = path.extname(item).toLowerCase();
|
||||||
|
if (fileExtensions.includes(ext)) {
|
||||||
|
const filePath = `/downloads/${item}`;
|
||||||
|
const fileUrl = `${baseUrl}${filePath}`;
|
||||||
|
files.push({
|
||||||
|
name: item,
|
||||||
|
path: filePath,
|
||||||
|
url: fileUrl,
|
||||||
|
size: (stats.size / 1024).toFixed(1), // KB
|
||||||
|
type: ext.substring(1).toUpperCase()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error reading output directory:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>mice - 3DS Music Player Downloads</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="refresh" content="30">
|
||||||
|
${CSS}
|
||||||
|
<script>
|
||||||
|
// Auto-refresh every 30 seconds to check for new builds
|
||||||
|
setInterval(() => {
|
||||||
|
fetch('/api/version')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const versionElement = document.getElementById('current-version');
|
||||||
|
const buildElement = document.getElementById('build-time');
|
||||||
|
if (versionElement) versionElement.textContent = data.version;
|
||||||
|
if (buildElement) buildElement.textContent = data.buildTime;
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🎵 mice - 3DS Music Player 🎵</h1>
|
||||||
|
|
||||||
|
<div class="version-info">
|
||||||
|
<strong>Current Version:</strong> <span id="current-version">${getCurrentVersion()}</span><br>
|
||||||
|
<strong>Last Build:</strong> <span id="build-time">${getBuildTimestamp()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="installation-guide">
|
||||||
|
<h3>📱 Installation Instructions:</h3>
|
||||||
|
<p><strong>For .3dsx files (Homebrew Launcher):</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Copy the .3dsx file to <code>/3ds/</code> folder on your SD card</li>
|
||||||
|
<li>Launch via Homebrew Launcher</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p><strong>For .cia files (installed to HOME menu):</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Install using FBI, DevMenu, or similar CIA installer</li>
|
||||||
|
<li>Appears on HOME menu after installation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
html += `
|
||||||
|
<div class="file-info">
|
||||||
|
<h3>⚠️ No build files found</h3>
|
||||||
|
<p>Please run <code>make</code> in the mice directory to build the project first.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
html += `<h2>📦 Available Downloads:</h2>`;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
// Generate QR code for each file
|
||||||
|
let qrCodeDataUrl = '';
|
||||||
|
try {
|
||||||
|
qrCodeDataUrl = await QRCode.toDataURL(file.url);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error generating QR code:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="download-section">
|
||||||
|
<h3>🎮 ${file.name}</h3>
|
||||||
|
<div class="file-info">
|
||||||
|
<strong>Type:</strong> ${file.type}
|
||||||
|
<strong>Size:</strong> ${file.size} KB
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="${file.path}" class="download-link" download>
|
||||||
|
📥 Download ${file.type}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
${qrCodeDataUrl ? `
|
||||||
|
<div class="qr-code">
|
||||||
|
<p><strong>📱 Scan QR Code for 3DS Browser:</strong></p>
|
||||||
|
<img src="${qrCodeDataUrl}" alt="QR Code for ${file.name}" style="border: 2px solid #ddd; padding: 10px; background: white;">
|
||||||
|
<br>
|
||||||
|
<small style="color: #666; word-break: break-all;">${file.url}</small>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="file-info">
|
||||||
|
<h3>🎵 Features:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Support for MP3, FLAC, OGG Vorbis, Opus, WAV, and SID formats</li>
|
||||||
|
<li>Metadata display (artist, album, title) on top screen</li>
|
||||||
|
<li>File browser with folder navigation</li>
|
||||||
|
<li>Playback controls and sleep mode support</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="installation-guide">
|
||||||
|
<h3>🔗 Access Information:</h3>
|
||||||
|
<p>Server running at: <strong>${baseUrl}</strong></p>
|
||||||
|
${baseUrl.includes('github.dev') ?
|
||||||
|
'<p>✅ GitHub Codespaces detected - URL is ready for 3DS browser!</p>' :
|
||||||
|
'<p>💡 For 3DS access, use your local network IP address</p>'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
res.send(html);
|
||||||
|
});
|
||||||
|
|
||||||
|
// API endpoint for version info
|
||||||
|
app.get('/api/version', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
version: getCurrentVersion(),
|
||||||
|
buildTime: getBuildTimestamp(),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`🎵 mice Download Server`);
|
||||||
|
|
||||||
|
// Detect if we're in GitHub Codespaces
|
||||||
|
const codespace = process.env.CODESPACE_NAME;
|
||||||
|
if (codespace) {
|
||||||
|
const codespacesUrl = `https://${codespace}-${PORT}.app.github.dev/`;
|
||||||
|
console.log(`🌐 GitHub Codespaces URL: ${codespacesUrl}`);
|
||||||
|
console.log(`📱 Use this URL for 3DS browser access!`);
|
||||||
|
} else {
|
||||||
|
console.log(`🌐 Server running at: http://localhost:${PORT}`);
|
||||||
|
console.log(`📱 Access from 3DS browser using your local IP`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📦 Serving files from: ${path.join(__dirname, '../output')}`);
|
||||||
|
console.log(`\n🔗 To find your local IP address:`);
|
||||||
|
console.log(` - Windows: ipconfig`);
|
||||||
|
console.log(` - Mac/Linux: ifconfig or ip addr show`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('🛑 Server shutting down...');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
Reference in New Issue
Block a user