Added more missing files.

This commit is contained in:
Storm Dragon
2025-07-17 00:20:15 -04:00
parent d2e82fadf0
commit 2b4f369c8b
12 changed files with 1957 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
Section "Monitor"
Identifier "dummy_monitor"
HorizSync 28.0-80.0
VertRefresh 48.0-75.0
Modeline "1920x1080" 172.80 1920 2040 2248 2576 1080 1081 1084 1118
EndSection
Section "Device"
Identifier "dummy_card"
Driver "dummy"
VideoRam 256000
Option "UseFBDev" "false"
EndSection
Section "Screen"
Identifier "dummy_screen"
Device "dummy_card"
Monitor "dummy_monitor"
SubSection "Display"
Depth 24
Modes "1920x1080"
EndSubSection
EndSection
Section "ServerLayout"
Identifier "dummy_layout"
Screen 0 "dummy_screen"
EndSection

View File

@@ -0,0 +1,4 @@
Section "Device"
Identifier "FBdev"
Driver "fbdev"
EndSection

View File

@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# Modified from the script at:
# https://gist.github.com/fdietze/6768a0970d7d732b7fbd7930ccceee2a
# The next line has been commented because if there's nothing in clipboard the script breaks.
# set -Eeuo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/#:~:text=set%20%2Du,is%20often%20highly%20desirable%20behavior.
shopt -s expand_aliases
if [[ $# -ne 2 ]]; then
echo "Usage: $0 \"application name\" \"file name\"."
exit 1
fi
# Wait for the application to start
while ! pgrep -u "$USER" ^$1 &> /dev/null ; do
sleep 0.05
done
fileName="${2,,}"
fileName="${fileName//[[:space:]]/-}.sqlite"
translationFile="/home/stormux/.local/files/translations/${fileName}"
# Make sure the directory exists
mkdir -p "$(dirname "$translationFile")"
# Initialize database if it doesn't exist
if [[ ! -s "$translationFile" ]]; then
rm -f "$translationFile"
sqlite3 "$translationFile" <<EOF
CREATE TABLE translations(
text TEXT NOT NULL,
translation TEXT NOT NULL,
PRIMARY KEY (text)
);
CREATE INDEX translations_text_idx ON translations (text);
EOF
fi
# Define a function to safely query the database
query_database() {
local db_file="$1"
local sql_query="$2"
sqlite3 -line "$db_file" "$sql_query"
}
# Define a function to safely insert into the database
insert_database() {
local db_file="$1"
local text="$2"
local translation="$3"
# Use sqlite3 .import feature which is more robust for special characters
echo "$text|$translation" | sqlite3 -separator "|" "$db_file" ".import /dev/stdin temp_import"
sqlite3 "$db_file" "INSERT OR IGNORE INTO translations SELECT * FROM temp_import; DROP TABLE IF EXISTS temp_import;"
}
# Read so long as the application is running
while pgrep -u "$USER" ^$1 &> /dev/null ; do
sleep 0.05
text="$(xclip -d "${DISPLAY:-:0}" -selection clipboard -o 2> /dev/null)"
if [[ -f ~/.agmsilent ]]; then
continue
fi
if [[ "${text}" =~ ^[0-9A-Za-z[:space:][:punct:]]+$ ]]; then
spd-say -- "$text"
echo "" | xclip -d "${DISPLAY:-:0}" -selection clipboard 2> /dev/null
continue
fi
if [[ -z "$text" ]]; then
continue
fi
# https://en.wikipedia.org/wiki/Unicode_equivalence#Combining_and_precomposed_characters
# https://www.effectiveperlprogramming.com/2011/09/normalize-your-perl-source/
alias nfc="perl -MUnicode::Normalize -CS -ne 'print NFC(\$_)'" # composed characters
# Normalize different unicode space characters to the same space
# https://stackoverflow.com/a/43640405
alias normalize_spaces="perl -CSDA -plE 's/[^\\S\\t]/ /g'"
alias normalize_unicode="normalize_spaces | nfc"
# Normalize text
normalized_text="$(echo "$text" | normalize_unicode)"
# Create a temporary database for import
sqlite3 "$translationFile" "CREATE TABLE IF NOT EXISTS temp_import(text TEXT, translation TEXT);"
# Check if we already have a translation
translated=$(sqlite3 "$translationFile" "SELECT translation FROM translations WHERE text = '$normalized_text' LIMIT 1;" 2>/dev/null)
if [[ -z "$translated" ]]; then
# Get translation from the trans utility
translated="$(trans -no-autocorrect -no-warn -brief "$normalized_text" | head -1 | sed 's/\s*$//' | normalize_unicode)"
if [[ -n "$translated" ]]; then
# Insert using echo piping to avoid escaping issues
echo "$normalized_text|$translated" | sqlite3 -separator "|" "$translationFile" ".import /dev/stdin temp_import"
sqlite3 "$translationFile" "INSERT OR IGNORE INTO translations SELECT * FROM temp_import; DELETE FROM temp_import;"
fi
fi
# If we got a translation, speak it
if [[ -n "$translated" ]]; then
spd-say -- "$translated"
fi
# Clear clipboard
echo "" | xclip -d "${DISPLAY:-:0}" -selection clipboard 2> /dev/null
done
exit 0

View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Adapted from the bash snippet found at:
# https://bbs.archlinux.org/viewtopic.php?id=117031
# Wait for the application to start
while ! pgrep -u "$USER" ^$1 &> /dev/null ; do
sleep 0.05
done
# Read so long as the application is running
while pgrep -u "$USER" ^$1 &> /dev/null ; do
sleep 0.05
if [[ -f ~/.agmsilent ]]; then
continue
fi
wnd_focus=$(xdotool getwindowfocus)
wnd_title=$(xprop -id $wnd_focus WM_NAME)
lookfor='"(.*)"'
if [[ "$wnd_title" =~ $lookfor ]]; then
wnd_title=${BASH_REMATCH[1]}
if [[ "$old_title" != "$wnd_title" ]]; then
spd-say -- "$wnd_title"
old_title="$wnd_title"
fi
fi
done
exit 0

View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Classify Files</title>
<style>
/* Same CSS as index.html */
/* ... (include all CSS from index.html) ... */
/* Additional styles for classification */
.file-item {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.file-name {
font-weight: bold;
margin-bottom: 10px;
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.radio-option {
display: flex;
align-items: center;
}
.radio-option input[type="radio"] {
margin-right: 5px;
}
.radio-option input[type="radio"]:focus + label {
outline: var(--focus-outline);
border-radius: 3px;
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Announcer for screen readers -->
<div id="status-announcer" aria-live="polite"></div>
<div class="container" id="main-content">
<h1>Classify Unknown Files</h1>
<p>Please select the file type for each uploaded file:</p>
<form action="/classify" method="POST">
{% for file in unknown_files %}
<div class="file-item">
<div class="file-name" id="file-{{ loop.index }}">{{ file.name }}</div>
<fieldset aria-labelledby="file-{{ loop.index }}">
<legend class="sr-only">Select file type for {{ file.name }}</legend>
<div class="radio-group" role="radiogroup">
{% for file_type in file_types %}
<div class="radio-option">
<input type="radio"
id="{{ file.name }}-{{ file_type }}"
name="{{ file.name }}"
value="{{ file_type }}"
required
{% if file_type == 'other' %}checked{% endif %}>
<label for="{{ file.name }}-{{ file_type }}">{{ file_type|title }}</label>
</div>
{% endfor %}
</div>
</fieldset>
</div>
{% endfor %}
<div class="form-group">
<button type="submit" class="btn">Save and Continue</button>
</div>
</form>
<div class="form-group" style="margin-top: 30px; text-align: right;">
<a href="/shutdown" class="btn" style="background-color: #e74c3c;">Shutdown Server</a>
</div>
</div>
<script>
// Ensure at least one option is selected for each file
document.querySelector('form').addEventListener('submit', function(e) {
const fileItems = document.querySelectorAll('.file-item');
let isValid = true;
fileItems.forEach(item => {
const fileName = item.querySelector('.file-name').textContent;
const selectedOption = document.querySelector(`input[name="${fileName}"]:checked`);
if (!selectedOption) {
isValid = false;
document.getElementById('status-announcer').textContent =
`Please select a file type for ${fileName}`;
// Highlight the unselected file item
item.style.borderColor = 'var(--error-color)';
setTimeout(() => {
item.scrollIntoView({ behavior: 'smooth' });
}, 100);
}
});
if (!isValid) {
e.preventDefault();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Error</title>
<style>
/* Same CSS as index.html */
/* ... (include CSS from index.html) ... */
</style>
</head>
<body>
<div class="container">
<h1>Error</h1>
<div class="alert alert-error" role="alert">
{{ message }}
</div>
<div style="text-align: right; margin-top: 20px;">
<a href="/shutdown" class="btn" style="background-color: #e74c3c;">Shutdown Server</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,377 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload Server</title>
<style>
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--background-color: #f8f9fa;
--text-color: #333;
--error-color: #e74c3c;
--success-color: #2ecc71;
--focus-outline: 3px solid #f39c12;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
h1, h2 {
color: var(--primary-color);
}
.container {
background-color: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}
.file-input {
border: 2px dashed #ddd;
padding: 20px;
text-align: center;
cursor: pointer;
border-radius: 5px;
background-color: #f8f9fa;
transition: all 0.3s ease;
}
.file-input:hover, .file-input:focus-within {
border-color: var(--secondary-color);
background-color: #edf2f7;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
input[type="file"] {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.file-label {
display: block;
cursor: pointer;
font-weight: normal;
padding: 10px;
}
.btn {
display: inline-block;
background-color: var(--secondary-color);
color: white;
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
margin-right: 10px;
}
.btn:hover, .btn:focus {
background-color: #2980b9;
}
.btn:focus {
outline: var(--focus-outline);
outline-offset: 2px;
}
.alert {
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.alert-error {
background-color: #fdecea;
color: var(--error-color);
border: 1px solid var(--error-color);
}
.alert-success {
background-color: #d4f6e6;
color: var(--success-color);
border: 1px solid var(--success-color);
}
.skip-link {
position: absolute;
top: -40px;
left: 0;
color: white;
background-color: var(--secondary-color);
padding: 8px;
z-index: 100;
transition: top 0.3s ease;
}
.skip-link:focus {
top: 0;
}
/* For screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Status announcer for screen readers */
#status-announcer {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
pointer-events: none;
}
/* Upload sections */
.upload-section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.upload-section:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Announcer for screen readers -->
<div id="status-announcer" aria-live="polite"></div>
<div class="container" id="main-content">
<h1>File Upload Server</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- File Upload Section -->
<section class="upload-section" aria-labelledby="file-section-heading">
<h2 id="file-section-heading">Upload Files</h2>
<p>Upload game ROMs and other individual files.</p>
<form action="/upload" method="POST" enctype="multipart/form-data">
<div class="form-group">
<div class="file-input" id="file-drop-area">
<label for="file-upload" class="file-label">
Choose files or drag and drop here
<span class="sr-only">Select one or more files to upload</span>
</label>
<input id="file-upload" type="file" name="file" multiple aria-describedby="file-help">
<p id="file-help" class="help-text">Selected files: <span id="selected-files">None</span></p>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn" id="upload-button">Upload Files</button>
</div>
</form>
</section>
<!-- Music Folder Upload Section -->
<section class="upload-section" aria-labelledby="music-section-heading">
<h2 id="music-section-heading">Upload Music Folder</h2>
<p>Upload an entire music folder with its structure directly to your Music directory.</p>
<form action="/upload_music" method="POST" enctype="multipart/form-data">
<div class="form-group">
<div class="file-input" id="folder-drop-area">
<label for="folder-upload" class="file-label">
Select a music folder to upload
<span class="sr-only">Select a folder containing music files</span>
</label>
<input id="folder-upload" type="file" name="folder" webkitdirectory directory multiple aria-describedby="folder-help">
<p id="folder-help" class="help-text">Selected folder: <span id="selected-folder">None</span></p>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn" id="upload-folder-button">Upload Music Folder</button>
</div>
</form>
</section>
<div class="form-group" style="margin-top: 30px; text-align: right;">
<a href="/shutdown" class="btn btn-secondary" style="background-color: #e74c3c;">Shutdown Server</a>
</div>
</div>
<script>
// Make the entire area clickable for file selection
document.getElementById('file-drop-area').addEventListener('click', function(e) {
if (e.target !== this) return;
document.getElementById('file-upload').click();
});
// Make the entire area clickable for folder selection
document.getElementById('folder-drop-area').addEventListener('click', function(e) {
if (e.target !== this) return;
document.getElementById('folder-upload').click();
});
// Show selected files
document.getElementById('file-upload').addEventListener('change', function(e) {
const fileList = e.target.files;
const fileNames = Array.from(fileList).map(file => file.name);
const selectedFiles = document.getElementById('selected-files');
if (fileNames.length > 0) {
selectedFiles.textContent = fileNames.join(', ');
// Announce to screen readers
document.getElementById('status-announcer').textContent =
`Selected ${fileNames.length} files: ${fileNames.join(', ')}`;
} else {
selectedFiles.textContent = 'None';
document.getElementById('status-announcer').textContent = 'No files selected';
}
});
// Show selected folder
document.getElementById('folder-upload').addEventListener('change', function(e) {
const fileList = e.target.files;
const fileCount = fileList.length;
const selectedFolder = document.getElementById('selected-folder');
if (fileCount > 0) {
// Get the common folder path from the first file
const firstFile = fileList[0];
const folderPath = firstFile.webkitRelativePath.split('/')[0];
selectedFolder.textContent = `${folderPath} (${fileCount} files)`;
// Announce to screen readers
document.getElementById('status-announcer').textContent =
`Selected folder: ${folderPath} containing ${fileCount} files`;
} else {
selectedFolder.textContent = 'None';
document.getElementById('status-announcer').textContent = 'No folder selected';
}
});
// Handle drag and drop for files
const fileDropArea = document.getElementById('file-drop-area');
setupDragDrop(fileDropArea, 'file-upload');
// Handle drag and drop for folders
const folderDropArea = document.getElementById('folder-drop-area');
setupDragDrop(folderDropArea, 'folder-upload');
function setupDragDrop(dropArea, inputId) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, function() {
this.style.borderColor = 'var(--secondary-color)';
this.style.backgroundColor = '#edf2f7';
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, function() {
this.style.borderColor = '#ddd';
this.style.backgroundColor = '#f8f9fa';
}, false);
});
dropArea.addEventListener('drop', function(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (inputId === 'file-upload') {
document.getElementById(inputId).files = files;
// Trigger change event
const event = new Event('change');
document.getElementById(inputId).dispatchEvent(event);
} else {
// Show message that folder drop might not be supported
document.getElementById('status-announcer').textContent =
'For folder upload, please use the file picker';
document.getElementById('selected-folder').textContent =
'Please use the folder selection button';
}
}, false);
}
// Check if the client is on local network
fetch('/check_local')
.then(response => {
if (!response.ok) {
throw new Error("Not on local network");
}
return response.json();
})
.then(data => {
if (!data.local) {
document.body.innerHTML = '<div class="container"><h1>Error</h1><p>Access denied: Local network only</p></div>';
}
})
.catch(error => {
console.error("Error checking local network:", error);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shutdown Server</title>
<style>
/* Same CSS as index.html */
/* ... (include CSS from index.html) ... */
/* Additional styles */
.shutdown-warning {
color: #e74c3c;
font-weight: bold;
}
.btn-secondary {
background-color: #95a5a6;
}
.btn-danger {
background-color: #e74c3c;
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<div class="container" id="main-content">
<h1>Shutdown Server</h1>
<p class="shutdown-warning" role="alert">Warning: This will completely shut down the upload server.</p>
<p>Are you sure you want to shut down the server? You can restart it from the system menu if you want to use it again.</p>
<div class="button-group">
<a href="/" class="btn btn-secondary">Cancel</a>
<form action="/shutdown" method="POST">
<button type="submit" class="btn btn-danger">Confirm Shutdown</button>
</form>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Shutting Down</title>
<style>
/* Same CSS as index.html */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8f9fa;
margin: 0;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.container {
background-color: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 30px;
text-align: center;
}
/* Additional styles */
.shutdown-message {
color: #3498db;
text-align: center;
font-size: 1.2em;
margin: 30px 0;
}
.spinner {
border: 6px solid #f3f3f3;
border-top: 6px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 2s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<h1>Server Shutting Down</h1>
<div class="spinner" role="status" aria-label="Server is shutting down"></div>
<p class="shutdown-message" aria-live="polite">
The upload server is shutting down.
</p>
</div>
<script>
// Close the window automatically
window.onload = function() {
// Try multiple approaches to close the window
setTimeout(function() {
// Method 1: Try to close the window
window.close();
// Method 2: Add a fallback message if window doesn't close
setTimeout(function() {
document.querySelector('.shutdown-message').textContent =
'Server has shut down. You can close this window and return to the menu.';
// Method 3: Redirect to about:blank as a last resort
setTimeout(function() {
window.location.href = 'about:blank';
}, 1000);
}, 1000);
}, 1000);
};
</script>
</body>
</html>

View File

@@ -0,0 +1,308 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Complete</title>
<style>
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--background-color: #f8f9fa;
--text-color: #333;
--error-color: #e74c3c;
--success-color: #2ecc71;
--focus-outline: 3px solid #f39c12;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
h1, h2 {
color: var(--primary-color);
}
.container {
background-color: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 30px;
}
.file-list {
margin-top: 20px;
}
.file-item {
display: flex;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid #eee;
}
.file-item:last-child {
border-bottom: none;
}
.file-item.success {
background-color: rgba(46, 204, 113, 0.1);
}
.file-item.error {
background-color: rgba(231, 76, 60, 0.1);
}
.file-status {
font-weight: bold;
}
.status-success {
color: var(--success-color);
}
.status-error {
color: var(--error-color);
}
.back-link {
display: inline-block;
margin-top: 20px;
color: var(--secondary-color);
text-decoration: none;
}
.back-link:hover, .back-link:focus {
text-decoration: underline;
}
.alert {
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.alert-error {
background-color: #fdecea;
color: var(--error-color);
border: 1px solid var(--error-color);
}
.alert-success {
background-color: #d4f6e6;
color: var(--success-color);
border: 1px solid var(--success-color);
}
.alert-info {
background-color: #e7f4fd;
color: var(--secondary-color);
border: 1px solid var(--secondary-color);
}
.pagination {
display: flex;
justify-content: center;
margin-top: 20px;
}
.pagination button {
background-color: var(--secondary-color);
color: white;
border: none;
padding: 5px 10px;
margin: 0 5px;
border-radius: 3px;
cursor: pointer;
}
.pagination button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.file-count {
text-align: center;
margin-bottom: 10px;
color: var(--secondary-color);
}
/* For displaying folder structure */
.folder-path {
font-family: monospace;
color: #666;
font-size: 0.9em;
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<div class="container" id="main-content">
<h1>Upload Complete</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<h2>Files Processed</h2>
{% if uploaded_files %}
<div class="file-count">
Showing <span id="showing-count">1-{{ uploaded_files|length if uploaded_files|length < 10 else 10 }}</span> of {{ uploaded_files|length }} files
</div>
<div class="file-list" role="table" aria-label="Uploaded files">
<div class="file-item" role="row">
<div role="columnheader" style="font-weight: bold; width: 60%;">File Name</div>
<div role="columnheader" style="font-weight: bold; width: 20%;">File Type</div>
<div role="columnheader" style="font-weight: bold; width: 20%;">Status</div>
</div>
<div id="file-items-container">
{% for file in uploaded_files[:10] %}
<div class="file-item {{ 'success' if file.success else 'error' }}" role="row">
<div role="cell" style="width: 60%;">
{{ file.name }}
{% if file.error %}
<div class="folder-path">Error: {{ file.error }}</div>
{% endif %}
</div>
<div role="cell" style="width: 20%;">{{ file.type|title }}</div>
<div role="cell" style="width: 20%;" class="file-status {{ 'status-success' if file.success else 'status-error' }}">
{{ 'Success' if file.success else 'Failed' }}
</div>
</div>
{% endfor %}
</div>
</div>
{% if uploaded_files|length > 10 %}
<div class="pagination">
<button id="prev-btn" disabled>Previous</button>
<button id="next-btn">Next</button>
</div>
{% endif %}
<script>
// Simple pagination for large file lists
const allFiles = [
{% for file in uploaded_files %}
{
name: "{{ file.name|safe }}",
type: "{{ file.type|title }}",
success: {{ 'true' if file.success else 'false' }},
{% if file.error %}
error: "{{ file.error|safe }}"
{% endif %}
}{{ ',' if not loop.last }}
{% endfor %}
];
const itemsPerPage = 10;
let currentPage = 0;
function updateDisplay() {
const container = document.getElementById('file-items-container');
const start = currentPage * itemsPerPage;
const end = Math.min(start + itemsPerPage, allFiles.length);
// Update displayed count
document.getElementById('showing-count').textContent =
`${start + 1}-${end}`;
// Clear current items
container.innerHTML = '';
// Add items for current page
for (let i = start; i < end; i++) {
const file = allFiles[i];
const item = document.createElement('div');
item.className = `file-item ${file.success ? 'success' : 'error'}`;
item.setAttribute('role', 'row');
const nameCell = document.createElement('div');
nameCell.setAttribute('role', 'cell');
nameCell.style.width = '60%';
nameCell.textContent = file.name;
if (file.error) {
const errorDiv = document.createElement('div');
errorDiv.className = 'folder-path';
errorDiv.textContent = `Error: ${file.error}`;
nameCell.appendChild(errorDiv);
}
const typeCell = document.createElement('div');
typeCell.setAttribute('role', 'cell');
typeCell.style.width = '20%';
typeCell.textContent = file.type;
const statusCell = document.createElement('div');
statusCell.setAttribute('role', 'cell');
statusCell.style.width = '20%';
statusCell.className = `file-status ${file.success ? 'status-success' : 'status-error'}`;
statusCell.textContent = file.success ? 'Success' : 'Failed';
item.appendChild(nameCell);
item.appendChild(typeCell);
item.appendChild(statusCell);
container.appendChild(item);
}
// Update button states
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
if (prevBtn && nextBtn) {
prevBtn.disabled = currentPage === 0;
nextBtn.disabled = end >= allFiles.length;
}
}
// Set up pagination buttons if they exist
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
if (prevBtn && nextBtn) {
prevBtn.addEventListener('click', function() {
if (currentPage > 0) {
currentPage--;
updateDisplay();
}
});
nextBtn.addEventListener('click', function() {
if ((currentPage + 1) * itemsPerPage < allFiles.length) {
currentPage++;
updateDisplay();
}
});
}
</script>
{% else %}
<p>No files were processed.</p>
{% endif %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 20px;">
<a href="/" class="back-link">Back to main page</a>
<a href="/shutdown" class="btn" style="background-color: #e74c3c;">Shutdown Server</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,212 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Voxin Voice Installation</title>
<style>
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--background-color: #f8f9fa;
--text-color: #333;
--error-color: #e74c3c;
--success-color: #2ecc71;
--focus-outline: 3px solid #f39c12;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
h1, h2 {
color: var(--primary-color);
}
.container {
background-color: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 30px;
}
.file-list {
margin-top: 20px;
}
.file-item {
display: flex;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid #eee;
}
.file-item:last-child {
border-bottom: none;
}
.file-item.success {
background-color: rgba(46, 204, 113, 0.1);
}
.file-item.error {
background-color: rgba(231, 76, 60, 0.1);
}
.file-status {
font-weight: bold;
}
.status-success {
color: var(--success-color);
}
.status-error {
color: var(--error-color);
}
.back-link {
display: inline-block;
margin-top: 20px;
color: var(--secondary-color);
text-decoration: none;
}
.back-link:hover, .back-link:focus {
text-decoration: underline;
}
.alert {
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.alert-error {
background-color: #fdecea;
color: var(--error-color);
border: 1px solid var(--error-color);
}
.alert-success {
background-color: #d4f6e6;
color: var(--success-color);
border: 1px solid var(--success-color);
}
.alert-info {
background-color: #e7f4fd;
color: var(--secondary-color);
border: 1px solid var(--secondary-color);
}
.details-container {
margin-top: 10px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 5px;
font-family: monospace;
font-size: 0.9em;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
}
.details-toggle {
background-color: transparent;
border: none;
color: var(--secondary-color);
cursor: pointer;
padding: 0;
font-size: 0.9em;
text-decoration: underline;
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<div class="container" id="main-content">
<h1>Voxin Voice Installation</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<h2>Installation Results</h2>
{% if results %}
<div class="file-list" role="table" aria-label="Voice installation results">
<div class="file-item" role="row">
<div role="columnheader" style="font-weight: bold; width: 50%;">Voice Package</div>
<div role="columnheader" style="font-weight: bold; width: 25%;">Status</div>
<div role="columnheader" style="font-weight: bold; width: 25%;">Message</div>
</div>
{% for result in results %}
<div class="file-item {{ 'success' if result.success else 'error' }}" role="row">
<div role="cell" style="width: 50%;">{{ result.name }}</div>
<div role="cell" style="width: 25%;" class="file-status {{ 'status-success' if result.success else 'status-error' }}">
{{ 'Success' if result.success else 'Failed' }}
</div>
<div role="cell" style="width: 25%;">
{{ result.message }}
{% if result.details %}
<div>
<button class="details-toggle"
onclick="toggleDetails('details-{{ loop.index }}')"
aria-expanded="false"
aria-controls="details-{{ loop.index }}">
Show details
</button>
<div id="details-{{ loop.index }}" class="details-container" style="display: none;">
{{ result.details }}
</div>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<p>No voice packages were processed.</p>
{% endif %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 20px;">
<a href="/" class="back-link">Back to main page</a>
<a href="/shutdown" class="btn" style="background-color: #e74c3c;">Shutdown Server</a>
</div>
</div>
<script>
function toggleDetails(id) {
const details = document.getElementById(id);
const button = document.querySelector(`[aria-controls="${id}"]`);
if (details.style.display === 'none') {
details.style.display = 'block';
button.textContent = 'Hide details';
button.setAttribute('aria-expanded', 'true');
} else {
details.style.display = 'none';
button.textContent = 'Show details';
button.setAttribute('aria-expanded', 'false');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,609 @@
#!/usr/bin/env python3
from flask import Flask, request, redirect, url_for, render_template, flash, jsonify
import os
import mimetypes
import shutil
import subprocess
import socket
import sys
import re
import logging
import threading
import termios
import tty
import select
from threading import Timer
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.secret_key = os.urandom(24) # For flash messages
# Configure logging
def setup_logging():
"""Configure logging for the upload server"""
logDir = os.path.expanduser("~/.local/log/stormux")
os.makedirs(logDir, exist_ok=True)
logFile = os.path.join(logDir, "upload_server.log")
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(logFile),
logging.StreamHandler()
]
)
return logging.getLogger(__name__)
logger = setup_logging()
# Configuration
UPLOAD_FOLDER = '/tmp/uploads' # Temporary storage
FINAL_DESTINATIONS = {
'apple': '/home/stormux/.local/games/apple2e/disks',
'NES': '/home/stormux/Roms/Nintendo Entertainment System',
'SNES': '/home/stormux/Roms/Super Nintendo Entertainment System',
'SMS': '/home/stormux/Roms/Sega Master System',
'music': '/home/stormux/Music',
'voxin': '/home/stormux/Downloads',
'other': '/home/stormux/Downloads'
}
# Allowed file extensions by type (can be easily expanded)
FILE_TYPES = {
'apple': ['.do', '.dsk'],
'NES': ['.nes'],
'SNES': ['.fig', '.smc'],
'SMS': ['.sms'],
'music': ['.mp3', '.ogg', '.flac', '.wav', '.m4a', '.opus'],
'voxin': ['.tgz'],
'other': [] # Empty list to represent any other file type
}
# Get local IP address
def get_local_ip():
try:
# Create a socket connection to determine the local IP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Doesn't even have to be reachable
s.connect(('10.255.255.255', 1))
IP = s.getsockname()[0]
s.close()
except Exception:
IP = '127.0.0.1'
return IP
# Global variable to store server info for repeat announcement
server_info = {'ip': '', 'port': 0}
# Announce server start with speech
def announce_server_start(ip, port):
formatted_ip = ""
segments = ip.split(".")
for i, segment in enumerate(segments):
# Add each digit with space between
formatted_ip += " ".join(segment)
# Add " dot " between segments (but not after the last segment)
if i < len(segments) - 1:
formatted_ip += " dot "
try:
message = f"Upload server running on {formatted_ip} port {port}"
# Using spd-say with -Cw flag for better speech synthesis
subprocess.run(['spd-say', '-Cw', message])
logger.info(f"Announced server start: {message}")
except Exception as e:
logger.error(f"Could not announce server start: {e}")
print(f"Could not announce server start: {e}")
# Function to repeat the server announcement
def repeat_announcement():
if server_info['ip'] and server_info['port']:
announce_server_start(server_info['ip'], server_info['port'])
# Keyboard listener function
def keyboard_listener():
"""Listen for key presses and repeat server announcement"""
if not sys.stdin.isatty():
logger.info("Not running in a terminal, keyboard listener disabled")
return
try:
# Save original terminal settings
old_settings = termios.tcgetattr(sys.stdin)
while True:
try:
# Set terminal to raw mode to capture individual key presses
tty.setraw(sys.stdin.fileno())
# Use select to check if input is available (non-blocking)
if select.select([sys.stdin], [], [], 1) == ([sys.stdin], [], []):
# Read a single character
key = sys.stdin.read(1)
# Any key press will trigger the announcement
if key:
logger.info(f"Key pressed: {repr(key)}, repeating server announcement")
repeat_announcement()
except KeyboardInterrupt:
# Handle Ctrl+C gracefully
logger.info("Keyboard listener interrupted")
break
except Exception as e:
logger.error(f"Error in keyboard listener: {e}")
break
finally:
# Always restore terminal settings
try:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
except:
pass
except Exception as e:
logger.error(f"Could not initialize keyboard listener: {e}")
# Start keyboard listener in background thread
def start_keyboard_listener():
"""Start the keyboard listener in a daemon thread"""
try:
listener_thread = threading.Thread(target=keyboard_listener, daemon=True)
listener_thread.start()
logger.info("Keyboard listener started")
except Exception as e:
logger.error(f"Could not start keyboard listener: {e}")
# Set file permissions
def set_file_permissions(filepath):
try:
# Set permissions to 644 (rw-r--r--)
os.chmod(filepath, 0o644)
return True
except OSError as e:
print(f"Warning: Could not set permissions for {filepath}: {e}")
return False
# Ensure all directories exist and have correct permissions
def setup_directories():
logger.info("Setting up upload directories")
# Create temp directory
if not os.path.exists(UPLOAD_FOLDER):
try:
os.makedirs(UPLOAD_FOLDER)
# Set permissions to 755
os.chmod(UPLOAD_FOLDER, 0o755)
logger.info(f"Created temp directory: {UPLOAD_FOLDER}")
except OSError as e:
logger.error(f"Could not create or set permissions for {UPLOAD_FOLDER}: {e}")
print(f"Warning: Could not create or set permissions for {UPLOAD_FOLDER}: {e}")
# Create all destination directories if they don't exist
for directory in FINAL_DESTINATIONS.values():
if not os.path.exists(directory):
try:
os.makedirs(directory)
# Set permissions to 755
os.chmod(directory, 0o755)
logger.info(f"Created destination directory: {directory}")
except OSError as e:
logger.error(f"Could not create or set permissions for {directory}: {e}")
print(f"Warning: Could not create or set permissions for {directory}: {e}")
# Create directory if it doesn't exist
def ensure_directory_exists(directory):
if not os.path.exists(directory):
try:
os.makedirs(directory)
# Set permissions to 755
os.chmod(directory, 0o755)
return True
except OSError as e:
print(f"Warning: Could not create or set permissions for {directory}: {e}")
return False
return True
# Get file type from extension
def get_file_type(filename):
ext = os.path.splitext(filename.lower())[1]
for file_type, extensions in FILE_TYPES.items():
if ext in extensions:
return file_type
return None
@app.route('/')
def index():
return render_template('index.html', file_types=FILE_TYPES.keys())
@app.route('/upload', methods=['POST'])
def upload_file():
client_ip = request.remote_addr
logger.info(f"File upload request from {client_ip}")
if 'file' not in request.files:
logger.warning(f"Upload request from {client_ip} missing file part")
flash('No file part', 'error')
return redirect(request.url)
files = request.files.getlist('file')
logger.info(f"Received {len(files)} files from {client_ip}")
uploaded_files = []
unknown_files = []
voxin_results = []
for file in files:
if file.filename == '':
continue
filename = secure_filename(file.filename)
logger.info(f"Processing file: {filename} from {client_ip}")
file_path = os.path.join(UPLOAD_FOLDER, filename)
file.save(file_path)
# Set file permissions
set_file_permissions(file_path)
# Check if it's a voxin package by filename pattern
if is_voxin_package(filename):
logger.info(f"Detected voxin package: {filename}")
# Move to Downloads directory
dest_dir = FINAL_DESTINATIONS['voxin']
dest_path = os.path.join(dest_dir, filename)
shutil.move(file_path, dest_path)
set_file_permissions(dest_path)
# Process the voxin package
result = handle_voxin_package(dest_path)
logger.info(f"Voxin package processing result for {filename}: {result['success']}")
voxin_results.append({
'name': filename,
'type': 'voxin',
'success': result['success'],
'message': result['message'],
'details': result.get('details', '')
})
else:
# Handle regular files
file_type = get_file_type(filename)
if file_type:
logger.info(f"File {filename} categorized as: {file_type}")
# Move to appropriate directory
dest_dir = FINAL_DESTINATIONS.get(file_type, FINAL_DESTINATIONS['other'])
dest_path = os.path.join(dest_dir, filename)
shutil.move(file_path, dest_path)
set_file_permissions(dest_path)
uploaded_files.append({'name': filename, 'type': file_type})
else:
logger.info(f"File {filename} requires manual classification")
# Keep in temporary folder for classification
unknown_files.append({'name': filename, 'path': file_path})
# Handle results
if voxin_results:
# If there were voxin packages, show their installation results
return render_template('voxin_results.html', results=voxin_results)
elif unknown_files:
# If there are files needing classification
return render_template('classify.html', unknown_files=unknown_files, file_types=FILE_TYPES.keys())
else:
# All other files
flash('All files uploaded successfully!', 'success')
return render_template('success.html', uploaded_files=uploaded_files)
@app.route('/upload_music', methods=['POST'])
def upload_music():
if 'folder' not in request.files:
flash('No folder selected', 'error')
return redirect('/')
files = request.files.getlist('folder')
if len(files) == 0 or files[0].filename == '':
flash('No files in selected folder', 'error')
return redirect('/')
# Process each file
processed_files = []
skipped_files = []
# Get the root folder name from the first file
first_file = files[0]
if '/' not in first_file.filename:
flash('Invalid folder structure in uploaded files', 'error')
return redirect('/')
# Extract the root folder name
root_folder_name = secure_filename(first_file.filename.split('/')[0])
# Create the main folder in Music directory
music_root_dir = os.path.join(FINAL_DESTINATIONS['music'], root_folder_name)
ensure_directory_exists(music_root_dir)
for file in files:
if file.filename == '':
continue
# Get the relative path within the folder
rel_path = file.filename
# Skip if there's no path structure
if '/' not in rel_path:
skipped_files.append(rel_path)
continue
# Extract folder structure: "FolderName/SubFolder/file.mp3"
path_parts = rel_path.split('/')
# Create secure path components
secure_parts = [secure_filename(part) for part in path_parts]
# We keep ALL parts including the root folder for the final path
# BUT we need to recreate the structure inside the Music directory
rel_dest_path = '/'.join(secure_parts)
# Create the full destination path - put directly in Music/root_folder_name/...
dest_path = os.path.join(FINAL_DESTINATIONS['music'], rel_dest_path)
dest_dir = os.path.dirname(dest_path)
# Ensure the destination directory exists
ensure_directory_exists(dest_dir)
# Save to temp location first
temp_path = os.path.join(UPLOAD_FOLDER, secure_parts[-1])
file.save(temp_path)
set_file_permissions(temp_path)
# Move to final location
try:
shutil.move(temp_path, dest_path)
set_file_permissions(dest_path)
processed_files.append({
'name': rel_dest_path,
'type': 'music',
'success': True
})
except Exception as e:
print(f"Error moving file {temp_path} to {dest_path}: {e}")
processed_files.append({
'name': rel_dest_path,
'type': 'music',
'success': False,
'error': str(e)
})
# Provide feedback based on results
success_count = sum(1 for file in processed_files if file.get('success', False))
if success_count > 0:
flash(f'Music folder "{root_folder_name}" uploaded successfully! {success_count} files processed.', 'success')
else:
flash('No files were successfully processed. Please check the folder structure.', 'error')
return render_template('success.html', uploaded_files=processed_files)
# Add a function to check if a file is a voxin package
def is_voxin_package(filename):
"""Check if the filename starts with 'voxin-' and ends with '.tgz'."""
return filename.lower().startswith('voxin-') and filename.lower().endswith('.tgz')
# Add function to handle voxin installation
def handle_voxin_package(file_path):
"""
Process a voxin package:
1. Extract the package
2. Run the installer script with -l option
3. Test the new voice
Returns: dict with status and message
"""
try:
filename = os.path.basename(file_path)
logger.info(f"Starting voxin package processing: {filename}")
extract_dir = os.path.join('/tmp', 'voxin_extract')
# Create fresh extraction directory
if os.path.exists(extract_dir):
shutil.rmtree(extract_dir)
os.makedirs(extract_dir)
# Extract the tarball
logger.info(f"Extracting voxin package: {filename}")
subprocess.run(['tar', 'xf', file_path, '-C', extract_dir], check=True)
# Find the installer script
installer_path = None
for root, dirs, files in os.walk(extract_dir):
if 'voxin-installer.sh' in files:
installer_path = os.path.join(root, 'voxin-installer.sh')
break
if not installer_path:
logger.error(f"Voxin installer script not found in package: {filename}")
return {
'success': False,
'message': 'Voxin installer script not found in package.'
}
# Make the installer executable
os.chmod(installer_path, 0o755)
# Run the installer with -l option for root's speech-dispatcher
logger.info(f"Running voxin installer for: {filename}")
install_result = subprocess.run(
['sudo', installer_path, '-l'],
capture_output=True,
text=True,
timeout=180 # 3 minute timeout
)
# Run the installer with -l option for RootFS's speech-dispatcher
install_result = subprocess.run(
['sudo', installer_path, '-l'],
capture_output=True,
text=True,
timeout=180 # 3 minute timeout
)
if install_result.returncode != 0:
logger.error(f"Voxin installation failed for {filename}: {install_result.stderr}")
return {
'success': False,
'message': 'Voxin installation failed.',
'details': install_result.stderr
}
# Restart speech-dispatcher
subprocess.run(['killall', 'speech-dispatcher'],
stderr=subprocess.DEVNULL) # Ignore errors if not running
# Wait a moment for service to fully terminate
subprocess.run(['sleep', '2'])
# Test the voice
test_message = "If you hear this message, Voxin installation was successful, and you may now change the voice from the system menu."
test_result = subprocess.run(
['spd-say', '-o', 'voxin', test_message],
capture_output=True,
text=True
)
# Clean up extraction directory
shutil.rmtree(extract_dir)
logger.info(f"Voxin package installation completed successfully: {filename}")
return {
'success': True,
'message': 'Voice package installed successfully!',
'details': 'The test message should have been spoken using the voxin voice system.'
}
except subprocess.TimeoutExpired:
logger.error(f"Voxin installation timed out for: {filename}")
return {
'success': False,
'message': 'Voice installation timed out.',
'details': 'The installation process took too long and was terminated.'
}
except Exception as e:
logger.error(f"Voxin installation error for {filename}: {e}")
return {
'success': False,
'message': 'Voice installation error.',
'details': str(e)
}
@app.route('/classify', methods=['POST'])
def classify_files():
results = []
for filename, file_type in request.form.items():
if filename == 'csrf_token':
continue
file_path = os.path.join(UPLOAD_FOLDER, filename)
if os.path.exists(file_path):
# Move to selected directory
dest_dir = FINAL_DESTINATIONS.get(file_type, FINAL_DESTINATIONS['other'])
dest_path = os.path.join(dest_dir, filename)
shutil.move(file_path, dest_path)
set_file_permissions(dest_path)
results.append({'name': filename, 'type': file_type, 'success': True})
else:
results.append({'name': filename, 'success': False})
flash('Files classified and moved successfully!', 'success')
return render_template('success.html', uploaded_files=results)
@app.route('/check_local', methods=['GET'])
def check_local():
# Get client IP
client_ip = request.remote_addr
# Define local networks
local_networks = ['127.0.0.1', '192.168.', '10.', '172.16.', '172.17.', '172.18.', '172.19.', '172.20.',
'172.21.', '172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.', '172.28.',
'172.29.', '172.30.', '172.31.']
# Check if IP is local
is_local = any(client_ip.startswith(network) for network in local_networks)
if is_local:
return jsonify({'status': 'success', 'local': True})
else:
return jsonify({'status': 'error', 'local': False}), 403
# Add a shutdown route to close the application
@app.route('/shutdown', methods=['GET', 'POST'])
def shutdown():
# Function to shutdown the server
def shutdown_server():
# Announce shutdown
try:
# Using spd-say with -Cw flag for better speech synthesis
subprocess.run(['spd-say', '-Cw', 'Server shutting down'])
except Exception:
pass
# Force exit with os._exit which is more aggressive than sys.exit
os._exit(0)
# Render confirmation page on GET
if request.method == 'GET':
return render_template('shutdown.html')
# Process shutdown on POST
flash('Server is shutting down...', 'info')
# Use a shorter timer to give time for the response to be sent
Timer(0.5, shutdown_server).start()
return render_template('shutdown_confirm.html')
# Middleware to check if request is from local network
@app.before_request
def check_local_network():
# Skip for certain routes
if request.endpoint == 'check_local':
return
# Get client IP
client_ip = request.remote_addr
# Define local networks
local_networks = ['127.0.0.1', '192.168.', '10.', '172.16.', '172.17.', '172.18.', '172.19.', '172.20.',
'172.21.', '172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.', '172.28.',
'172.29.', '172.30.', '172.31.']
# Check if IP is local
is_local = any(client_ip.startswith(network) for network in local_networks)
if not is_local:
return render_template('error.html', message="Access denied: Local network only"), 403
if __name__ == '__main__':
logger.info("Starting upload server")
# Set up directories
setup_directories()
# Get local IP and port
local_ip = get_local_ip()
port = 5000
# Store server info for repeat announcements
server_info['ip'] = local_ip
server_info['port'] = port
logger.info(f"Server will run on {local_ip}:{port}")
# Start keyboard listener for repeat announcements
start_keyboard_listener()
# Announce server start
announce_server_start(local_ip, port)
# Run the server
logger.info("Upload server started successfully")
app.run(host='0.0.0.0', port=port, debug=True)