diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b244df --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ffd9f1 --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fe2ad6c --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/docker/midtownplaydio/Dockerfile b/docker/midtownplaydio/Dockerfile new file mode 100644 index 0000000..f156119 --- /dev/null +++ b/docker/midtownplaydio/Dockerfile @@ -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 \ No newline at end of file diff --git a/docker/midtownplaydio/Dockerfile.production b/docker/midtownplaydio/Dockerfile.production new file mode 100644 index 0000000..f482914 --- /dev/null +++ b/docker/midtownplaydio/Dockerfile.production @@ -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 \ No newline at end of file diff --git a/docker/midtownplaydio/src/app.py b/docker/midtownplaydio/src/app.py new file mode 100644 index 0000000..5630064 --- /dev/null +++ b/docker/midtownplaydio/src/app.py @@ -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(''' + + + + MidTownPlaydio + + + + + + + +
+

MidTownPlaydio

+ +
+ + + + ''') + +# 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) \ No newline at end of file diff --git a/docker/midtownplaydio/src/requirements.txt b/docker/midtownplaydio/src/requirements.txt new file mode 100644 index 0000000..989fb05 --- /dev/null +++ b/docker/midtownplaydio/src/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.104.0 +uvicorn==0.23.2 +gunicorn==21.2.0 +uvloop==0.18.0 +httptools==0.6.0 \ No newline at end of file