From f0a84c1f41064c666330cb5429d02432b97ce305 Mon Sep 17 00:00:00 2001 From: sudacode Date: Wed, 4 Sep 2024 15:15:40 -0700 Subject: [PATCH] initial commit --- .gitea/workflows/build_docker.yml | 33 +++++ .gitignore | 3 + Dockerfile | 26 ++++ VERSION | 1 + docker-compose.yml | 18 +++ env.example | 12 ++ mpv-youtube-queue-server.service | 20 +++ requirements.txt | 8 ++ server.py | 198 ++++++++++++++++++++++++++++++ 9 files changed, 319 insertions(+) create mode 100644 .gitea/workflows/build_docker.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 VERSION create mode 100644 docker-compose.yml create mode 100644 env.example create mode 100644 mpv-youtube-queue-server.service create mode 100644 requirements.txt create mode 100755 server.py diff --git a/.gitea/workflows/build_docker.yml b/.gitea/workflows/build_docker.yml new file mode 100644 index 0000000..cbf2440 --- /dev/null +++ b/.gitea/workflows/build_docker.yml @@ -0,0 +1,33 @@ +name: Build Docker Image +on: + push: + branches: + - master +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Read current version + id: get_version + run: echo "current_version=$(cat VERSION)" >> $GITHUB_ENV + + - name: Log in to Gitea Docker Registry + run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login https://gitea.suda.codes -u ${{ secrets.DOCKER_USERNAME }} --password-stdin + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: | + gitea.suda.codes/sudacode/mpv-youtube-queue-server:${{ env.current_version }} + gitea.suda.codes/sudacode/mpv-youtube-queue-server:latest + + - name: Log out from Gitea Docker Registry + run: docker logout https://gitea.suda.codes diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d76b9fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +env/* +.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9f185c3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# Use an official Python runtime as a parent image +FROM python:3.10-slim + +# Set environment variables for the MPV socket and server host/port +ENV MPV_SOCKET="/tmp/mpvsocket" \ + HOST_NAME="0.0.0.0" \ + PORT_NUMBER=8080 \ + LOGLEVEL="info" + +# Set the working directory in the container +WORKDIR /app + +# Copy the current directory contents into the container at /app +COPY server.py /app/server.py + +# Install any needed packages specified in requirements.txt +# If there are no external dependencies, you can skip this step +# RUN pip install --no-cache-dir -r requirements.txt + +# Make port 8080 available to the world outside this container +EXPOSE "${PORT_NUMBER}" + +RUN pip3 install --no-cache-dir Flask mysql-connector-python + +# Run server.py when the container launches +CMD ["python3", "server.py", "--host", "${HOST_NAME}", "--port", "${PORT_NUMBER}", "--input-ipc-server", "${MPV_SOCKET}"] diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..8acdd82 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.1 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..65adb63 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +--- +services: + mpv-youtube-queue-server: + build: . + image: mpv-youtube-queue-server:latest + container_name: mpv-youtube-queue-server + user: 1000:1000 + volumes: + - /tmp:/tmp + ports: + - 42069:8080 + env_file: .env + networks: + - mpv-youtube-queue-server + restart: unless-stopped +networks: + mpv-youtube-queue-server: + external: true diff --git a/env.example b/env.example new file mode 100644 index 0000000..4bab203 --- /dev/null +++ b/env.example @@ -0,0 +1,12 @@ +IP=0.0.0.0 # Lisen on all interfaces +PORT_NUMBER=8080 # Internal port number +MPV_SOCKET=/tmp/mpvsocket # Path to mpv socket +LOGLEVEL=info + +# MySQL connection info +MYSQL_HOST=localhost +MYSQL_USER=mpvuser +MYSQL_PASSWORD=SecretPassword +MYSQL_DATABASE=mpv +MYSQL_PORT=3306 + diff --git a/mpv-youtube-queue-server.service b/mpv-youtube-queue-server.service new file mode 100644 index 0000000..99da8b9 --- /dev/null +++ b/mpv-youtube-queue-server.service @@ -0,0 +1,20 @@ +[Unit] +Description=Python Server for MPV +After=network.target + +[Service] +User= +WorkingDirectory= +ExecStart= +Restart=on-failure +Environment="MPV_SOCKET=/tmp/mpvsocket" +Environment="HOST_NAME=0.0.0.0" +Environment="PORT_NUMBER=42069" +Environment="MYSQL_HOST=http://localhost" +Environment="MYSQL_USER=mpvuser" +Environment="MYSQL_PASSWORD=SecretPassword" +Environment="MYSQL_PORT=3306" + +[Install] +WantedBy=multi-user.target + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9705fea --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +blinker==1.8.2 +click==8.1.7 +Flask==3.0.3 +itsdangerous==2.2.0 +Jinja2==3.1.4 +MarkupSafe==2.1.5 +mysql-connector-python==9.0.0 +Werkzeug==3.0.4 diff --git a/server.py b/server.py new file mode 100755 index 0000000..0b89889 --- /dev/null +++ b/server.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +import logging +import os +import socket +import time +import urllib.parse +from datetime import date + +import mysql.connector +from flask import Flask, jsonify, request +from mysql.connector import Error + +# Set up basic logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) + +SOCKET_RETRY_DELAY = 5 # Time in seconds between retries to connect to the socket +MAX_RETRIES = 10 # Maximum number of retries to connect to the socket + +# Configuration +MPV_SOCKET: str = os.getenv("MPV_SOCKET", "/tmp/mpvsocket") +HOST_NAME: str = os.getenv("HOST_NAME", "0.0.0.0") +PORT_NUMBER: int = int(os.getenv("PORT_NUMBER", "8080")) +# MySQL Configuration +MYSQL_HOST: str = os.getenv("MYSQL_HOST", "localhost") +MYSQL_DATABASE: str = os.getenv("MYSQL_DATABASE", "your_database") +MYSQL_USER: str = os.getenv("MYSQL_USER", "your_username") +MYSQL_PASSWORD: str = os.getenv("MYSQL_PASSWORD", "your_password") +MYSQL_PORT: int = int(os.getenv("MYSQL_PORT", "3306")) +LOGLEVEL = os.getenv("LOGLEVEL", "INFO").strip().upper() +if LOGLEVEL == "DEBUG": + logging.getLogger().setLevel(logging.DEBUG) +elif LOGLEVEL == "WARNING": + logging.getLogger().setLevel(logging.WARNING) +elif LOGLEVEL == "ERROR": + logging.getLogger().setLevel(logging.ERROR) +else: + logging.getLogger().setLevel(logging.INFO) + + +app = Flask(__name__) + + +def get_mysql_connection(): + """Get a MySQL database connection.""" + try: + logging.debug( + f"Connection information: {MYSQL_HOST}, {MYSQL_USER}, {MYSQL_PORT}" + ) + connection = mysql.connector.connect( + host=MYSQL_HOST, + user=MYSQL_USER, + password=MYSQL_PASSWORD, + port=MYSQL_PORT, + ) + if connection.is_connected(): + logging.info("Connected to MySQL database") + return connection + except Error as e: + logging.error(f"Error while connecting to MySQL: {e}") + return None + + +def ensure_watch_history_table_exists(): + """Ensure the watch_history table exists in the mpv schema, otherwise create it.""" + connection = get_mysql_connection() + if connection: + try: + cursor = connection.cursor() + cursor.execute("CREATE DATABASE IF NOT EXISTS mpv") + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS mpv.watch_history ( + whid INT AUTO_INCREMENT PRIMARY KEY, + video_url VARCHAR(255) NOT NULL, + video_name VARCHAR(255) NOT NULL, + channel_url VARCHAR(255) NOT NULL, + channel_name VARCHAR(255) NOT NULL, + watch_date DATE NOT NULL + ) + """ + ) + connection.commit() + logging.info("Ensured watch_history table exists") + except Error as e: + logging.error(f"Failed to ensure watch_history table exists: {e}") + finally: + cursor.close() + connection.close() + + +def send_to_mpv(command): + """Send a command to the mpv socket, retrying up to MAX_RETRIES times if the socket is not available.""" + attempts = 0 + while attempts < MAX_RETRIES: + try: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket: + client_socket.connect(MPV_SOCKET) + client_socket.sendall(command.encode("utf-8")) + logging.info("Command sent to mpv successfully.") + return True + except socket.error as e: + attempts += 1 + logging.error( + f"Failed to connect to socket (attempt {attempts}/{MAX_RETRIES}): {e}. Retrying in {SOCKET_RETRY_DELAY} seconds..." + ) + time.sleep(SOCKET_RETRY_DELAY) + + logging.error(f"Exceeded maximum retries ({MAX_RETRIES}). Ignoring the request.") + return False + + +@app.route("/add_video", methods=["POST"]) +def add_video(): + data = request.get_json() + + if data: + video_url: str = data.get("video_url") + video_name: str = data.get("video_name") + channel_url: str = data.get("channel_url") + channel_name: str = data.get("channel_name") + watch_date: date = data.get("watch_date") + + if video_url and video_name and channel_url and channel_name and watch_date: + logging.info(f"Received data: {data}") + + # Insert the data into the MySQL database + connection = get_mysql_connection() + if connection: + try: + query = """ + INSERT INTO mpv.watch_history (video_url, video_name, channel_url, channel_name, watch_date) + VALUES (%s, %s, %s, %s, %s) + """ + cursor = connection.cursor() + cursor.execute( + query, + ( + video_url, + video_name, + channel_url, + channel_name, + watch_date, + ), + ) + connection.commit() + logging.info("Data inserted into MySQL database") + return ( + jsonify(message="Data added to mpv queue and database"), + 200, + ) + except Error as e: + logging.error(f"Failed to insert data into MySQL database: {e}") + return jsonify(message="Failed to add data to database"), 500 + finally: + cursor.close() + connection.close() + else: + return jsonify(message="Failed to connect to MySQL database"), 500 + else: + logging.error("Missing required data fields") + return jsonify(message="Missing required data fields"), 400 + else: + logging.error("Invalid JSON data") + return jsonify(message="Invalid JSON data"), 400 + + +@app.route("/", methods=["GET"]) +def handle_request(): + video_url = request.args.get("url") + if video_url: + video_url = urllib.parse.unquote(video_url) # Decode the URL + logging.info(f"Received URL: {video_url}") + + # Create the command to send to mpv + command = f'{{"command": ["script-message", "add_to_youtube_queue", "{video_url}"]}}\n' + + # Try to send the command to mpv + if send_to_mpv(command): + return "URL added to mpv queue", 200 + else: + return "Failed to add URL to mpv queue after max retries", 500 + else: + logging.error("Missing 'url' parameter") + return "Missing 'url' parameter", 400 + + +if __name__ == "__main__": + logging.info(f"Starting server on {HOST_NAME}:{PORT_NUMBER}...") + ensure_watch_history_table_exists() + try: + app.run(host=HOST_NAME, port=PORT_NUMBER) + except Exception as e: + logging.exception(f"Error occurred: {e}") + except KeyboardInterrupt: + logging.info("Server is shutting down...") + logging.info("Server stopped.")