Compare commits

..

No commits in common. "06040a1a0520718051580992d789fdb6dfc78fe2" and "87c8c5c786cb96650f7d953fffc2c44a72833a58" have entirely different histories.

9 changed files with 53 additions and 310 deletions

View File

@ -1,33 +0,0 @@
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

2
.gitignore vendored
View File

@ -1,3 +1 @@
.env .env
env/*
.git

View File

@ -1,25 +0,0 @@
# 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 LISTEN_ADDRESS="0.0.0.0" \
LISTEN_PORT=8080
# Set the working directory in the container
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY server.py requirements.txt /app/
# 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 -r requirements.txt
# Run server.py when the container launches
# CMD ["python3", "server.py", "--host", "${LISTEN_ADDRESS}", "--port", "${LISTEN_PORT}", "--input-ipc-server", "${MPV_SOCKET}"]
CMD gunicorn --bind "${LISTEN_ADDRESS}":"${LISTEN_PORT}" server:app

View File

@ -1 +0,0 @@
0.0.2

View File

@ -1,18 +0,0 @@
---
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

View File

@ -1,13 +1,3 @@
# Server config IP=0.0.0.0 # Listen on all interfaces
LISTEN_ADDRESS=0.0.0.0 # Lisen on all interfaces PORT_NUMBER=8080 # You can change this port if needed
LISTEN_PORT=8080 # Internal port number MPV_SOCKET=/mpvsocket
MPV_SOCKET=/tmp/mpvsocket # Path to mpv socket
# MySQL connection config
MYSQL_HOST=localhost
MYSQL_USER=mpvuser
MYSQL_PASSWORD=SecretPassword
MYSQL_DATABASE=mpv
# Logging
LOGLEVEL=warning

View File

@ -10,10 +10,6 @@ Restart=on-failure
Environment="MPV_SOCKET=/tmp/mpvsocket" Environment="MPV_SOCKET=/tmp/mpvsocket"
Environment="HOST_NAME=0.0.0.0" Environment="HOST_NAME=0.0.0.0"
Environment="PORT_NUMBER=42069" Environment="PORT_NUMBER=42069"
Environment="MYSQL_HOST=http://localhost"
Environment="MYSQL_USER=mpvuser"
Environment="MYSQL_PASSWORD=SecretPassword"
Environment="MYSQL_PORT=3306"
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -1,10 +0,0 @@
blinker==1.8.2
click==8.1.7
Flask==3.0.3
gunicorn==23.0.0
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==2.1.5
mysql-connector-python==9.0.0
packaging==24.1
Werkzeug==3.0.4

244
server.py
View File

@ -1,120 +1,25 @@
#!/usr/bin/env python3
import logging import logging
import os import os
import socket import socket
import time import time
import urllib.parse import urllib.parse
from datetime import date from http.server import BaseHTTPRequestHandler, HTTPServer
import mysql.connector
from flask import Flask, jsonify, request
from mysql.connector import Error
MAX_RETRIES = 5
SOCKET_RETRY_DELAY = 1.5
# Configuration # Configuration
MPV_SOCKET: str = os.getenv("MPV_SOCKET", "/tmp/mpvsocket") MPV_SOCKET = os.getenv("MPV_SOCKET", "/tmp/mpvsocket")
HOST_NAME: str = os.getenv("HOST_NAME", "0.0.0.0") HOST_NAME = os.getenv("HOST_NAME", "0.0.0.0")
PORT_NUMBER: int = int(os.getenv("PORT_NUMBER", "8080")) PORT_NUMBER = int(os.getenv("PORT_NUMBER", "8080"))
# MySQL Configuration SOCKET_RETRY_DELAY = 5 # Time in seconds between retries to connect to the socket
MYSQL_HOST: str = os.getenv("MYSQL_HOST", "localhost") MAX_RETRIES = 10 # Maximum number of retries to connect to the socket
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()
# Initialize Flask # Set up basic logging
app = Flask(__name__) logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
# Set up logging def send_to_mpv(command):
def setup_logging():
"""Sets up logging for both the app and flask."""
if not app.logger.hasHandlers(): # Check if there are already handlers
# Create a formatter
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Create a console handler
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
# Configure the root logger
app.logger.addHandler(console_handler)
# Set the log level based on LOGLEVEL
if LOGLEVEL == "DEBUG":
app.logger.setLevel(logging.DEBUG)
elif LOGLEVEL == "WARNING":
app.logger.setLevel(logging.WARNING)
elif LOGLEVEL == "ERROR":
app.logger.setLevel(logging.ERROR)
else:
app.logger.setLevel(logging.INFO)
# Silence noisy logs from certain libraries if necessary
logging.getLogger("logger").setLevel(logging.INFO)
# Set up logging
setup_logging()
def get_mysql_connection():
"""
Get a MySQL database connection.
--------
Returns
--------
connection: mysql.connector.connection.MySQLConnection
The MySQL connection object if successful, otherwise None.
"""
try:
connection = mysql.connector.connect(
host=MYSQL_HOST,
user=MYSQL_USER,
password=MYSQL_PASSWORD,
port=MYSQL_PORT,
)
if connection.is_connected():
app.logger.debug("Connected to database successfully.")
return connection
except Error as e:
app.logger.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()
app.logger.info("Ensured watch_history table exists")
except Error as e:
app.logger.error(f"Failed to ensure watch_history table exists: {e}")
finally:
cursor.close()
connection.close()
def send_to_mpv(command: str):
"""Send a command to the mpv socket, retrying up to MAX_RETRIES times if the socket is not available.""" """Send a command to the mpv socket, retrying up to MAX_RETRIES times if the socket is not available."""
attempts = 0 attempts = 0
while attempts < MAX_RETRIES: while attempts < MAX_RETRIES:
@ -122,122 +27,63 @@ def send_to_mpv(command: str):
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket:
client_socket.connect(MPV_SOCKET) client_socket.connect(MPV_SOCKET)
client_socket.sendall(command.encode("utf-8")) client_socket.sendall(command.encode("utf-8"))
app.logger.info("Command sent to mpv successfully.") logging.info("Command sent to mpv successfully.")
return True return True
except socket.error as e: except socket.error as e:
attempts += 1 attempts += 1
app.logger.error( logging.error(
f"Failed to connect to socket (attempt {attempts}/{MAX_RETRIES}): {e}. Retrying in {SOCKET_RETRY_DELAY} seconds..." f"Failed to connect to socket (attempt {attempts}/{MAX_RETRIES}): {e}. Retrying in {SOCKET_RETRY_DELAY} seconds..."
) )
time.sleep(SOCKET_RETRY_DELAY) time.sleep(SOCKET_RETRY_DELAY)
app.logger.error(f"Exceeded maximum retries ({MAX_RETRIES}). Ignoring the request.") logging.error(f"Exceeded maximum retries ({MAX_RETRIES}). Ignoring the request.")
return False return False
@app.route("/add_video", methods=["POST"]) class MyHandler(BaseHTTPRequestHandler):
def add_video(): def do_GET(self):
""" # Parse the URL and extract the "url" parameter
Adds a video to the mpv queue and the MySQL database. query_components = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
video_url = query_components.get("url", [None])[0]
--------
Parameters
--------
data: dict
The JSON data containing the video information.
Required fields:
- video_url: str
- video_name: str
- channel_url: str
- channel_name: str
e.g. {"video_url": "https://www.youtube.com/watch?v=video_id", "video_name": "video_name", "channel_url": "https://www.youtube.com/channel/channel_id", "channel_name": "channel_name"}
"""
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 = date.today().strftime("%Y-%m-%d")
if video_url and video_name and channel_url and channel_name and watch_date:
app.logger.debug(f"Received data: {data}")
app.logger.debug(f"Watch date: {watch_date}")
# 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()
app.logger.info("Data inserted into MySQL database")
return (
jsonify(message="Data added to mpv queue and database"),
200,
)
except Error as e:
app.logger.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:
app.logger.error("Missing required data fields")
return jsonify(message="Missing required data fields"), 400
else:
app.logger.error("Invalid JSON data")
return jsonify(message="Invalid JSON data"), 400
@app.route("/", methods=["GET"])
def handle_request():
"""
Handle GET requests to the root URL. This function is used to add a video to the mpv queue.
"""
video_url = request.args.get("url")
if video_url: if video_url:
video_url = urllib.parse.unquote(video_url) # Decode the URL video_url = urllib.parse.unquote(video_url) # Decode the URL
app.logger.info(f"Received URL: {video_url}") logging.info(f"Received URL: {video_url}")
# Create the command to send to mpv # Create the command to send to mpv
command = f'{{"command": ["script-message", "add_to_youtube_queue", "{video_url}"]}}\n' command = f'{{"command": ["script-message", "add_to_youtube_queue", "{video_url}"]}}\n'
# Try to send the command to mpv # Try to send the command to mpv
if send_to_mpv(command): if send_to_mpv(command):
return "URL added to mpv queue", 200 self.send_response(200)
self.end_headers()
self.wfile.write(b"URL added to mpv queue")
else: else:
return "Failed to add URL to mpv queue after max retries", 500 self.send_response(500)
self.end_headers()
self.wfile.write(b"Failed to add URL to mpv queue after max retries")
else: else:
app.logger.error("Missing 'url' parameter") logging.error("Missing 'url' parameter")
return "Missing 'url' parameter", 400 self.send_response(400)
self.end_headers()
self.wfile.write(b"Missing 'url' parameter")
def log_message(self, format, *args):
# Override default log_message method to avoid duplicate logging from BaseHTTPRequestHandler
logging.info(f"{self.address_string()} - {format % args}")
if __name__ == "__main__": if __name__ == "__main__":
app.logger.info(f"Starting server on {HOST_NAME}:{PORT_NUMBER}...") logging.info(f"Starting server on {HOST_NAME}:{PORT_NUMBER}...")
ensure_watch_history_table_exists()
try: try:
app.run(host=HOST_NAME, port=PORT_NUMBER) httpd = HTTPServer((HOST_NAME, PORT_NUMBER), MyHandler)
logging.info(f"Server running on port {PORT_NUMBER}...")
httpd.serve_forever()
except Exception as e: except Exception as e:
app.logger.exception(f"Error occurred: {e}") logging.exception(f"Error occurred: {e}")
except KeyboardInterrupt: except KeyboardInterrupt:
app.logger.info("Server is shutting down...") logging.info("Server is shutting down...")
app.logger.info("Server stopped.") httpd.server_close()
logging.info("Server stopped.")