Compare commits

...

10 Commits

Author SHA1 Message Date
ksyasuda
06040a1a05
update logging
update logging to integrate with flask
2024-09-05 04:20:21 -07:00
ksyasuda
ae511bdb93
update variable names 2024-09-05 04:19:13 -07:00
ksyasuda
dc696b51b3
change to gunicorn to serve app 2024-09-05 04:18:58 -07:00
ksyasuda
6b19d96658
update main to master in workflow 2024-09-04 12:07:09 -07:00
ksyasuda
52491358fd
bump version 2024-09-04 12:05:57 -07:00
ksyasuda
cb0334a3e3
update main branch name 2024-09-04 12:03:45 -07:00
ksyasuda
a49bcca3f7
add docker build workflow and version file 2024-09-04 12:03:10 -07:00
bebeedc0ae history-db (#1)
- update to Flask
- add history db for watch history tracking
- update service file

Co-authored-by: ksyasuda <ksyasuda@umich.edu>
Reviewed-on: #1
2024-09-04 11:54:05 -07:00
ksyasuda
3fcd7b1706
update example env file 2024-08-23 01:38:30 -07:00
dbfc19f878 add docker and compose file 2024-08-22 22:47:48 -07:00
9 changed files with 310 additions and 53 deletions

View File

@ -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

2
.gitignore vendored
View File

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

25
Dockerfile Normal file
View File

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

1
VERSION Normal file
View File

@ -0,0 +1 @@
0.0.2

18
docker-compose.yml Normal file
View File

@ -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

View File

@ -1,3 +1,13 @@
IP=0.0.0.0 # Listen on all interfaces
PORT_NUMBER=8080 # You can change this port if needed
MPV_SOCKET=/mpvsocket
# 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

View File

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

10
requirements.txt Normal file
View File

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

254
server.py
View File

@ -1,25 +1,120 @@
#!/usr/bin/env python3
import logging
import os
import socket
import time
import urllib.parse
from http.server import BaseHTTPRequestHandler, HTTPServer
from datetime import date
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 = 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
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()
# Set up basic logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
# Initialize Flask
app = Flask(__name__)
def send_to_mpv(command):
# 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"
)
# 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."""
attempts = 0
while attempts < MAX_RETRIES:
@ -27,63 +122,122 @@ def send_to_mpv(command):
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.")
app.logger.info("Command sent to mpv successfully.")
return True
except socket.error as e:
attempts += 1
logging.error(
app.logger.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.")
app.logger.error(f"Exceeded maximum retries ({MAX_RETRIES}). Ignoring the request.")
return False
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]
@app.route("/add_video", methods=["POST"])
def add_video():
"""
Adds a video to the mpv queue and the MySQL database.
if video_url:
video_url = urllib.parse.unquote(video_url) # Decode the URL
logging.info(f"Received URL: {video_url}")
--------
Parameters
--------
data: dict
The JSON data containing the video information.
# Create the command to send to mpv
command = f'{{"command": ["script-message", "add_to_youtube_queue", "{video_url}"]}}\n'
Required fields:
- video_url: str
- video_name: str
- channel_url: str
- channel_name: str
# Try to send the command to mpv
if send_to_mpv(command):
self.send_response(200)
self.end_headers()
self.wfile.write(b"URL added to mpv queue")
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:
self.send_response(500)
self.end_headers()
self.wfile.write(b"Failed to add URL to mpv queue after max retries")
return jsonify(message="Failed to connect to MySQL database"), 500
else:
logging.error("Missing 'url' parameter")
self.send_response(400)
self.end_headers()
self.wfile.write(b"Missing 'url' parameter")
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
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}")
@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}")
# 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:
app.logger.error("Missing 'url' parameter")
return "Missing 'url' parameter", 400
if __name__ == "__main__":
logging.info(f"Starting server on {HOST_NAME}:{PORT_NUMBER}...")
app.logger.info(f"Starting server on {HOST_NAME}:{PORT_NUMBER}...")
ensure_watch_history_table_exists()
try:
httpd = HTTPServer((HOST_NAME, PORT_NUMBER), MyHandler)
logging.info(f"Server running on port {PORT_NUMBER}...")
httpd.serve_forever()
app.run(host=HOST_NAME, port=PORT_NUMBER)
except Exception as e:
logging.exception(f"Error occurred: {e}")
app.logger.exception(f"Error occurred: {e}")
except KeyboardInterrupt:
logging.info("Server is shutting down...")
httpd.server_close()
logging.info("Server stopped.")
app.logger.info("Server is shutting down...")
app.logger.info("Server stopped.")