From 5475698fc566d45e184630055725c118335c41e6 Mon Sep 17 00:00:00 2001 From: Jo Eskil Date: Thu, 13 Nov 2025 17:27:18 +0100 Subject: [PATCH] Transition to Docker-based framework --- Dockerfile | 17 ++++++ __pycache__/auth.cpython-313.pyc | Bin 0 -> 3824 bytes __pycache__/chat_listeners.cpython-313.pyc | Bin 0 -> 3706 bytes __pycache__/database.cpython-313.pyc | Bin 0 -> 1906 bytes __pycache__/main.cpython-313.pyc | Bin 0 -> 5615 bytes auth.py | 13 ++--- chat_listeners.py | 65 +++++++++++++++++++++ database.py | 8 +-- docker-compose.yml | 29 +++++++++ index.html | 21 +++++++ main.py | 35 +++-------- multichat.db | Bin 0 -> 20480 bytes requirements.txt | 37 ++++++++++++ uvicorn.log | 0 14 files changed, 185 insertions(+), 40 deletions(-) create mode 100644 Dockerfile create mode 100644 __pycache__/auth.cpython-313.pyc create mode 100644 __pycache__/chat_listeners.cpython-313.pyc create mode 100644 __pycache__/database.cpython-313.pyc create mode 100644 __pycache__/main.cpython-313.pyc create mode 100644 chat_listeners.py create mode 100644 docker-compose.yml create mode 100644 index.html create mode 100644 multichat.db create mode 100644 requirements.txt create mode 100644 uvicorn.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..696187f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Use an official Python runtime as a parent image +FROM python:3.9-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the requirements file into the container +COPY requirements.txt . + +# Install any needed packages specified in requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code into the container +COPY . . + +# Command to run the application +CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "4", "-b", "0.0.0.0:8000", "main:app"] diff --git a/__pycache__/auth.cpython-313.pyc b/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6878527c82be647524445aa8bad25f09c4bab1ad GIT binary patch literal 3824 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 tn~}*zHVbU`7S;2`nOP0fc!_>i>zGmZbmy literal 0 HcmV?d00001 diff --git a/__pycache__/chat_listeners.cpython-313.pyc b/__pycache__/chat_listeners.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddc0905c1358eddf9f795d21ff00f96cea5e7619 GIT binary patch literal 3706 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 VTQ@hP15LU6{sme);bZ^+ literal 0 HcmV?d00001 diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c6c0e5832159e35fd7c6db1225383081f2331df GIT binary patch literal 5615 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 + + + MultiChat Overlay + + +

Chat Messages

+
    + + + diff --git a/main.py b/main.py index 6303ab7..8c73251 100644 --- a/main.py +++ b/main.py @@ -8,10 +8,16 @@ from sqlalchemy.orm import Session from chat_listeners import listen_youtube_chat, listen_twitch_chat from auth import router as auth_router, serializer -from database import get_db, User +from database import get_db, User, create_tables app = FastAPI() +@app.on_event("startup") +async def startup_event(): + create_tables() + # The chat listeners will be started dynamically based on user activity + # and not on application startup. + class SessionMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): response = await call_next(request) @@ -28,33 +34,6 @@ class SessionMiddleware(BaseHTTPMiddleware): request.state.user = None return response -app.add_middleware(SessionMiddleware) - -def get_current_user(request: Request): - return request.state.user - -app.include_router(auth_router, prefix="/auth") # Include the auth router - -connected_clients = [] - -async def broadcast_message(message: dict): - # Convert the message dictionary to a JSON string before sending - message_json = json.dumps(message) - for client in connected_clients: - try: - await client.send_text(message_json) - except RuntimeError: - # Handle cases where client might have disconnected - connected_clients.remove(client) - -@app.on_event("startup") -async def startup_event(): - # Start chat listeners in the background - # Replace with actual video ID and Twitch token/channel - # For now, using placeholders. These will need to be configured. - asyncio.create_task(listen_youtube_chat("YOUR_YOUTUBE_VIDEO_ID", broadcast_message)) - asyncio.create_task(listen_twitch_chat("YOUR_TWITCH_OAUTH_TOKEN", "YOUR_TWITCH_CHANNEL", broadcast_message)) - @app.get("/") async def read_root(): return {"Hello": "World"} diff --git a/multichat.db b/multichat.db new file mode 100644 index 0000000000000000000000000000000000000000..1479c2b03dfa78462e8645c10aa0ea1615922128 GIT binary patch literal 20480 zcmeI#O-lkn7{Kw_ToZ+)+n$1fE(#HZmyT_=lw@me8hMgcCt4~cUCFu?eXOp1uV%&d zrA+85|AE<;nb~LN`ORMDs#*?0C2xEEM?aJWu`LWk97!pJFm+Ap8kgujs&O&mhZT2I z?7u#3=>g`}r^tR~{p^l@pdf$%0tg_000IagfB*sr{7ax|rmO?YcuNG`jv57{=5V0; zgJ$>~gzdX#&>6obi=JcG99efu)w&}~?um0Fe~*pC;JMhd#{B!Ck+Lky_^5<_>p_k4 z7+*FfVQcnr*@^SZr1ct0Zp}G$Jn2?y(yf=vdoq)rCN8gSuXtvAbB$5BuK%c(bouSJ z8ff<3z3MLM?yK9r8r=QvpCvH=RouF4!np6R8RPyxr@Zt^7q-`s=T1ZB^yXyp(b-%d z`Y8w?fB*srAb