Added more missing files.
This commit is contained in:
28
home/stormux/.local/files/10-headless.conf
Normal file
28
home/stormux/.local/files/10-headless.conf
Normal 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
|
||||
4
home/stormux/.local/files/10-screen.conf
Normal file
4
home/stormux/.local/files/10-screen.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
Section "Device"
|
||||
Identifier "FBdev"
|
||||
Driver "fbdev"
|
||||
EndSection
|
||||
112
home/stormux/.local/files/clipboard_translator.sh
Executable file
112
home/stormux/.local/files/clipboard_translator.sh
Executable 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
|
||||
29
home/stormux/.local/files/speak_window_title.sh
Executable file
29
home/stormux/.local/files/speak_window_title.sh
Executable 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
|
||||
119
home/stormux/.local/upload_server/templates/classify.html
Normal file
119
home/stormux/.local/upload_server/templates/classify.html
Normal 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>
|
||||
24
home/stormux/.local/upload_server/templates/error.html
Normal file
24
home/stormux/.local/upload_server/templates/error.html
Normal 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>
|
||||
377
home/stormux/.local/upload_server/templates/index.html
Normal file
377
home/stormux/.local/upload_server/templates/index.html
Normal 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>
|
||||
49
home/stormux/.local/upload_server/templates/shutdown.html
Normal file
49
home/stormux/.local/upload_server/templates/shutdown.html
Normal 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>
|
||||
@@ -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>
|
||||
308
home/stormux/.local/upload_server/templates/success.html
Normal file
308
home/stormux/.local/upload_server/templates/success.html
Normal 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>
|
||||
212
home/stormux/.local/upload_server/templates/voxin_results.html
Normal file
212
home/stormux/.local/upload_server/templates/voxin_results.html
Normal 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>
|
||||
609
home/stormux/.local/upload_server/uploader.py
Executable file
609
home/stormux/.local/upload_server/uploader.py
Executable 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)
|
||||
Reference in New Issue
Block a user