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/*
.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
LISTEN_ADDRESS=0.0.0.0 # Lisen on all interfaces
LISTEN_PORT=8080 # Internal port number
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
IP=0.0.0.0 # Listen on all interfaces
PORT_NUMBER=8080 # You can change this port if needed
MPV_SOCKET=/mpvsocket

View File

@ -10,10 +10,6 @@ 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

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

242
server.py
View File

@ -1,120 +1,25 @@
#!/usr/bin/env python3
import logging
import os
import socket
import time
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
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()
MPV_SOCKET = os.getenv("MPV_SOCKET", "/tmp/mpvsocket")
HOST_NAME = os.getenv("HOST_NAME", "0.0.0.0")
PORT_NUMBER = int(os.getenv("PORT_NUMBER", "8080"))
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
# Initialize Flask
app = Flask(__name__)
# Set up logging
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"
# Set up basic logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)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):
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:
@ -122,122 +27,63 @@ def send_to_mpv(command: str):
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket:
client_socket.connect(MPV_SOCKET)
client_socket.sendall(command.encode("utf-8"))
app.logger.info("Command sent to mpv successfully.")
logging.info("Command sent to mpv successfully.")
return True
except socket.error as e:
attempts += 1
app.logger.error(
logging.error(
f"Failed to connect to socket (attempt {attempts}/{MAX_RETRIES}): {e}. Retrying in {SOCKET_RETRY_DELAY} seconds..."
)
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
@app.route("/add_video", methods=["POST"])
def add_video():
"""
Adds a video to the mpv queue and the MySQL database.
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
# Parse the URL and extract the "url" parameter
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:
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
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
self.send_response(200)
self.end_headers()
self.wfile.write(b"URL added to mpv queue")
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:
app.logger.error("Missing 'url' parameter")
return "Missing 'url' parameter", 400
logging.error("Missing 'url' parameter")
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__":
app.logger.info(f"Starting server on {HOST_NAME}:{PORT_NUMBER}...")
ensure_watch_history_table_exists()
logging.info(f"Starting server on {HOST_NAME}:{PORT_NUMBER}...")
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:
app.logger.exception(f"Error occurred: {e}")
logging.exception(f"Error occurred: {e}")
except KeyboardInterrupt:
app.logger.info("Server is shutting down...")
app.logger.info("Server stopped.")
logging.info("Server is shutting down...")
httpd.server_close()
logging.info("Server stopped.")