├── 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 |
11 | {% with messages = get_flashed_messages() %}
12 | {% if messages %}
13 |
14 | {% for message in messages %}
15 | - {{ message }}
16 | {% endfor %}
17 |
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 |
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 |
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 |
12 | {% for category, message in messages %}
13 | - {{ message }}
14 | {% endfor %}
15 |
16 | {% endif %}
17 | {% endwith %}
18 |
19 | {% if video_files %}
20 |
21 |
22 |
23 | | Video File |
24 | Size (bytes) |
25 | Created Date |
26 | Actions |
27 |
28 |
29 |
30 | {% for video in video_files %}
31 |
32 | | {{ video['name'] }} |
33 |
34 | {{ video['size'] }} |
35 | {{ video['created_date'] }} |
36 |
37 |
38 | |
39 |
40 | {% endfor %}
41 |
42 |
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 | 
15 |
16 | Editor
17 | 
18 |
19 | Video file selector
20 | 
21 |
22 | Player
23 | 
24 |
25 | Journalctl -f
26 | 
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 |
32 |
33 |
34 |
35 |
36 |
37 |
Radxa Temps
38 |
39 |
40 |
41 |
42 | | Component |
43 | Temperature (°C) |
44 | Temperature (°F) |
45 |
46 |
47 |
48 |
49 | | SOC Temperature |
50 |
51 | N/A
52 | |
53 |
54 | N/A
55 | |
56 |
57 |
58 | | GPU Temperature |
59 |
60 | N/A
61 | |
62 |
63 | N/A
64 | |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
System Control
76 |
77 |
78 |
79 |
80 |
81 | | Service |
82 | Status |
83 | Actions |
84 |
85 |
86 |
87 | {% for service, status in services.items() %}
88 |
89 | | {{ service }} |
90 |
91 |
92 | {{ status.capitalize() }}
93 |
94 | |
95 |
96 |
97 |
105 |
106 |
107 |
111 | |
112 |
113 | {% endfor %}
114 |
115 |
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
--------------------------------------------------------------------------------