Add initial PWA application structure
ci/woodpecker/push/woodpecker Pipeline was successful Details

This commit is contained in:
Your Name 2025-03-12 19:22:40 -04:00
parent 05eb81aa64
commit 4a0797fcff
7 changed files with 337 additions and 0 deletions

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
ENV/
.env
# IDE specific files
.idea/
.vscode/
*.swp
*.swo
# Docker
.dockerignore
# Application specific
docker/midtownplaydio/src/static/
# OS specific
.DS_Store
Thumbs.db

56
README.md Normal file
View File

@ -0,0 +1,56 @@
# MidTownPlaydio
A progressive web app (PWA) to stream Midtown Radio KW in a mobile-friendly interface.
## Features
- Responsive web interface
- Progressive Web App (PWA) capabilities
- Offline caching
- Mobile-friendly design
## Development
### Prerequisites
- Docker and Docker Compose
### Running Locally
```bash
# Clone the repository
git clone https://git.nixc.us/colin/midtownplaydio.git
cd midtownplaydio
# Start the development server
docker-compose up
```
The app will be available at http://localhost:3000
### Directory Structure
```
├── docker/
│ └── midtownplaydio/
│ ├── src/ # Source code
│ ├── Dockerfile # Development/staging Dockerfile
│ └── Dockerfile.production # Production Dockerfile
├── docker-compose.yml # Local development setup
├── docker-compose.staging.yml # Staging deployment
├── docker-compose.production.yml # Production deployment
├── stack.staging.yml # Docker Swarm stack for staging
├── stack.production.yml # Docker Swarm stack for production
└── .woodpecker.yml # CI/CD configuration
```
## Deployment
The application is set up for automatic deployment through Woodpecker CI.
- Push to `main` branch deploys to staging environment
- Cron job or manual promotion deploys to production
## License
Copyright (c) 2023

14
docker-compose.yml Normal file
View File

@ -0,0 +1,14 @@
services:
fancy-qr:
build:
context: ./docker/fancy-qr
dockerfile: Dockerfile
ports:
- "3000:3000"
volumes:
- ./docker/fancy-qr/src:/app/web
- ./uploads:/app/web/uploads
- ./output:/app/web/outputs
environment:
- NODE_ENV=development
restart: unless-stopped

View File

@ -0,0 +1,31 @@
FROM python:3.11-slim
WORKDIR /app
# Copy requirements first for better caching
COPY src/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Install development tools
RUN pip install --no-cache-dir pytest pytest-cov black isort
# Copy the rest of the application
COPY src/ .
# Create directory for static files
RUN mkdir -p /app/static
# Create non-root user for security
RUN adduser --disabled-password --gecos '' appuser
RUN chown -R appuser:appuser /app
USER appuser
# Expose the port the app runs on
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Start with uvicorn for hot reloading
CMD uvicorn app:app --host 0.0.0.0 --port 3000 --reload

View File

@ -0,0 +1,28 @@
FROM python:3.11-slim
WORKDIR /app
# Copy requirements first for better caching
COPY src/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application
COPY src/ .
# Create directory for static files
RUN mkdir -p /app/static
# Create non-root user for security
RUN adduser --disabled-password --gecos '' appuser
RUN chown -R appuser:appuser /app
USER appuser
# Expose the port the app runs on
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Run the application with Gunicorn
CMD gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:3000 app:app

View File

@ -0,0 +1,161 @@
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
import os
import json
app = FastAPI(title="MidTownPlaydio")
# Directory to store static files
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
os.makedirs(STATIC_DIR, exist_ok=True)
# Generate index.html
def generate_index_html():
with open(os.path.join(STATIC_DIR, 'index.html'), 'w') as f:
f.write('''
<!DOCTYPE html>
<html>
<head>
<title>MidTownPlaydio</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#ffffff">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-color: #f0f0f0;
}
.container {
text-align: center;
padding: 20px;
}
h1 {
color: #333;
}
</style>
</head>
<body>
<div class="container">
<h1>MidTownPlaydio</h1>
<iframe src="https://midtownradiokw.airtime.pro/player" width="300" height="200" allow="autoplay"></iframe>
</div>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
console.log('ServiceWorker registration failed: ', err);
});
});
}
</script>
</body>
</html>
''')
# Generate manifest.json
def generate_manifest_json():
manifest = {
"name": "MidTownPlaydio",
"short_name": "MidTownPlaydio",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"icons": [
{
"src": "/icon.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
with open(os.path.join(STATIC_DIR, 'manifest.json'), 'w') as f:
json.dump(manifest, f, indent=4)
# Generate service-worker.js
def generate_service_worker():
with open(os.path.join(STATIC_DIR, 'service-worker.js'), 'w') as f:
f.write('''
const CACHE_NAME = 'midtownplaydio-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/manifest.json',
'/icon.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
})
);
});
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
''')
# Create a simple placeholder icon
def create_placeholder_icon():
# This is just a placeholder - in a real app, you would use a proper icon file
with open(os.path.join(STATIC_DIR, 'icon.png'), 'w') as f:
f.write("Placeholder for icon")
# Generate all static files on startup
@app.on_event("startup")
async def startup_event():
generate_index_html()
generate_manifest_json()
generate_service_worker()
create_placeholder_icon()
# Mount static files
app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static")
# Health check endpoint
@app.get("/health", status_code=200)
async def health_check():
return {"status": "ok"}
# If the file is executed directly, run a development server
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3000)

View File

@ -0,0 +1,5 @@
fastapi==0.104.0
uvicorn==0.23.2
gunicorn==21.2.0
uvloop==0.18.0
httptools==0.6.0