├── setup.py ├── config ├── etc │ └── wifibroadcast.cfg ├── scripts │ └── stream.sh ├── py-config-gs.json └── py-config-gs-dev.json ├── app ├── static │ ├── js │ │ └── script.js │ ├── images │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ └── android-chrome-512x512.png │ └── css │ │ └── styles.css ├── templates │ ├── footer.html │ ├── upload.html │ ├── edit.html │ ├── journal.html │ ├── base.html │ ├── videos.html │ ├── play.html │ └── home.html ├── __init__.py └── routes.py ├── VERSION ├── images ├── editor.png ├── home.png ├── journal.png ├── v_player.png ├── v_select.png └── home_bottom.png ├── uploads_dev └── gs.key ├── run.sh ├── requirements.txt ├── app.py ├── pyproject.toml ├── improver.service ├── install.sh ├── package.sh ├── gunicorn_config.py ├── Dockerfile ├── docker-compose.yaml ├── nginx.conf ├── Makefile ├── update_nginx.sh ├── .github └── workflows │ └── package-release.yml ├── .gitignore ├── README.md └── deploy_improver.sh /setup.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/etc/wifibroadcast.cfg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/scripts/stream.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/js/script.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | release-${VERSION}-7-gce5f94f 2 | -------------------------------------------------------------------------------- /images/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/images/editor.png -------------------------------------------------------------------------------- /images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/images/home.png -------------------------------------------------------------------------------- /images/journal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/images/journal.png -------------------------------------------------------------------------------- /images/v_player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/images/v_player.png -------------------------------------------------------------------------------- /images/v_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/images/v_select.png -------------------------------------------------------------------------------- /uploads_dev/gs.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/uploads_dev/gs.key -------------------------------------------------------------------------------- /images/home_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/images/home_bottom.png -------------------------------------------------------------------------------- /app/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/app/static/images/favicon.ico -------------------------------------------------------------------------------- /app/static/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/app/static/images/favicon-16x16.png -------------------------------------------------------------------------------- /app/static/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/app/static/images/favicon-32x32.png -------------------------------------------------------------------------------- /app/static/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/app/static/images/apple-touch-icon.png -------------------------------------------------------------------------------- /app/static/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/app/static/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/static/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/improver/master/app/static/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /app/templates/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to run the Flask app using Gunicorn 4 | echo "Starting Flask app with Gunicorn..." 5 | gunicorn -c gunicorn_config.py "app:create_app()" 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.8.2 2 | click==8.1.7 3 | Flask==3.0.3 4 | gunicorn==23.0.0 5 | itsdangerous==2.2.0 6 | Jinja2==3.1.4 7 | MarkupSafe 8 | packaging==24.2 9 | Werkzeug==3.1.3 10 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # app.py (root of your project, outside the app/ folder) 2 | from app import create_app 3 | import logging 4 | 5 | app = create_app() 6 | 7 | if __name__ == '__main__': 8 | app.run() 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "improver" 3 | version = "1.0.0" 4 | 5 | [build-system] 6 | requires = ["setuptools>=42", "wheel", "setuptools_scm"] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [tool.setuptools_scm] 10 | write_to = "py_config_gs/_version.py" 11 | -------------------------------------------------------------------------------- /improver.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=OpenIPC Improver Flask App 3 | After=network.target 4 | 5 | [Service] 6 | User=www-data 7 | Group=www-data 8 | WorkingDirectory=/opt/improver 9 | Environment="FLASK_ENV=production" 10 | ExecStart=/usr/local/bin/gunicorn -w 4 -b 127.0.0.1:5001 "app:create_app()" 11 | Restart=always 12 | RestartSec=5 13 | 14 | # Security options (optional) 15 | PrivateTmp=true 16 | ProtectSystem=full 17 | ProtectHome=true 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /app/templates/upload.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |
6 |

Upload gs.key

7 |
8 | 9 | 10 |
11 | {% with messages = get_flashed_messages() %} 12 | {% if messages %} 13 | 18 | {% endif %} 19 | {% endwith %} 20 | {% endblock %} -------------------------------------------------------------------------------- /config/py-config-gs.json: -------------------------------------------------------------------------------- 1 | { 2 | "config_files": [ 3 | { 4 | "name": "wifibroadcast.cfg", 5 | "path": "/etc/wifibroadcast.cfg" 6 | }, 7 | { 8 | "name": "stream.sh", 9 | "path": "/config/scripts/stream.sh" 10 | }, 11 | { 12 | "name": "wifibroadcast", 13 | "path": "/etc/default/wifibroadcast" 14 | }, 15 | { 16 | "name": "OpenIPC service (systemd)", 17 | "path": "/etc/systemd/system/openipc.service" 18 | }, 19 | { 20 | "name": "WifiBroadcast", 21 | "path": "/etc/systemd/system/wifibroadcast.service.wants/wifibroadcast@gs.service" 22 | } 23 | 24 | 25 | ], 26 | 27 | "VIDEO_DIR": "/media", 28 | "SERVER_PORT": 5001 29 | 30 | } 31 | -------------------------------------------------------------------------------- /config/py-config-gs-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "config_files": [ 3 | { 4 | "name": "wifibroadcast.cfg", 5 | "path": "~/config/etc/wifibroadcast.cfg" 6 | }, 7 | { 8 | "name": "stream.sh", 9 | "path": "~/config/scripts/stream.sh" 10 | }, 11 | { 12 | "name": "wifibroadcast", 13 | "path": "~/config/etc/default/wifibroadcast" 14 | }, 15 | { 16 | "name": "OpenIPC service (systemd)", 17 | "path": "~/config/etc/systemd/system/openipc.service" 18 | }, 19 | { 20 | "name": "WifiBroadcast", 21 | "path": "~/config/etc/systemd/system/wifibroadcast.service.wants/wifibroadcast@gs.service" 22 | } 23 | 24 | 25 | ], 26 | 27 | "VIDEO_DIR": "/media", 28 | "SERVER_PORT": 5001 29 | 30 | } 31 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define paths 4 | SERVICE_FILE="py_config_gs/systemd/py-config-gs.service" 5 | TARGET_SERVICE_PATH="/etc/systemd/system/py-config-gs.service" 6 | CONFIG_FILE="py_config_gs/settings.json" 7 | TARGET_CONFIG_PATH="/etc/py-config-gs.json" 8 | 9 | # Copy the systemd service file to the system location 10 | echo "Installing systemd service file..." 11 | sudo cp "$SERVICE_FILE" "$TARGET_SERVICE_PATH" 12 | 13 | # Reload systemd and enable the service 14 | echo "Reloading systemd and enabling py-config-gs service..." 15 | sudo systemctl daemon-reload 16 | sudo systemctl enable py-config-gs.service 17 | 18 | # Copy the configuration file to /etc 19 | echo "Installing configuration file..." 20 | sudo cp "$CONFIG_FILE" "$TARGET_CONFIG_PATH" 21 | 22 | # Set proper permissions for configuration file 23 | sudo chmod 644 "$TARGET_CONFIG_PATH" 24 | 25 | echo "Installation complete." 26 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to package the Flask app for deployment to Radxa 4 | 5 | APP_NAME="improver" 6 | ARCHIVE_NAME="${APP_NAME}_source.tar.gz" 7 | VERSION_FILE="VERSION" 8 | 9 | GIT_VERSION=$(git describe --tags --always) 10 | echo "Packaging version $GIT_VERSION" 11 | echo $GIT_VERSION > $VERSION_FILE 12 | 13 | # Step 1: Create a compressed tarball of the source code 14 | echo "Packaging source code..." 15 | find app config -name '__pycache__' -prune -o -type f -print | tar czvf $ARCHIVE_NAME \ 16 | -T - \ 17 | requirements.txt \ 18 | gunicorn_config.py \ 19 | run.sh \ 20 | update_nginx.sh \ 21 | improver.service \ 22 | $VERSION_FILE 23 | 24 | # Step 2: Display the packaged file 25 | echo "Packaged application into: $ARCHIVE_NAME" 26 | 27 | # Step 3: Instructions for deployment 28 | echo "To deploy on Radxa, transfer $ARCHIVE_NAME and update_nginx.sh using SCP:" 29 | echo "scp $ARCHIVE_NAME deploy_improver.sh root@radxa:/tmp/" 30 | -------------------------------------------------------------------------------- /gunicorn_config.py: -------------------------------------------------------------------------------- 1 | # gunicorn_config.py 2 | 3 | # Bind to a socket file or an IP and port 4 | bind = "0.0.0.0:5001" # For a development setup; use a socket file in production, e.g., "unix:/tmp/simple_flask_app.sock" 5 | 6 | # Number of worker processes (adjust based on your server’s CPU) 7 | workers = 2 # Start with 2 workers; adjust based on your Radxa's resources 8 | 9 | # Number of worker threads (in addition to worker processes) 10 | threads = 2 # Additional threads to handle requests within each worker 11 | 12 | # Log settings 13 | loglevel = 'debug' 14 | accesslog = "-" # Log access information to stdout 15 | errorlog = "-" # Log error information to stdout 16 | 17 | # Daemonize the Gunicorn process 18 | # daemon = True # Uncomment if you want Gunicorn to run in the background without using systemd 19 | 20 | # Timeout settings 21 | timeout = 60 # Increase if your app handles longer requests 22 | 23 | # Security headers (optional) 24 | secure_scheme_headers = {'X-Forwarded-Proto': 'https'} 25 | -------------------------------------------------------------------------------- /app/templates/edit.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |
6 | {% if content is not none %} 7 |
8 | WARNING: YOU ARE RESPONSIBLE FOR ANY CHANGES YOU MAKE, ALWAYS MAKE SURE YOU HAVE A BACKUP 9 |
10 | 11 |

Edit Configuration File: {{ filename }}

12 |
13 |
14 | 15 |
16 | {% else %} 17 |
18 |

File Not Found: {{ filename }}

19 |

The requested file could not be found. Please check the filename or return to the home page.

20 |
21 | {% endif %} 22 | Cancel 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.9-slim 3 | 4 | # Set the working directory 5 | WORKDIR /opt/improver 6 | 7 | # Install dependencies 8 | RUN set -eux; \ 9 | apt-get update; \ 10 | apt-get install -y --no-install-recommends \ 11 | curl \ 12 | ffmpeg \ 13 | ; \ 14 | rm -rf /var/lib/apt/lists/* 15 | 16 | # Install Python packages 17 | COPY requirements.txt /opt/improver/ 18 | RUN pip install --upgrade pip && pip install -r requirements.txt 19 | 20 | # Create necessary directories 21 | RUN mkdir /opt/improver/logs 22 | RUN mkdir /config 23 | 24 | # Copy config file 25 | #COPY config/py-config-gs.json /config/py-config-gs.json 26 | 27 | # Make port 5001 available to the world outside this container 28 | EXPOSE 5001 29 | 30 | # Define environment variables for Flask 31 | # Pointing to the app directory 32 | ENV FLASK_APP=app 33 | # Change to "development" or "production" if necessary 34 | ENV FLASK_ENV=development 35 | 36 | # Run Gunicorn to serve the app 37 | CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5001", "app:create_app()"] 38 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | services: 4 | flask_app: 5 | build: . 6 | container_name: flask_app 7 | volumes: 8 | - .:/opt/improver # Mount current directory to allow Flask to access everything it needs 9 | - ./logs:/opt/improver/logs # Persist logs to host 10 | - ./config:/config # Mount configuration file to ensure it's accessible 11 | - ./media:/media # Mount configuration file to ensure it's accessible 12 | ports: 13 | - "5001:5001" # Expose Flask app to port 5001 14 | environment: 15 | FLASK_ENV: development # Sets Flask to development mode for Docker 16 | SETTINGS_FILE: /config/py-config-gs-dev.json # Use the config file from the mounted volume 17 | networks: 18 | - improver_network 19 | 20 | nginx: 21 | image: nginx:alpine 22 | container_name: nginx_proxy 23 | volumes: 24 | - ./nginx.conf:/etc/nginx/nginx.conf # Use a custom nginx configuration file 25 | ports: 26 | - "80:80" # Expose Nginx to port 80 27 | depends_on: 28 | - flask_app # Ensure Flask app is ready before Nginx starts 29 | networks: 30 | - improver_network 31 | 32 | networks: 33 | improver_network: 34 | driver: bridge 35 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | events { } 2 | 3 | http { 4 | server { 5 | listen 80; 6 | 7 | # Proxy requests for / to the Flask app running on port 5001 8 | location / { 9 | proxy_pass http://flask_app:5001; 10 | proxy_set_header Host $host; 11 | proxy_set_header X-Real-IP $remote_addr; 12 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 13 | proxy_set_header X-Forwarded-Proto $scheme; 14 | } 15 | 16 | # location /play/ { 17 | # proxy_pass http://flask_app:5001; 18 | # proxy_set_header Host $host; 19 | # proxy_set_header X-Real-IP $remote_addr; 20 | # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | # proxy_set_header X-Forwarded-Proto $scheme; 22 | 23 | # # Disable buffering for video files 24 | # proxy_request_buffering off; 25 | # proxy_buffering off; 26 | # } 27 | 28 | # Serve static files 29 | # location /static/ { 30 | # alias /opt/improver/app/static/; # Ensure this path matches the mounted directory 31 | # expires 30d; 32 | # add_header Cache-Control "public, max-age=2592000"; 33 | # } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/templates/journal.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |
6 |

Journal Logs

7 | 8 | 9 |
10 | 11 | 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Define variables for image and container names 2 | IMAGE_NAME := py-config-gs 3 | CONTAINER_NAME := py-config-gs 4 | DOCKER_COMPOSE_FILE := docker-compose.yaml 5 | PORT := 8080 # The port on the host to access Nginx 6 | 7 | # Target to build the Docker image using Docker Compose 8 | build: 9 | @echo "Building Docker images with Docker Compose..." 10 | docker-compose -f $(DOCKER_COMPOSE_FILE) build 11 | 12 | # Target to run the Docker containers in the foreground 13 | run: build 14 | @echo "Starting Docker containers..." 15 | docker-compose -f $(DOCKER_COMPOSE_FILE) up 16 | 17 | # Target to run Docker containers in detached mode (background) 18 | run-detached: build 19 | @echo "Starting Docker containers in detached mode..." 20 | docker-compose -f $(DOCKER_COMPOSE_FILE) up -d 21 | 22 | # Target to stop the Docker containers 23 | stop: 24 | @echo "Stopping Docker containers..." 25 | docker-compose -f $(DOCKER_COMPOSE_FILE) down 26 | 27 | # Target to view logs from all services 28 | logs: 29 | @echo "Showing logs from all Docker services..." 30 | docker-compose -f $(DOCKER_COMPOSE_FILE) logs -f 31 | 32 | # Target to clean up all images and containers 33 | clean: 34 | @echo "Removing Docker images and containers..." 35 | docker-compose -f $(DOCKER_COMPOSE_FILE) down --rmi all --volumes --remove-orphans 36 | 37 | # Help target to show available commands 38 | help: 39 | @echo "Available commands:" 40 | @echo " make build - Build the Docker images with Docker Compose" 41 | @echo " make run - Run the Docker containers in the foreground" 42 | @echo " make run-detached - Run the Docker containers in detached mode" 43 | @echo " make stop - Stop the running Docker containers" 44 | @echo " make logs - View logs for all services" 45 | @echo " make clean - Remove all containers, images, and volumes" 46 | @echo " make help - Show this help message" 47 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}Py-Config-GS{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

Welcome to the Configuration Manager

23 | 24 |
25 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 | 38 | {% with messages = get_flashed_messages(with_categories=true) %} 39 | {% if messages %} 40 |
41 |
    42 | {% for category, message in messages %} 43 |
  • {{ message }}
  • 44 | {% endfor %} 45 |
46 |
47 | {% endif %} 48 | {% endwith %} 49 | 50 |
51 | {% block content %} 52 | {% endblock %} 53 | 54 | 55 | 56 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /update_nginx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NGINX_CONFIG="/etc/nginx/sites-available/default" 4 | BACKUP_CONFIG="/etc/nginx/sites-available/default.bak" 5 | 6 | # Ensure the script is run as root 7 | if [ "$EUID" -ne 0 ]; then 8 | echo "Please run as root or use sudo." 9 | exit 1 10 | fi 11 | 12 | # Check if the Nginx configuration file exists 13 | if [ ! -f "$NGINX_CONFIG" ]; then 14 | echo "Nginx configuration file not found: $NGINX_CONFIG" 15 | exit 1 16 | fi 17 | 18 | # Backup the existing Nginx configuration 19 | echo "Creating a backup of the Nginx configuration..." 20 | cp "$NGINX_CONFIG" "$BACKUP_CONFIG" 21 | 22 | # Check if the configuration block for /improver/ is already present 23 | if grep -q "location /improver/" "$NGINX_CONFIG"; then 24 | echo "Configuration block for /improver already exists. Updating..." 25 | sed -i '/location \/improver\//,/}/d' "$NGINX_CONFIG" 26 | else 27 | echo "Adding configuration block for /improver..." 28 | fi 29 | 30 | # Insert the updated configuration block before the closing } of the server block 31 | sed -i "/^}/i \\ 32 | # Improver Flask App Configuration\\ 33 | location /improver/ {\\ 34 | proxy_pass http://127.0.0.1:5001;\\ 35 | proxy_set_header Host \$host;\\ 36 | proxy_set_header X-Real-IP \$remote_addr;\\ 37 | proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\\ 38 | proxy_set_header X-Forwarded-Proto \$scheme;\\ 39 | proxy_set_header SCRIPT_NAME /improver;\\ 40 | proxy_buffering off;\\ 41 | proxy_cache off;\\ 42 | }\\ 43 | \\n\\ 44 | # Serve static files for the Flask app\\ 45 | location /improver/static/ {\\ 46 | alias /opt/improver/app/static/;\\ 47 | expires 30d;\\ 48 | add_header Cache-Control \"public, max-age=2592000\";\\ 49 | }\\ 50 | \\n\\ 51 | # Custom error page for server errors\\ 52 | error_page 500 502 503 504 /50x.html;\\ 53 | location = /50x.html {\\ 54 | root /usr/share/nginx/html;\\ 55 | }" "$NGINX_CONFIG" 56 | 57 | # Test Nginx configuration 58 | echo "Testing Nginx configuration..." 59 | nginx -t 60 | 61 | if [ $? -eq 0 ]; then 62 | echo "Reloading Nginx..." 63 | systemctl reload nginx 64 | echo "Nginx reloaded successfully." 65 | else 66 | echo "Nginx configuration test failed. Restoring the backup..." 67 | cp "$BACKUP_CONFIG" "$NGINX_CONFIG" 68 | nginx -t 69 | exit 1 70 | fi 71 | 72 | echo "Nginx configuration updated successfully." 73 | -------------------------------------------------------------------------------- /app/templates/videos.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |
6 |

Available Videos

7 | 8 | 9 | {% with messages = get_flashed_messages(with_categories=true) %} 10 | {% if messages %} 11 | 16 | {% endif %} 17 | {% endwith %} 18 | 19 | {% if video_files %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% for video in video_files %} 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | {% endfor %} 41 | 42 |
Video FileSize (bytes)Created DateActions
{{ video['name'] }}{{ video['size'] }}{{ video['created_date'] }} 37 | 38 |
43 | {% else %} 44 |

No video files found.

45 | {% endif %} 46 |
47 | 48 | 81 | {% endblock %} -------------------------------------------------------------------------------- /.github/workflows/package-release.yml: -------------------------------------------------------------------------------- 1 | name: Package and Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v*' # Trigger for tags that start with 'v', e.g., v1.0.0, v2.1.3 8 | 9 | permissions: 10 | contents: write # Grants write access to repository contents 11 | 12 | jobs: 13 | build-and-release: 14 | runs-on: ubuntu-latest 15 | 16 | env: 17 | APP_NAME: "improver" 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | 23 | - name: Extract Tag Version 24 | id: extract_tag 25 | run: echo "TAG_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: '3.x' 31 | 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install -r requirements.txt 36 | 37 | - name: Generate version file 38 | run: | 39 | echo "Packaging version $TAG_NAME" 40 | echo $TAG_NAME > VERSION 41 | 42 | - name: Package source code as tar.gz 43 | run: | 44 | ARCHIVE_NAME="${APP_NAME}_source_${TAG_NAME}.tar.gz" 45 | echo "Packaging source code into ${ARCHIVE_NAME}..." 46 | find app config -name '__pycache__' -prune -o -type f -print | tar czvf $ARCHIVE_NAME \ 47 | -T - \ 48 | requirements.txt \ 49 | gunicorn_config.py \ 50 | run.sh \ 51 | update_nginx.sh \ 52 | improver.service \ 53 | VERSION 54 | 55 | - name: Create final deployable ZIP 56 | run: | 57 | DEPLOY_ZIP="deploy_${APP_NAME}_${TAG_NAME}.zip" 58 | echo "Creating deployable ZIP file: ${DEPLOY_ZIP}" 59 | zip -r $DEPLOY_ZIP "${APP_NAME}_source_${TAG_NAME}.tar.gz" deploy_improver.sh 60 | echo "DEPLOY_ZIP=${DEPLOY_ZIP}" >> $GITHUB_ENV 61 | 62 | - name: Fetch Autogenerated Release Notes 63 | id: fetch_notes 64 | run: | 65 | AUTH_HEADER="Authorization: token ${{ secrets.GITHUB_TOKEN }}" 66 | API_URL="https://api.github.com/repos/${{ github.repository }}/releases/generate-notes" 67 | TAG_NAME="${{ env.TAG_NAME }}" 68 | echo "Fetching autogenerated release notes for tag: $TAG_NAME" 69 | RESPONSE=$(curl -s -H "$AUTH_HEADER" -X POST -d "{\"tag_name\": \"$TAG_NAME\"}" "$API_URL") 70 | echo "$RESPONSE" | jq -r '.body' > RELEASE_NOTES.md # Save to a file 71 | shell: bash 72 | 73 | # Combine custom notes with autogenerated ones 74 | - name: Generate Release Body 75 | id: generate_body 76 | run: | 77 | echo "Adding custom release notes" 78 | CUSTOM_NOTES=$(printf "To deploy on Radxa, transfer %s to Radxa using SCP:\n\`\`\`\nscp %s root@radxa:/tmp/\n\`\`\`\nThen run \`\`\`deploy_improver.sh\`\`\` on Radxa" "$DEPLOY_ZIP" "$DEPLOY_ZIP") 79 | echo -e "$CUSTOM_NOTES\n\n$(cat RELEASE_NOTES.md)" > RELEASE_BODY.md 80 | 81 | - name: List packaged files 82 | run: ls -lh *.tar.gz *.zip 83 | 84 | 85 | - name: Create GitHub Release and Upload Assets 86 | uses: softprops/action-gh-release@v1 87 | if: startsWith(github.ref, 'refs/tags/') 88 | with: 89 | draft: true 90 | body_path: RELEASE_BODY.md # Use the generated body 91 | files: | 92 | *.zip 93 | -------------------------------------------------------------------------------- /app/templates/play.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | 6 |
7 |
8 | File: {{ filename }} 9 |
10 |
11 | 12 | Download 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | 25 | 26 | 27 |
28 | 29 | 36 |
37 | 38 |
39 | 40 |
41 |

Video Details

42 |
    43 |
  • File Name: {{ filename }}
  • 44 | {% if video_stats %} 45 |
  • Duration: {{ video_stats.duration | format_duration }}
  • 46 |
  • Resolution: {{ video_stats.resolution }}
  • 47 |
  • Codec: {{ video_stats.codec }}
  • 48 |
  • Bitrate: {{ (video_stats.bitrate / 1000) | round(2) }} kbps
  • 49 |
  • Frame Rate: {{ video_stats.frame_rate }}
  • 50 | {% else %} 51 |
  • Unable to retrieve video stats.
  • 52 | {% endif %} 53 |
54 |
55 | 56 | 57 | 60 |
61 | 62 | 63 | 96 | {% endblock %} 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .vscode 3 | *.mp4 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | *.tar.gz 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 115 | .pdm.toml 116 | .pdm-python 117 | .pdm-build/ 118 | 119 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 120 | __pypackages__/ 121 | 122 | # Celery stuff 123 | celerybeat-schedule 124 | celerybeat.pid 125 | 126 | # SageMath parsed files 127 | *.sage.py 128 | 129 | # Environments 130 | .env 131 | .venv 132 | env/ 133 | venv/ 134 | ENV/ 135 | env.bak/ 136 | venv.bak/ 137 | 138 | # Spyder project settings 139 | .spyderproject 140 | .spyproject 141 | 142 | # Rope project settings 143 | .ropeproject 144 | 145 | # mkdocs documentation 146 | /site 147 | 148 | # mypy 149 | .mypy_cache/ 150 | .dmypy.json 151 | dmypy.json 152 | 153 | # Pyre type checker 154 | .pyre/ 155 | 156 | # pytype static type analyzer 157 | .pytype/ 158 | 159 | # Cython debug symbols 160 | cython_debug/ 161 | 162 | # PyCharm 163 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 164 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 165 | # and can be added to the global gitignore or merged into this file. For a more nuclear 166 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 167 | #.idea/ 168 | 169 | logs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Improver 2 | 3 | *Temporarily put on hold since the work being done to put this in Betaflight Menus and to work on Avalonia Configurator ([OpenIPC-Config](https://github.com/OpenIPC/openipc-configurator)) MultiPlatform Configurator.* 4 | 5 | @JohhnGoblin has a newer version in the (Radxa image)[https://github.com/OpenIPC/sbc-groundstations/releases/tag/zero3w-v1.9.6] so this will probably be archived. 6 | 7 | 8 | OpenIPC Improver for setting up FPV and URLLC devices 9 | 10 | I wanted an easy way to edit files and watch videos on the Radxa 11 | 12 | ## Screenshots 13 | Home Page 14 | ![alt text](images/home.png) 15 | 16 | Editor 17 | ![alt text](images/editor.png) 18 | 19 | Video file selector 20 | ![alt text](images/v_select.png) 21 | 22 | Player 23 | ![alt text](images/v_player.png) 24 | 25 | Journalctl -f 26 | ![alt text](images/journal.png) 27 | 28 | ## Installation 29 | 30 | * Download latest release, 2 files 31 | * improver_source.tar.gz 32 | * deploy_improver.sh 33 | 34 | * transfer files to Radxa 35 | * ```scp improver_source.tar.gz deploy_improver.sh root@10.0.1.215:/tmp/``` 36 | * ```chmod +x deploy_improver.sh``` 37 | * ```./deploy_improver.sh``` 38 | 39 | 40 | ## Network Setup 41 | This requires either a Wifi or Lan connection 42 | 43 | * Ethernet 44 | * Navigate to http://radxaip:8080/improver 45 | * Wifi (Hotspot setup) 46 | ``` 47 | # run 'ip a' to determine which interface is your wifi address 48 | 49 | nmcli connection add type wifi ifname wlan1 con-name "MyWiFiNetwork" ssid "MyWiFiNetwork" 50 | 51 | nmcli connection modify "MyWiFiNetwork" wifi-sec.key-mgmt wpa-psk 52 | 53 | nmcli connection modify "MyWiFiNetwork" wifi-sec.psk "MyPassword123" 54 | 55 | nmcli connection up "MyWiFiNetwork" 56 | 57 | # You will also need to setup a static IP on the same subnet as your mobile device 58 | 59 | nmcli connection modify "MyWiFiNetwork" ipv4.addresses 192.168.1.100/24 60 | 61 | nmcli connection modify "MyWiFiNetwork" ipv4.gateway 192.168.1.1 62 | 63 | nmcli connection modify "MyWiFiNetwork" ipv4.dns 8.8.8.8,8.8.4.4 64 | 65 | nmcli connection modify "MyWiFiNetwork" ipv4.method manual 66 | ``` 67 | 68 | then navigate to http://mobile-ip:8080/improver/ 69 | 70 | 71 | ## Dev Setup and Running 72 | 73 | * Use Docker, it will bring up the flask and nginx apps 74 | ``` 75 | docker-compose up --build 76 | ``` 77 | Navigate to http://localhost/improver 78 | 79 | 80 | 81 | 82 | * Explanation of Makefile Targets 83 | * build: Builds the Docker images as specified in docker-compose.yml. 84 | * run: Builds (if needed) and runs the containers in the foreground. 85 | * run-detached: Builds (if needed) and runs the containers in detached mode (background). 86 | * stop: Stops all running containers defined in docker-compose.yml. 87 | * logs: Shows real-time logs from all services for debugging. 88 | * clean: Stops containers and removes all images, volumes, and orphaned containers associated with this Docker Compose setup. 89 | 90 | * Usage 91 | 1. Build the Images: 92 | ``` 93 | make build 94 | ``` 95 | 2. Run the Containers in Foreground: 96 | ``` 97 | make run 98 | ``` 99 | 3. Run the Containers in Detached Mode: 100 | ``` 101 | make run-detached 102 | ``` 103 | 4. Stop the Containers: 104 | ``` 105 | make stop 106 | ``` 107 | 5. View Logs: 108 | ``` 109 | make logs 110 | ``` 111 | 6. Clean Up Containers and Images: 112 | ``` 113 | make clean 114 | ``` 115 | 116 | With this Makefile, you can easily manage the lifecycle of your multi-container setup for testing the Flask app with Nginx in Docker. Let me know if you need more customization! 117 | 118 | 119 | ## Service file 120 | 121 | To manage the Improver Flask service, use the following commands: 122 | - Start the service: ```sudo systemctl start improver.service``` 123 | - Stop the service: ```sudo systemctl stop improver.service``` 124 | - Restart the service: ```sudo systemctl restart improver.service``` 125 | - Check status: ```sudo systemctl status improver.service``` 126 | 127 | ============================================================== 128 | 129 | 130 | ### Enable and Start the Service 131 | 132 |

133 |
134 |

This is an open project, so you can help, too.

135 | 136 | We try to collect, organize and share as much information regarding different aspects of the project as we can. But sometimes we overlook things that seem obvious to us, developers, but are not so obvious to end-users, people who are less familiar with nuts and bolts behind the scene. That is why we set up this wiki and let anyone having a GitHub account to make additions and improvements to the knowledgebase. Read [How to contribute](https://github.com/OpenIPC/wiki/blob/master/en/contribute.md). 137 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # app/__init__.py 2 | import logging 3 | from flask import Flask, request 4 | import json 5 | import os 6 | from importlib.metadata import version 7 | from werkzeug.middleware.proxy_fix import ProxyFix 8 | from logging.handlers import TimedRotatingFileHandler 9 | 10 | # Define the log file path 11 | log_file = '/opt/improver/logs/improver_app.log' 12 | 13 | # Create a TimedRotatingFileHandler to rotate the log file every day 14 | handler = TimedRotatingFileHandler(log_file, when="midnight", interval=1, backupCount=7) 15 | handler.setLevel(logging.DEBUG) 16 | handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')) 17 | 18 | # Get the root logger and set the level 19 | logger = logging.getLogger() 20 | logger.setLevel(logging.DEBUG) 21 | 22 | # Clear only if there are default handlers 23 | if logger.hasHandlers(): 24 | logger.handlers.clear() 25 | 26 | # Re-add the TimedRotatingFileHandler 27 | logger.addHandler(handler) 28 | 29 | # Log a startup message to verify logging works 30 | 31 | logger.info("Flask app started and logging to /opt/improver/logs/improver_app.log") 32 | 33 | 34 | 35 | def get_app_version(): 36 | # Assuming __file__ is within a sub-directory (e.g., /app), navigate to the project root 37 | root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 38 | version_file = os.path.join(root_dir, 'VERSION') 39 | try: 40 | with open(version_file, 'r') as f: 41 | return f.read().strip() 42 | except FileNotFoundError: 43 | return 'unknown' 44 | 45 | 46 | def format_duration(seconds): 47 | """Convert seconds to hh:mm:ss format.""" 48 | h = int(seconds // 3600) 49 | m = int((seconds % 3600) // 60) 50 | s = int(seconds % 60) 51 | return f"{h:02}:{m:02}:{s:02}" 52 | 53 | def create_app(): 54 | app = Flask(__name__) 55 | 56 | # Load the app version 57 | app_version = get_app_version() 58 | app.config['APP_VERSION'] = app_version 59 | 60 | logger.debug(f"********************************************************************************") 61 | logger.debug(f"Starting app version: {app_version}") 62 | logger.debug(f"********************************************************************************") 63 | 64 | # Register the format_duration filter 65 | app.jinja_env.filters['format_duration'] = format_duration 66 | 67 | # Set the application root 68 | app.config['APPLICATION_ROOT'] = '/improver' 69 | 70 | # Apply ProxyFix to handle SCRIPT_NAME 71 | app.wsgi_app = ProxyFix(app.wsgi_app, x_prefix=1) 72 | 73 | app.secret_key = os.getenv('SECRET_KEY', 'default_secret_key') 74 | 75 | if os.getenv('FLASK_ENV') == 'development': 76 | SETTINGS_FILE = os.getenv('SETTINGS_FILE', '/config/py-config-gs-dev.json') 77 | else: 78 | SETTINGS_FILE = os.getenv('SETTINGS_FILE', '/config/py-config-gs.json') 79 | 80 | logger.info(f"FLASK_ENV: {os.getenv('FLASK_ENV')}") 81 | logger.info(f"SETTINGS_FILE: {os.getenv('SETTINGS_FILE')}") 82 | logger.info(f"Loading settings from: {SETTINGS_FILE}") 83 | 84 | with open(SETTINGS_FILE, 'r') as f: 85 | # Load settings.json 86 | settings = json.load(f) 87 | app.config['CONFIG_FILES'] = settings['config_files'] 88 | app.config['VIDEO_DIR'] = os.path.expanduser(settings['VIDEO_DIR']) 89 | app.config['SERVER_PORT'] = settings['SERVER_PORT'] 90 | logger.debug(f'Loaded settings: {settings}') 91 | 92 | 93 | # Determine the settings file based on the environment 94 | if os.getenv('FLASK_ENV') == 'development': 95 | #SETTINGS_FILE = os.path.expanduser('/config/py-config-gs.json') 96 | app.config['GS_UPLOAD_FOLDER'] = os.path.join(os.getcwd(), 'uploads_dev') 97 | else: 98 | #SETTINGS_FILE = '/config/py-config-gs.json' 99 | app.config['GS_UPLOAD_FOLDER'] = '/etc/' 100 | 101 | # Ensure the upload folder exists 102 | os.makedirs(app.config['GS_UPLOAD_FOLDER'], exist_ok=True) 103 | logger.info(f"Upload folder set to: {app.config['GS_UPLOAD_FOLDER']}") 104 | 105 | 106 | # Log SCRIPT_NAME for debugging 107 | # @app.before_request 108 | # def log_script_name(): 109 | # script_name = request.environ.get('SCRIPT_NAME', '') 110 | # logger.debug(f"SCRIPT_NAME: {script_name}") 111 | # logger.debug(f"PATH_INFO: {request.environ.get('PATH_INFO')}") 112 | 113 | # Import and register blueprints 114 | # Determine the blueprint prefix based on the environment 115 | if os.getenv('FLASK_ENV') == 'development': 116 | url_prefix = '/improver' # Development uses '/improver' for testing 117 | else: 118 | url_prefix = '/' # Production uses root 119 | 120 | # Register blueprint with the correct prefix 121 | from .routes import main 122 | app.register_blueprint(main, url_prefix=url_prefix) 123 | 124 | 125 | return app -------------------------------------------------------------------------------- /app/static/css/styles.css: -------------------------------------------------------------------------------- 1 | /* General Styles */ 2 | body { 3 | font-family: Arial, sans-serif; 4 | margin: 0; 5 | padding: 20px; 6 | background-color: #f4f4f4; 7 | line-height: 1.6; 8 | } 9 | 10 | h1, h2 { 11 | color: #333; 12 | } 13 | 14 | p { 15 | color: #555; 16 | } 17 | 18 | /* Tabs */ 19 | .tabs { 20 | margin-bottom: 20px; 21 | } 22 | 23 | .tabs ul { 24 | list-style: none; 25 | padding: 0; 26 | display: flex; 27 | background-color: #e2e2e2; 28 | border-radius: 5px; 29 | flex-wrap: wrap; /* Allows tabs to wrap on small screens */ 30 | } 31 | 32 | .tabs li { 33 | margin: 0; 34 | flex: 1 1 auto; /* Allow tabs to be responsive */ 35 | } 36 | 37 | .tabs a { 38 | display: block; 39 | padding: 10px 15px; 40 | text-decoration: none; 41 | color: #333; 42 | text-align: center; 43 | border-radius: 5px; 44 | margin-right: 5px; 45 | transition: background-color 0.3s; 46 | } 47 | 48 | .tabs a:hover { 49 | background-color: #d1d1d1; 50 | } 51 | 52 | /* Content Area */ 53 | .content { 54 | background: white; 55 | padding: 20px; 56 | border-radius: 5px; 57 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 58 | } 59 | 60 | .content ul { 61 | padding: 0; 62 | } 63 | 64 | .content li { 65 | list-style: none; 66 | margin-bottom: 10px; 67 | } 68 | 69 | .content a { 70 | text-decoration: none; 71 | color: #007bff; 72 | } 73 | 74 | .content a:hover { 75 | text-decoration: underline; 76 | } 77 | 78 | /* Video Container */ 79 | .video-container { 80 | display: flex; 81 | flex-wrap: wrap; 82 | gap: 10px; 83 | } 84 | 85 | .video-container video { 86 | max-width: 100%; 87 | border-radius: 5px; 88 | } 89 | 90 | /* Footer Styles */ 91 | footer { 92 | background-color: #f1f1f1; 93 | text-align: center; 94 | padding: 10px; 95 | width: 100%; 96 | position: relative; /* Change from fixed to relative */ 97 | bottom: 0; 98 | flex-shrink: 0; /* Ensure the footer is not squeezed by the content */ 99 | } 100 | 101 | /* Styled Box */ 102 | .styled-box { 103 | border: 1px solid #ccc; 104 | background-color: #f0f0f0; 105 | padding: 10px; 106 | margin: 15px 0; 107 | border-radius: 5px; 108 | font-family: Arial, sans-serif; 109 | } 110 | 111 | /* Warning Styles */ 112 | .warning { 113 | color: red; 114 | font-weight: bold; 115 | font-style: italic; 116 | margin-bottom: 20px; 117 | } 118 | 119 | /* Table Styles */ 120 | table { 121 | width: 100%; /* Make the table take full width */ 122 | border-collapse: collapse; 123 | margin: 20px 0; 124 | font-size: 16px; 125 | } 126 | 127 | table th, table td { 128 | padding: 15px; /* Increase padding for more space inside cells */ 129 | text-align: left; 130 | border: 1px solid #ddd; 131 | } 132 | 133 | table th { 134 | background-color: #f2f2f2; 135 | font-weight: bold; 136 | } 137 | 138 | table th:nth-child(1), 139 | table td:nth-child(1) { 140 | width: 30%; /* Service column */ 141 | } 142 | 143 | table th:nth-child(2), 144 | table td:nth-child(2) { 145 | width: 20%; /* Status column */ 146 | } 147 | 148 | table th:nth-child(3), 149 | table td:nth-child(3) { 150 | width: 50%; /* Actions column */ 151 | } 152 | 153 | /* Status Styles */ 154 | .status-enabled { 155 | color: green; 156 | font-weight: bold; 157 | } 158 | 159 | .status-disabled { 160 | color: red; 161 | font-weight: bold; 162 | } 163 | 164 | /* Button Styles */ 165 | button { 166 | padding: 12px 16px; /* Increase button size for touch screens */ 167 | margin: 0 5px; 168 | border: none; 169 | cursor: pointer; 170 | background-color: #007bff; 171 | color: white; 172 | border-radius: 4px; 173 | font-size: 16px; 174 | } 175 | 176 | button[type="submit"]:hover { 177 | background-color: #0056b3; 178 | } 179 | 180 | /* Form Styles */ 181 | form { 182 | display: inline; 183 | } 184 | 185 | /* Responsive Layout */ 186 | @media (max-width: 768px) { 187 | body { 188 | padding: 10px; 189 | } 190 | 191 | .tabs ul { 192 | flex-direction: column; /* Stack tabs on small screens */ 193 | } 194 | 195 | button { 196 | width: 100%; /* Make buttons full-width on mobile */ 197 | margin-bottom: 10px; 198 | } 199 | 200 | table th, table td { 201 | font-size: 14px; /* Smaller text for tables */ 202 | padding: 10px; 203 | } 204 | 205 | .content { 206 | padding: 15px; 207 | } 208 | 209 | footer { 210 | padding: 8px; 211 | } 212 | } 213 | 214 | 215 | /* Configuration Files Section */ 216 | .config-section { 217 | border: 1px solid #ccc; /* Thin border around the section */ 218 | border-radius: 5px; /* Optional: Add border radius for smooth edges */ 219 | padding: 20px; 220 | margin-bottom: 20px; /* Space between this section and others */ 221 | background-color: #fff; 222 | } 223 | 224 | .config-section h2 { 225 | margin-top: 0; /* Remove extra top margin */ 226 | font-size: 24px; 227 | color: #333; 228 | } 229 | 230 | /* Configuration Content */ 231 | .config-content { 232 | display: flex; 233 | flex-direction: column; /* Align children vertically */ 234 | gap: 15px; /* Add space between Edit and Actions */ 235 | } 236 | 237 | /* Individual Config Items */ 238 | .config-item h3 { 239 | font-size: 20px; 240 | margin: 0 0 10px 0; /* Space below each heading */ 241 | color: #007bff; /* Optional: Add color to the headings */ 242 | } 243 | 244 | .config-item p { 245 | margin: 0; 246 | color: #555; 247 | font-size: 16px; 248 | } 249 | 250 | /* For mobile responsiveness, if needed */ 251 | @media (max-width: 768px) { 252 | .config-content { 253 | gap: 10px; /* Reduce gap on smaller screens */ 254 | } 255 | } -------------------------------------------------------------------------------- /app/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 | 6 | 7 |
8 |

Configuration Files

9 |
10 |
11 |

Edit

12 | {% if config_files %} 13 |
    14 | {% for file in config_files %} 15 |
  • 16 | {{ file.name }} 17 |
  • 18 | {% endfor %} 19 |
20 | {% else %} 21 |

No configuration files available.

22 | {% endif %} 23 |
24 |
25 |

Actions

26 | 31 |
32 |
33 |
34 | 35 | 36 |
37 |

Radxa Temps

38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 53 | 56 | 57 | 58 | 59 | 62 | 65 | 66 | 67 |
ComponentTemperature (°C)Temperature (°F)
SOC Temperature 51 |
N/A
52 |
54 |
N/A
55 |
GPU Temperature 60 |
N/A
61 |
63 |
N/A
64 |
68 |
69 | 70 |
71 | 72 | 73 | 74 |
75 |

System Control

76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {% for service, status in services.items() %} 88 | 89 | 90 | 95 | 112 | 113 | {% endfor %} 114 | 115 |
ServiceStatusActions
{{ service }} 91 | 92 | {{ status.capitalize() }} 93 | 94 | 96 | 97 |
98 | 99 | {% if status == 'enabled' %} 100 | 101 | {% else %} 102 | 103 | {% endif %} 104 |
105 | 106 | 107 |
108 | 109 | 110 |
111 |
116 |
117 | 118 | 119 | 134 | 135 | 165 | 166 | 167 | {% endblock %} -------------------------------------------------------------------------------- /deploy_improver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NGINX_CONFIG="/etc/nginx/sites-available/default" 4 | BACKUP_CONFIG="/etc/nginx/sites-available/default.bak" 5 | SUDOERS_FILE="/etc/sudoers" 6 | ARCHIVE_NAME="improver_source.tar.gz" 7 | DEST_DIR="/opt/improver" 8 | LOG_DIR="/opt/improver/logs" 9 | VENV_DIR="$DEST_DIR/venv" 10 | CONFIG_SRC="$DEST_DIR/config/py-config-gs.json" 11 | CONFIG_DEST="/config/py-config-gs.json" 12 | CONFIG_BACKUP="/config/py-config-gs.json.bak" 13 | 14 | # Ensure the script is run as root 15 | if [ "$EUID" -ne 0 ]; then 16 | echo "Please run as root or use sudo." 17 | exit 1 18 | fi 19 | 20 | # Step 1: Create necessary directories 21 | echo "Creating necessary directories..." 22 | mkdir -p "$DEST_DIR" 23 | mkdir -p "$LOG_DIR" 24 | mkdir -p "/config" 25 | 26 | # Step 2: Create a virtual environment 27 | echo "Creating a virtual environment..." 28 | python3 -m venv "$VENV_DIR" 29 | 30 | 31 | apt install ffmpeg -y 32 | 33 | # Step 3: Activate the virtual environment 34 | echo "Activating the virtual environment..." 35 | source "$VENV_DIR/bin/activate" 36 | 37 | # Step 4: Install required packages in the virtual environment 38 | echo "Installing required packages..." 39 | pip install --upgrade pip 40 | pip install gunicorn 41 | 42 | # Step 5: Extract the archive 43 | if [ ! -f "$ARCHIVE_NAME" ]; then 44 | echo "Archive file $ARCHIVE_NAME not found." 45 | exit 1 46 | fi 47 | 48 | echo "Extracting $ARCHIVE_NAME to $DEST_DIR..." 49 | tar xzvf "$ARCHIVE_NAME" -C "$DEST_DIR" 50 | chown -R root:root "$DEST_DIR" 51 | chmod -R 755 "$DEST_DIR" 52 | 53 | # Step 6: Install application dependencies from requirements.txt 54 | if [ -f "$DEST_DIR/requirements.txt" ]; then 55 | echo "Installing application dependencies..." 56 | pip install -r "$DEST_DIR/requirements.txt" 57 | else 58 | echo "requirements.txt not found in $DEST_DIR. Skipping dependency installation." 59 | fi 60 | 61 | # Step 7: Configure Nginx 62 | echo "Creating a backup of the Nginx configuration..." 63 | cp "$NGINX_CONFIG" "$BACKUP_CONFIG" 64 | 65 | if grep -q "location /improver/" "$NGINX_CONFIG"; then 66 | echo "Configuration block for /improver already exists. Updating..." 67 | sed -i '/location \/improver\//,/}/d' "$NGINX_CONFIG" 68 | fi 69 | 70 | echo "Updating Nginx configuration..." 71 | sed -i "/^}/i \\ 72 | # Improver Flask App Configuration\\ 73 | location /improver/ {\\ 74 | proxy_pass http://127.0.0.1:5001;\\ 75 | proxy_set_header Host \$host;\\ 76 | proxy_set_header X-Real-IP \$remote_addr;\\ 77 | proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\\ 78 | proxy_set_header X-Forwarded-Proto \$scheme;\\ 79 | proxy_set_header SCRIPT_NAME /improver;\\ 80 | proxy_buffering off;\\ 81 | proxy_cache off;\\ 82 | }\\ 83 | \\n\\ 84 | # Serve static files for the Flask app\\ 85 | location /improver/static/ {\\ 86 | alias /opt/improver/app/static/;\\ 87 | expires 30d;\\ 88 | add_header Cache-Control \"public, max-age=2592000\";\\ 89 | }\\ 90 | \\n\\ 91 | " "$NGINX_CONFIG" 92 | 93 | # Test Nginx configuration 94 | echo "Testing Nginx configuration..." 95 | nginx -t 96 | 97 | if [ $? -eq 0 ]; then 98 | echo "Reloading Nginx..." 99 | systemctl reload nginx 100 | else 101 | echo "Nginx configuration test failed. Restoring backup..." 102 | cp "$BACKUP_CONFIG" "$NGINX_CONFIG" 103 | nginx -t 104 | exit 1 105 | fi 106 | 107 | # Step 8: Add sudo permissions for www-data user 108 | echo "Adding sudo permissions for www-data..." 109 | if ! grep -q "www-data.*systemctl" "$SUDOERS_FILE"; then 110 | echo "Adding sudo permissions for www-data..." 111 | echo "www-data ALL=(ALL) NOPASSWD: /usr/bin/journalctl, /bin/systemctl restart wifibroadcast.service, /bin/systemctl restart openipc, /bin/rm /media/*" | tee -a "$SUDOERS_FILE" 112 | else 113 | echo "www-data already has necessary sudo permissions." 114 | fi 115 | 116 | # Step 9: Copy the configuration file to /config 117 | echo "Copying configuration file to /config..." 118 | if [ -f "$CONFIG_DEST" ]; then 119 | echo "Configuration file already exists. Creating a backup at $CONFIG_BACKUP..." 120 | cp "$CONFIG_DEST" "$CONFIG_BACKUP" 121 | fi 122 | cp "$CONFIG_SRC" "$CONFIG_DEST" 123 | 124 | # Step 10: Verify the deployment 125 | echo "Verifying the deployment..." 126 | if [ -f "$DEST_DIR/run.sh" ]; then 127 | echo "Run script found. You can start the application with:" 128 | echo "sudo $DEST_DIR/run.sh" 129 | else 130 | echo "Run script not found. Please check the extracted files." 131 | fi 132 | 133 | echo "Changing permissions on $LOG_DIR" 134 | chown -R www-data:www-data "$LOG_DIR" 135 | chmod -R 755 "$LOG_DIR" 136 | 137 | # Step 11: Provide instructions for creating and managing the systemd service 138 | SERVICE_FILE_PATH="/etc/systemd/system/improver.service" 139 | 140 | if [ ! -f "$SERVICE_FILE_PATH" ]; then 141 | echo "Creating systemd service file at $SERVICE_FILE_PATH..." 142 | 143 | cat < $SERVICE_FILE_PATH 144 | [Unit] 145 | Description=Improver Flask Application 146 | After=network.target 147 | 148 | [Service] 149 | User=www-data 150 | Group=www-data 151 | WorkingDirectory=/opt/improver 152 | ExecStart=/opt/improver/venv/bin/gunicorn -w 4 -b 127.0.0.1:5001 app:create_app() 153 | Restart=always 154 | Environment="FLASK_ENV=production" 155 | Environment="SETTINGS_FILE=/config/py-config-gs.json" 156 | 157 | [Install] 158 | WantedBy=multi-user.target 159 | EOL 160 | 161 | echo "Reloading systemd to recognize the new service..." 162 | systemctl daemon-reload 163 | 164 | echo "Enabling the Improver service to start on boot..." 165 | systemctl enable improver.service 166 | 167 | echo "Starting the Improver service..." 168 | systemctl start improver.service 169 | 170 | echo "Service created and started successfully." 171 | else 172 | echo "Service file $SERVICE_FILE_PATH already exists. Skipping creation." 173 | fi 174 | 175 | echo 176 | echo "==============================================================" 177 | echo "Deployment completed successfully." 178 | echo 179 | echo "To manage the Improver Flask service, use the following commands:" 180 | echo " - Start the service: sudo systemctl start improver.service" 181 | echo " - Stop the service: sudo systemctl stop improver.service" 182 | echo " - Restart the service: sudo systemctl restart improver.service" 183 | echo " - Check status: sudo systemctl status improver.service" 184 | echo "==============================================================" 185 | echo . 186 | echo "OpenIPC kick ass!" 187 | -------------------------------------------------------------------------------- /app/routes.py: -------------------------------------------------------------------------------- 1 | # app/routes.py 2 | from flask import Blueprint, render_template, request, redirect, jsonify,url_for, flash, Response, send_from_directory, current_app, abort 3 | from importlib.metadata import version 4 | from werkzeug.utils import secure_filename 5 | import subprocess 6 | import os 7 | import platform 8 | import logging 9 | import time 10 | from datetime import datetime 11 | import json 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | main = Blueprint('main', __name__) # Define the Blueprint 16 | 17 | 18 | @main.route('/health') 19 | def health(): 20 | return {"status": "ok"}, 200 21 | 22 | 23 | # Add this function in routes.py 24 | @main.route('/get_logs', methods=['GET']) 25 | def get_logs(): 26 | """Fetch the last 50 lines from journalctl.""" 27 | 28 | logger.debug("Fetching logs...") 29 | try: 30 | if os.getenv("FLASK_ENV") != "development": 31 | # Run journalctl command to get the last 50 lines 32 | result = subprocess.run( 33 | ["journalctl", "-n", "50", "-u", "improver.service", "--no-pager"], 34 | stdout=subprocess.PIPE, 35 | stderr=subprocess.PIPE, 36 | text=True 37 | ) 38 | if result.returncode == 0: 39 | return jsonify({"logs": result.stdout.splitlines()}) 40 | else: 41 | return jsonify({"error": result.stderr}), 500 42 | else: 43 | # Development mode: return a mock response 44 | return jsonify({"logs": ["Development mode: No logs available"]}) 45 | except Exception as e: 46 | current_app.logger.error(f"Error fetching logs: {e}") 47 | return jsonify({"error": str(e)}), 500 48 | 49 | 50 | @main.route('/journal') 51 | def journal(): 52 | app_version = current_app.config.get('APP_VERSION', 'unknown') 53 | 54 | return render_template('journal.html', version=app_version) 55 | 56 | 57 | # Define the stream_journal function 58 | def stream_journal(): 59 | """Stream journalctl output in real-time.""" 60 | if os.getenv("FLASK_ENV") != "development": 61 | process = subprocess.Popen( 62 | ["journalctl", "-n 100", "-f"], 63 | stdout=subprocess.PIPE, 64 | stderr=subprocess.PIPE, 65 | text=True, 66 | ) 67 | while True: 68 | output = process.stdout.readline() 69 | if output: 70 | yield f"data: {output}\n\n" 71 | else: 72 | break 73 | else: 74 | logger.info("No data in DEVELOPMENT mode") 75 | yield "data: No data in DEVELOPMENT mode\n\n" 76 | 77 | 78 | @main.route('/stream') 79 | def stream(): 80 | return Response(stream_journal(), content_type='text/event-stream') 81 | 82 | 83 | @main.route('/') 84 | def home(): 85 | config_files = current_app.config['CONFIG_FILES'] 86 | video_dir = current_app.config['VIDEO_DIR'] 87 | app_version = current_app.config['APP_VERSION'] 88 | 89 | services = ['openipc', 'wifibroadcast.service'] 90 | service_statuses = {} 91 | 92 | # Check if running inside Docker 93 | is_docker = os.path.exists('/.dockerenv') 94 | 95 | # Check if the current system is Linux 96 | if is_docker: 97 | # Skip systemctl check inside Docker for each service 98 | for service in services: 99 | service_statuses[service] = 'unknown (Docker)' 100 | elif platform.system() == 'Linux': 101 | # Check service status using systemctl on Linux systems 102 | for service in services: 103 | try: 104 | result = subprocess.run( 105 | ['systemctl', 'is-enabled', service], 106 | stdout=subprocess.PIPE, 107 | stderr=subprocess.PIPE 108 | ) 109 | status = result.stdout.decode('utf-8').strip() 110 | service_statuses[service] = status 111 | except Exception as e: 112 | current_app.logger.error(f"Error checking service {service}: {e}") 113 | service_statuses[service] = 'error' 114 | else: 115 | # Skip systemctl checks on non-Linux systems 116 | service_statuses = {service: 'not applicable (non-Linux system)' for service in services} 117 | 118 | return render_template('home.html', config_files=config_files, version=app_version, is_docker=is_docker, services=service_statuses) 119 | 120 | 121 | @main.route('/edit/', methods=['GET', 'POST']) 122 | def edit(filename): 123 | try: 124 | # Get the config files from app configuration 125 | config_files = current_app.config['CONFIG_FILES'] 126 | 127 | # Determine the file path based on the environment 128 | running_env = os.getenv('FLASK_ENV', 'production') 129 | 130 | if running_env == 'development': 131 | # Use a local path for development 132 | file_path = next((item['path'] for item in config_files if item['name'] == filename and '/etc/' not in item['path']), None) 133 | else: 134 | # Use standard path in production 135 | file_path = next((item['path'] for item in config_files if item['name'] == filename), None) 136 | 137 | # If file is not found, set content to None and log the error 138 | if not file_path or not os.path.exists(file_path): 139 | current_app.logger.error(f"Configuration file {filename} not found at {file_path}.") 140 | content = None 141 | else: 142 | # If a POST request, update the file content 143 | if request.method == 'POST': 144 | content = request.form['content'] 145 | with open(file_path, 'w') as f: 146 | f.write(content) 147 | current_app.logger.debug(f'Updated configuration file: {filename}') 148 | return redirect(url_for('main.home')) 149 | 150 | # Otherwise, read the file content for display 151 | current_app.logger.debug(f'Reading configuration file: {file_path}') 152 | with open(file_path, 'r') as f: 153 | content = f.read() 154 | 155 | # Render the template, passing content (None if file not found) 156 | return render_template('edit.html', filename=filename, content=content, version=current_app.config['APP_VERSION']) 157 | 158 | except Exception as e: 159 | current_app.logger.error(f'Error in edit route: {e}') 160 | return render_template('edit.html', filename=filename, content=None, version=current_app.config['APP_VERSION']) 161 | 162 | 163 | 164 | @main.route('/save/', methods=['POST']) 165 | def save(filename): 166 | # Access CONFIG_FILES from app configuration 167 | config_files = current_app.config['CONFIG_FILES'] 168 | file_path = next((item['path'] for item in config_files if item['name'] == filename), None) 169 | content = request.form['content'] 170 | with open(file_path, 'w') as f: 171 | f.write(content) 172 | logger.debug(f'Saved configuration file: {filename}') 173 | return redirect(url_for('main.home')) 174 | 175 | 176 | def get_video_stats(video_path): 177 | try: 178 | # Use ffprobe to extract video metadata 179 | command = [ 180 | 'ffprobe', '-v', 'quiet', '-print_format', 'json', 181 | '-show_format', '-show_streams', video_path 182 | ] 183 | result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) 184 | metadata = json.loads(result.stdout) 185 | 186 | # Extract useful stats 187 | streams = metadata.get('streams', []) 188 | video_stream = next((s for s in streams if s.get('codec_type') == 'video'), {}) 189 | format_info = metadata.get('format', {}) 190 | 191 | stats = { 192 | 'duration': float(format_info.get('duration', 0)), # seconds 193 | 'bitrate': int(format_info.get('bit_rate', 0)), # bits per second 194 | 'size': int(format_info.get('size', 0)), # bytes 195 | 'codec': video_stream.get('codec_long_name', 'unknown'), 196 | 'resolution': f"{video_stream.get('width', 0)}x{video_stream.get('height', 0)}", 197 | 'frame_rate': video_stream.get('r_frame_rate', 'unknown'), 198 | } 199 | return stats 200 | except Exception as e: 201 | current_app.logger.error(f"Error extracting video stats: {e}") 202 | return None 203 | 204 | 205 | @main.route('/videos') 206 | def videos(): 207 | try: 208 | # Access VIDEO_DIR using current_app.config 209 | video_dir = current_app.config['VIDEO_DIR'] 210 | video_files = [] 211 | 212 | # Get list of video files with their sizes and creation dates 213 | for f in os.listdir(video_dir): 214 | if f.endswith(('.mp4', '.mkv', '.avi')): 215 | file_path = os.path.join(video_dir, f) 216 | file_size = os.path.getsize(file_path) 217 | file_stat = os.stat(file_path) 218 | 219 | # Get the creation time of the file (or last metadata change on some systems) 220 | created_date = datetime.fromtimestamp(file_stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S') 221 | 222 | video_files.append({ 223 | 'name': f, 224 | 'size': file_size, 225 | 'created_date': created_date 226 | }) 227 | 228 | current_app.logger.debug(f'VIDEO_DIR: {video_dir}') 229 | current_app.logger.debug(f'Video files found: {video_files}') 230 | 231 | return render_template( 232 | 'videos.html', 233 | video_files=video_files, 234 | version=current_app.config['APP_VERSION'] 235 | ) 236 | except Exception as e: 237 | current_app.logger.error(f'Error listing video files: {e}') 238 | return "Error listing video files", 500 239 | 240 | 241 | @main.route('/delete_video', methods=['POST']) 242 | def delete_video(): 243 | try: 244 | # Get the video directory from app configuration 245 | video_dir = current_app.config.get('VIDEO_DIR') 246 | 247 | # Check if VIDEO_DIR is properly set 248 | if not video_dir: 249 | current_app.logger.error('VIDEO_DIR is not set in the app configuration.') 250 | return jsonify({'error': 'Internal server error: VIDEO_DIR not configured'}), 500 251 | 252 | # Get the filename from the request JSON 253 | request_data = request.get_json() 254 | filename = request_data.get('filename') 255 | 256 | # Validate filename 257 | if not filename: 258 | current_app.logger.error('No filename provided in the request.') 259 | return jsonify({'error': 'No filename provided'}), 400 260 | 261 | # Construct the full path of the file to be deleted 262 | file_path = os.path.join(video_dir, filename) 263 | 264 | # Check if the file exists 265 | if not os.path.exists(file_path): 266 | current_app.logger.error(f'File not found: {file_path}') 267 | return jsonify({'error': 'File not found'}), 404 268 | 269 | # Delete the file 270 | # os.remove(file_path) 271 | command = ['sudo', 'rm', file_path] 272 | subprocess.run(command, check=True) 273 | 274 | current_app.logger.info(f'File deleted: {file_path}') 275 | 276 | flash('File deleted successfully', 'success') 277 | 278 | return jsonify({'message': 'File deleted successfully'}), 200 279 | 280 | except Exception as e: 281 | current_app.logger.error(f'Error deleting video: {e}') 282 | return jsonify({'error': 'Internal server error'}), 500 283 | 284 | 285 | 286 | def generate_file_chunks(file_path): 287 | with open(file_path, 'rb') as f: 288 | while chunk := f.read(8192): 289 | yield chunk 290 | 291 | 292 | @main.route('/download/') 293 | def download_video(filename): 294 | try: 295 | # Secure the filename to prevent directory traversal attacks 296 | filename = secure_filename(filename) 297 | 298 | # Get the directory where videos are stored 299 | video_dir = current_app.config.get('VIDEO_DIR') 300 | if not video_dir: 301 | return jsonify({'error': 'VIDEO_DIR not configured'}), 500 302 | 303 | # Serve the file for download 304 | return send_from_directory(video_dir, filename, as_attachment=True) 305 | except FileNotFoundError: 306 | return jsonify({'error': 'File not found'}), 404 307 | except Exception as e: 308 | current_app.logger.error(f"Error serving file for download: {e}") 309 | return jsonify({'error': 'Internal server error'}), 500 310 | 311 | 312 | @main.route('/play/') 313 | def play(filename): 314 | try: 315 | filename = secure_filename(filename) 316 | video_dir = current_app.config.get('VIDEO_DIR') 317 | file_path = os.path.join(video_dir, filename) 318 | current_app.logger.debug(f"Serving video file: {file_path}") 319 | return send_from_directory(video_dir, filename, mimetype='video/mp4') 320 | except FileNotFoundError: 321 | current_app.logger.error(f"Video file not found: {filename}") 322 | return abort(404, description="File not found") 323 | except Exception as e: 324 | current_app.logger.error(f"Error serving video file: {e}") 325 | return abort(500, description="Internal server error") 326 | 327 | 328 | # @main.route('/play/') 329 | # def play(filename): 330 | # try: 331 | # filename = secure_filename(filename) 332 | # video_dir = current_app.config.get('VIDEO_DIR') 333 | # if not video_dir: 334 | # return abort(500, description="VIDEO_DIR not configured") 335 | 336 | # file_path = os.path.join(video_dir, filename) 337 | # if not os.path.exists(file_path): 338 | # return abort(404, description="File not found") 339 | 340 | # return Response( 341 | # generate_file_chunks(file_path), 342 | # content_type="video/mp4", 343 | # ) 344 | # except Exception as e: 345 | # current_app.logger.error(f"Error serving video file: {e}") 346 | # return abort(500, description="Internal server error") 347 | 348 | @main.route('/video/') 349 | def show_video(filename): 350 | try: 351 | # Secure filename and resolve video path 352 | filename = secure_filename(filename) 353 | video_dir = current_app.config['VIDEO_DIR'] 354 | video_path = os.path.join(video_dir, filename) 355 | 356 | # Ensure the video file exists 357 | if not os.path.exists(video_path): 358 | return abort(404, description="File not found") 359 | 360 | # Get video stats 361 | video_stats = get_video_stats(video_path) 362 | 363 | # Pass stats to the template 364 | return render_template('play.html', filename=filename, video_stats=video_stats) 365 | except Exception as e: 366 | current_app.logger.error(f"Error displaying video: {e}") 367 | return abort(500, description="Internal server error") 368 | 369 | 370 | @main.route('/temperature') 371 | def get_temperature(): 372 | try: 373 | if platform.system() != 'Linux' or not os.path.exists('/sys/class/thermal'): 374 | return jsonify({ 375 | 'error': 'Temperature monitoring is only available on Linux systems.' 376 | }), 400 377 | 378 | soc_temp = int(open('/sys/class/thermal/thermal_zone0/temp').read().strip()) / 1000.0 379 | gpu_temp = int(open('/sys/class/thermal/thermal_zone1/temp').read().strip()) / 1000.0 380 | soc_temp_f = (soc_temp * 9/5) + 32 381 | gpu_temp_f = (gpu_temp * 9/5) + 32 382 | 383 | return { 384 | 'soc_temperature': f"{soc_temp:.1f}", 385 | 'soc_temperature_f': f"{soc_temp_f:.1f}", 386 | 'gpu_temperature': f"{gpu_temp:.1f}", 387 | 'gpu_temperature_f': f"{gpu_temp_f:.1f}" 388 | } 389 | 390 | except Exception as e: 391 | logger.error(f'Error getting temperature: {e}') 392 | return {'error': str(e)}, 500 393 | 394 | @main.route('/backup') 395 | def backup(): 396 | for item in config_files: 397 | file_path = item['path'] 398 | backup_path = file_path + '.bak' 399 | with open(file_path, 'r') as f: 400 | content = f.read() 401 | with open(backup_path, 'w') as f: 402 | f.write(content) 403 | logger.debug('Backup created for configuration files.') 404 | return redirect(url_for('main.home')) 405 | 406 | @main.route('/run_command', methods=['POST']) 407 | def run_command(): 408 | selected_command = request.form.get('command') 409 | 410 | cli_command = f"echo cli -s {selected_command} > /dev/udp/localhost/14550" 411 | logger.debug(f'Running command: {cli_command}') 412 | flash(f'Running command: {cli_command}', 'info') 413 | 414 | subprocess.run(cli_command, shell=True) 415 | subprocess.run("echo killall -1 majestic > /dev/udp/localhost/14550", shell=True) 416 | 417 | return redirect(url_for('main.home')) 418 | 419 | @main.route('/service_action', methods=['POST']) 420 | def service_action(): 421 | service_name = request.form.get('service_name') 422 | action = request.form.get('action') 423 | 424 | if service_name and action: 425 | try: 426 | # Determine if running inside Docker 427 | is_docker = os.path.exists('/.dockerenv') 428 | 429 | # Use 'sudo' if not running inside Docker 430 | if is_docker: 431 | command_prefix = [] 432 | else: 433 | command_prefix = ['sudo'] 434 | 435 | # Prepare the command based on the action 436 | if action == 'enable': 437 | command = command_prefix + ['systemctl', 'enable', service_name] 438 | subprocess.run(command, check=True) 439 | flash(f'Service {service_name} enabled successfully.', 'success') 440 | elif action == 'disable': 441 | command = command_prefix + ['systemctl', 'disable', service_name] 442 | subprocess.run(command, check=True) 443 | flash(f'Service {service_name} disabled successfully.', 'success') 444 | elif action == 'restart': 445 | command = command_prefix + ['systemctl', 'restart', service_name] 446 | subprocess.run(command, check=True) 447 | flash(f'Service {service_name} restarted successfully.', 'success') 448 | else: 449 | flash('Invalid action.', 'error') 450 | except subprocess.CalledProcessError as e: 451 | current_app.logger.error(f'Failed to {action} service {service_name}: {e}') 452 | flash(f'Failed to {action} service {service_name}: {e}', 'error') 453 | 454 | return redirect(url_for('main.home')) 455 | 456 | 457 | @main.route('/upload', methods=['GET', 'POST']) 458 | def upload_file(): 459 | if request.method == 'POST': 460 | if 'file' not in request.files: 461 | flash('No file part') 462 | return redirect(request.url) 463 | file = request.files['file'] 464 | if file.filename == '': 465 | flash('No selected file') 466 | return redirect(request.url) 467 | if file and allowed_file(file.filename): 468 | file_path = os.path.join(current_app.config['GS_UPLOAD_FOLDER'], 'gs.key') 469 | file.save(file_path) 470 | flash('File successfully uploaded') 471 | return redirect(url_for('main.home')) 472 | return render_template('upload.html') 473 | 474 | # Function to check allowed file extensions 475 | def allowed_file(filename): 476 | allowed_extensions = current_app.config.get('ALLOWED_EXTENSIONS', {'key', 'cfg', 'conf', 'yaml'}) 477 | return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions --------------------------------------------------------------------------------