Radio station track uploads.

This commit is contained in:
afk
2026-07-16 18:43:01 +02:00
parent f98f83d5df
commit 92781e4291
3 changed files with 120 additions and 12 deletions
+4
View File
@@ -6,6 +6,10 @@ ENV PYTHONUNBUFFERED=1
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ffmpeg yt-dlp && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./ COPY requirements.txt ./
RUN uv pip install --system --no-cache -r requirements.txt RUN uv pip install --system --no-cache -r requirements.txt
+6
View File
@@ -12,3 +12,9 @@ MATRIX_PASSWORD = ""
YOUTUBE_PLAYLIST_TITLE = "" YOUTUBE_PLAYLIST_TITLE = ""
# YouTube API client secret json path. # YouTube API client secret json path.
YOUTUBE_CLIENT_SECRETS_FILE = "" YOUTUBE_CLIENT_SECRETS_FILE = ""
# AzuraCast API key.
AZURACAST_API_KEY = ""
# AzuraCast API URL.
AZURACAST_URL = ""
# AzuraCast station ID.
AZURACAST_STATION_ID = ""
+101 -3
View File
@@ -4,7 +4,9 @@
import argparse import argparse
import asyncio import asyncio
import datetime import datetime
import glob
import html import html
import json
import os import os
import pickle import pickle
import re import re
@@ -30,6 +32,10 @@ MATRIX_PASSWORD = os.getenv("MATRIX_PASSWORD")
YOUTUBE_CLIENT_SECRETS_FILE = os.getenv("YOUTUBE_CLIENT_SECRETS_FILE") YOUTUBE_CLIENT_SECRETS_FILE = os.getenv("YOUTUBE_CLIENT_SECRETS_FILE")
YOUTUBE_PLAYLIST_TITLE = os.getenv("YOUTUBE_PLAYLIST_TITLE") YOUTUBE_PLAYLIST_TITLE = os.getenv("YOUTUBE_PLAYLIST_TITLE")
AZURACAST_API_KEY = os.getenv("AZURACAST_API_KEY")
AZURACAST_URL = os.getenv("AZURACAST_URL")
AZURACAST_STATION_ID = os.getenv("AZURACAST_STATION_ID")
def connect_db(): def connect_db():
"""Connect to DB and return connection and cursor.""" """Connect to DB and return connection and cursor."""
@@ -274,7 +280,8 @@ async def message_callback(conn, cursor, youtube, client, room, event):
) )
timestamp_sec = datetime.datetime.fromtimestamp( timestamp_sec = datetime.datetime.fromtimestamp(
event.server_timestamp / 1000, datetime.UTC # millisec to sec event.server_timestamp / 1000,
datetime.UTC, # millisec to sec
) )
current_time = datetime.datetime.now(datetime.UTC) current_time = datetime.datetime.now(datetime.UTC)
@@ -314,7 +321,9 @@ async def message_callback(conn, cursor, youtube, client, room, event):
# Escape HTML characters to prevent broken rendering in Matrix # Escape HTML characters to prevent broken rendering in Matrix
escaped_text = html.escape(plain_text) escaped_text = html.escape(plain_text)
html_text = f"<em><span data-mx-color='#808080'>{escaped_text}</span></em>" html_text = (
f"<em><span data-mx-color='#808080'>{escaped_text}</span></em>"
)
await client.room_send( await client.room_send(
room_id=room.room_id, room_id=room.room_id,
@@ -323,7 +332,7 @@ async def message_callback(conn, cursor, youtube, client, room, event):
"msgtype": "m.text", "msgtype": "m.text",
"body": plain_text, "body": plain_text,
"format": "org.matrix.custom.html", "format": "org.matrix.custom.html",
"formatted_body": html_text "formatted_body": html_text,
}, },
) )
@@ -345,6 +354,8 @@ async def message_callback(conn, cursor, youtube, client, room, event):
(playlist_id, message_id, video_id), (playlist_id, message_id, video_id),
) )
print(f"Added track to this week's playlist: {link}") print(f"Added track to this week's playlist: {link}")
if recent:
asyncio.create_task(process_radio_track(link, video_id, title))
def in_playlist(cursor, video_id, playlist_id): def in_playlist(cursor, video_id, playlist_id):
@@ -431,6 +442,93 @@ async def backwards_sync(conn, cursor, youtube, client, room, start_token):
from_token = response.end from_token = response.end
async def process_radio_track(video_link, video_id, title):
"""Downloads native audio and pushes it to AzuraCast API in the background."""
if not AZURACAST_API_KEY:
return
# Sanitize title for the local filesystem
safe_title = "".join(c for c in title if c.isalnum() or c in " -_").strip()
base_filename = f"{safe_title}_{video_id}"
base_filepath = os.path.join(DATA_DIR, base_filename)
print(f"📻 Downloading audio for radio: {title}")
# 1. Download with yt-dlp (Keeping native AAC/Opus format)
dl_cmd = [
"yt-dlp",
"--extract-audio",
"--output",
f"{base_filepath}.%(ext)s",
video_link,
]
process = await asyncio.create_subprocess_exec(
*dl_cmd, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL
)
await process.communicate()
# Check for the file using glob since the extension (.m4a, .webm, .opus) is dynamic
downloaded_files = glob.glob(f"{base_filepath}.*")
if not downloaded_files:
print(f"❌ Failed to download audio for {title}")
return
filepath = downloaded_files[0]
print(f"📻 Uploading to AzuraCast: {title}")
# 2. Upload to AzuraCast using curl
upload_cmd = [
"curl",
"-s",
"-X",
"POST",
f"{AZURACAST_URL}/api/station/{AZURACAST_STATION_ID}/files",
"-H",
f"X-API-Key: {AZURACAST_API_KEY}",
"-F",
f"file=@{filepath}",
]
curl_proc = await asyncio.create_subprocess_exec(
*upload_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
stdout, _ = await curl_proc.communicate()
# 3. Extract unique_id and push to the live queue
try:
response = json.loads(stdout.decode())
unique_id = response.get("unique_id")
if unique_id:
req_cmd = [
"curl",
"-s",
"-X",
"POST",
f"{AZURACAST_URL}/api/station/{AZURACAST_STATION_ID}/request/{unique_id}",
"-H",
f"X-API-Key: {AZURACAST_API_KEY}",
]
req_proc = await asyncio.create_subprocess_exec(
*req_cmd,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await req_proc.communicate()
print(f"✅ Queued on radio: {title}")
else:
print(f"⚠️ Uploaded, but no unique_id returned. Response: {stdout.decode()}")
except json.JSONDecodeError:
print(f"❌ Failed to parse AzuraCast response: {stdout.decode()}")
# Cleanup the local file so your container doesn't bloat
if os.path.exists(filepath):
os.remove(filepath)
async def main(): async def main():
"""Get DB and Matrix client ready, and start syncing.""" """Get DB and Matrix client ready, and start syncing."""
args = parse_arguments() args = parse_arguments()