mirror of
https://github.com/sanderfoobar/ircradio.git
synced 2024-11-22 03:49:27 +01:00
a4930649fc
and printing song results.
518 lines
17 KiB
Python
518 lines
17 KiB
Python
# SPDX-License-Identifier: BSD-3-Clause
|
|
# Copyright (c) 2021, dsc@xmr.pm
|
|
import sys
|
|
from typing import List, Optional
|
|
import os
|
|
import time
|
|
import asyncio
|
|
import random
|
|
|
|
from ircradio.factory import irc_bot as bot
|
|
from ircradio.station import Station
|
|
from ircradio.youtube import YouTube
|
|
import settings
|
|
from settings import radio_stations
|
|
|
|
radio_default = radio_stations["wow"]
|
|
msg_queue = asyncio.Queue()
|
|
YT_DLP_LOCK = asyncio.Lock()
|
|
DU_LOCK = asyncio.Lock()
|
|
|
|
|
|
async def message_worker():
|
|
from ircradio.factory import app
|
|
|
|
while True:
|
|
try:
|
|
data: dict = await msg_queue.get()
|
|
target = data['target']
|
|
msg = data['message']
|
|
bot.send("PRIVMSG", target=target, message=msg)
|
|
except Exception as ex:
|
|
app.logger.error(f"message_worker(): {ex}")
|
|
await asyncio.sleep(0.3)
|
|
|
|
|
|
@bot.on('CLIENT_CONNECT')
|
|
async def connect(**kwargs):
|
|
bot.send('NICK', nick=settings.irc_nick)
|
|
bot.send('USER', user=settings.irc_nick, realname=settings.irc_realname)
|
|
|
|
# @TODO: get rid of this nonsense after a while
|
|
args = {"return_when": asyncio.FIRST_COMPLETED}
|
|
if sys.version_info.major == 3 and sys.version_info.minor < 10:
|
|
args["loop"] = bot.loop
|
|
|
|
done, pending = await asyncio.wait(
|
|
[bot.wait("RPL_ENDOFMOTD"), bot.wait("ERR_NOMOTD")],
|
|
**args
|
|
)
|
|
|
|
# Cancel whichever waiter's event didn't come in.
|
|
for future in pending:
|
|
future.cancel()
|
|
|
|
for chan in settings.irc_channels:
|
|
if chan.startswith("#"):
|
|
bot.send('JOIN', channel=chan)
|
|
|
|
|
|
@bot.on('PING')
|
|
def keepalive(message, **kwargs):
|
|
bot.send('PONG', message=message)
|
|
|
|
|
|
@bot.on('client_disconnect')
|
|
def reconnect(**kwargs):
|
|
from ircradio.factory import app
|
|
app.logger.warning("Lost IRC server connection")
|
|
time.sleep(3)
|
|
bot.loop.create_task(bot.connect())
|
|
app.logger.warning("Reconnecting to IRC server")
|
|
|
|
|
|
class Commands:
|
|
LOOKUP = ['np', 'tune', 'boo', 'request', 'dj', 'url', 'urls',
|
|
'skip', 'listeners', 'queue',
|
|
'queue_user', 'pop', 'search', 'searchq', 'stats',
|
|
'rename', 'ban', 'whoami']
|
|
|
|
@staticmethod
|
|
async def np(*args, target=None, nick=None, **kwargs):
|
|
"""current song"""
|
|
radio_station = await Commands._parse_radio_station(args, target)
|
|
if not radio_station:
|
|
return
|
|
song = await radio_station.np()
|
|
if not song:
|
|
return await send_message(target, f"Nothing is playing?!")
|
|
|
|
np = "Now playing"
|
|
if radio_station.id != "wow":
|
|
np += f" @{radio_station.id}"
|
|
|
|
message = f"{np}: {song.title_cleaned}"
|
|
|
|
if song.id:
|
|
message += f" (rating: {song.karma}/10; by: {song.added_by}; id: {song.utube_id})"
|
|
|
|
time_status_str = song.time_status_str()
|
|
if time_status_str:
|
|
message += f" {time_status_str}"
|
|
|
|
await send_message(target=target, message=message)
|
|
|
|
@staticmethod
|
|
async def tune(*args, target=None, nick=None, **kwargs):
|
|
"""upvote song, only wow supported, not mixes"""
|
|
song = await radio_default.np()
|
|
if not song:
|
|
return await send_message(target, f"Nothing is playing?!")
|
|
|
|
song.karma += 1
|
|
song.save()
|
|
|
|
msg = f"Rating for \"{song.title}\" is {song.karma}/10 .. PARTY ON!!!!"
|
|
await send_message(target=target, message=msg)
|
|
|
|
@staticmethod
|
|
async def boo(*args, target=None, nick=None, **kwargs):
|
|
"""downvote song"""
|
|
song = await radio_default.np()
|
|
if not song:
|
|
return await send_message(target, f"Nothing is playing?!")
|
|
|
|
if song.karma >= 1:
|
|
song.karma -= 1
|
|
song.save()
|
|
|
|
msg = f"Rating for \"{song.title}\" is {song.karma}/10 .. BOOO!!!!"
|
|
await send_message(target=target, message=msg)
|
|
|
|
@staticmethod
|
|
async def request(*args, target=None, nick=None, **kwargs):
|
|
"""request a song by title or YouTube id"""
|
|
from ircradio.models import Song
|
|
if not args:
|
|
await send_message(target=target, message="usage: !request <id>")
|
|
|
|
songs = await Commands._return_song_results(*args, target=target, nick=nick, **kwargs)
|
|
|
|
if songs and len(songs) == 1:
|
|
song = songs[0]
|
|
await radio_default.queue_push(song.filepath)
|
|
msg = f"Added {song.title} to the queue"
|
|
|
|
return await send_message(target, msg)
|
|
|
|
if songs:
|
|
return await Commands._print_song_results(*args, target=target, nick=nick, songs=songs, **kwargs)
|
|
|
|
@staticmethod
|
|
async def search(*args, target=None, nick=None, **kwargs):
|
|
from ircradio.models import Song
|
|
if not args:
|
|
return await send_message(target=target, message="usage: !search <id>")
|
|
|
|
return await Commands._search(*args, target=target, nick=nick, **kwargs)
|
|
|
|
@staticmethod
|
|
async def searchq(*args, target=None, nick=None, **kwargs):
|
|
from ircradio.models import Song
|
|
if not args:
|
|
return await send_message(target=target, message="usage: !searchq <id>")
|
|
|
|
return await Commands._search(*args, target=target, nick=nick, report_quality=True, **kwargs)
|
|
|
|
@staticmethod
|
|
async def _search(*args, target=None, nick=None, **kwargs) -> Optional[List['Song']]:
|
|
"""search for a title"""
|
|
from ircradio.models import Song
|
|
|
|
report_quality = kwargs.get('report_quality')
|
|
|
|
songs = await Commands._return_song_results(*args, target=target, nick=nick, **kwargs)
|
|
|
|
if songs:
|
|
return await Commands._print_song_results(*args, target=target, nick=nick, report_quality=report_quality, songs=songs, **kwargs)
|
|
|
|
@staticmethod
|
|
async def _return_song_results(*args, target=None, nick=None, **kwargs) -> Optional[List['Song']]:
|
|
from ircradio.models import Song
|
|
|
|
needle = " ".join(args)
|
|
|
|
# https://git.wownero.com/dsc/ircradio/issues/1
|
|
needle_2nd = None
|
|
if "|" in needle:
|
|
spl = needle.split('|', 1)
|
|
a = spl[0].strip()
|
|
b = spl[1].strip()
|
|
needle = a
|
|
needle_2nd = b
|
|
|
|
try:
|
|
songs = Song.search(needle)
|
|
except Exception as ex:
|
|
return await send_message(target, f"{ex}")
|
|
if not songs:
|
|
return await send_message(target, "No song(s) found!")
|
|
|
|
if songs and needle_2nd:
|
|
songs = [s for s in songs if s.title and needle_2nd in s.title.lower()]
|
|
|
|
if not songs:
|
|
return await send_message(target, "No song(s) found after '|'!")
|
|
|
|
return songs
|
|
|
|
@staticmethod
|
|
async def _print_song_results(*args, target=None, nick=None, report_quality=None, songs=None, **kwargs):
|
|
from ircradio.models import Song
|
|
|
|
len_songs = len(songs)
|
|
max_songs = 6
|
|
moar = len_songs > max_songs
|
|
if len_songs > 1:
|
|
await send_message(target, "Multiple found:")
|
|
|
|
random.shuffle(songs)
|
|
|
|
for s in songs[:max_songs]:
|
|
msg = f"{s.utube_id} | {s.title}"
|
|
await s.scan(s.path or s.filepath)
|
|
|
|
if report_quality and s.meta:
|
|
if s.meta.bitrate:
|
|
msg += f" ({s.meta.bitrate / 1000}kbps)"
|
|
if s.meta.channels:
|
|
msg += f" (channels: {s.meta.channels}) "
|
|
if s.meta.sample_rate:
|
|
msg += f" (sample_rate: {s.meta.sample_rate}) "
|
|
await send_message(target, msg)
|
|
|
|
if moar:
|
|
await send_message(target, "[...]")
|
|
|
|
@staticmethod
|
|
async def dj(*args, target=None, nick=None, **kwargs):
|
|
"""add (or remove) a YouTube ID to the default radio"""
|
|
from ircradio.models import Song
|
|
if not args or args[0] not in ["-", "+"]:
|
|
return await send_message(target, "usage: dj+ <youtube_id>")
|
|
|
|
add: bool = args[0] == "+"
|
|
utube_id = args[1]
|
|
if not YouTube.is_valid_uid(utube_id):
|
|
return await send_message(target, "YouTube ID not valid.")
|
|
|
|
if add:
|
|
async with YT_DLP_LOCK:
|
|
try:
|
|
await send_message(target, f"Scheduled download for '{utube_id}'")
|
|
song = await YouTube.download(utube_id, added_by=nick)
|
|
await send_message(target, f"'{song.title}' added")
|
|
except Exception as ex:
|
|
return await send_message(target, f"Download '{utube_id}' failed; {ex}")
|
|
else:
|
|
try:
|
|
Song.delete_song(utube_id)
|
|
await send_message(target, "Press F to pay respects.")
|
|
except Exception as ex:
|
|
await send_message(target, f"Failed to remove {utube_id}; {ex}")
|
|
|
|
@staticmethod
|
|
async def skip(*args, target=None, nick=None, **kwargs):
|
|
"""skips current song"""
|
|
from ircradio.factory import app
|
|
radio_station = await Commands._parse_radio_station(args, target)
|
|
if not radio_station:
|
|
return
|
|
|
|
# song = radio_station.np()
|
|
# if not song:
|
|
# app.logger.error(f"nothing is playing?")
|
|
# return await send_message(target=target, message="Nothing is playing ?!")
|
|
|
|
try:
|
|
await radio_station.skip()
|
|
except Exception as ex:
|
|
app.logger.error(f"{ex}")
|
|
return await send_message(target=target, message="Nothing is playing ?!")
|
|
|
|
if radio_station.id == "wow":
|
|
_type = "Song"
|
|
else:
|
|
_type = "Mix"
|
|
|
|
await send_message(target, message=f"{_type} skipped. Booo! >:|")
|
|
|
|
@staticmethod
|
|
async def listeners(*args, target=None, nick=None, **kwargs):
|
|
"""current amount of listeners"""
|
|
from ircradio.factory import app
|
|
radio_station = await Commands._parse_radio_station(args, target)
|
|
if not radio_station:
|
|
return
|
|
|
|
listeners = await radio_station.get_listeners()
|
|
if listeners is None:
|
|
return await send_message(target, f"something went wrong")
|
|
if listeners == 0:
|
|
await send_message(target, f"no listeners, much sad :((")
|
|
|
|
msg = f"{listeners} client"
|
|
if listeners >= 2:
|
|
msg += "s"
|
|
msg += " connected"
|
|
return await send_message(target, msg)
|
|
|
|
@staticmethod
|
|
async def queue(*args, target=None, nick=None, **kwargs):
|
|
"""show currently queued tracks"""
|
|
from ircradio.models import Song
|
|
from ircradio.factory import app
|
|
radio_station = await Commands._parse_radio_station(args, target)
|
|
if not radio_station:
|
|
return
|
|
|
|
q: List[Song] = await radio_station.queue_get()
|
|
if not q:
|
|
return await send_message(target, "queue empty")
|
|
|
|
for i, s in enumerate(q):
|
|
await send_message(target, f"{s.utube_id} | {s.title}")
|
|
if i >= 8:
|
|
await send_message(target, "And some more...")
|
|
|
|
@staticmethod
|
|
async def rename(*args, target=None, nick=None, **kwargs):
|
|
from ircradio.models import Song
|
|
|
|
try:
|
|
utube_id = args[0]
|
|
title = " ".join(args[1:])
|
|
if not utube_id or not title or not YouTube.is_valid_uid(utube_id):
|
|
raise Exception("bad input")
|
|
except:
|
|
return await send_message(target, "usage: !rename <id> <new title>")
|
|
|
|
try:
|
|
song = Song.select().where(Song.utube_id == utube_id).get()
|
|
if not song:
|
|
raise Exception("Song not found")
|
|
except Exception as ex:
|
|
return await send_message(target, "Song not found.")
|
|
|
|
if song.added_by != nick and nick not in settings.irc_admins_nicknames:
|
|
return await send_message(target, "You may only rename your own songs.")
|
|
|
|
try:
|
|
Song.update(title=title).where(Song.utube_id == utube_id).execute()
|
|
except Exception as ex:
|
|
return await send_message(target, "Rename failure.")
|
|
|
|
await send_message(target, "Song renamed.")
|
|
|
|
@staticmethod
|
|
async def queue_user(*args, target=None, nick=None, **kwargs):
|
|
"""queue random song by username"""
|
|
from ircradio.models import Song
|
|
|
|
added_by = args[0]
|
|
try:
|
|
q = Song.select().where(Song.added_by ** f"%{added_by}%")
|
|
songs = [s for s in q]
|
|
except:
|
|
return await send_message(target, "No results.")
|
|
|
|
for i in range(0, 5):
|
|
song = random.choice(songs)
|
|
res = await radio_default.queue(song.filepath)
|
|
if res:
|
|
return await send_message(target, f"A random {added_by} has appeared in the queue: {song.title}")
|
|
|
|
await send_message(target, "queue_user exhausted!")
|
|
|
|
@staticmethod
|
|
async def stats(*args, target=None, nick=None, **kwargs):
|
|
"""random stats"""
|
|
songs = 0
|
|
try:
|
|
from ircradio.models import db
|
|
cursor = db.execute_sql('select count(*) from song;')
|
|
res = cursor.fetchone()
|
|
songs = res[0]
|
|
except:
|
|
pass
|
|
|
|
async with DU_LOCK:
|
|
disk = os.popen(f"du -h {settings.dir_music}").read().split("\t")[0]
|
|
mixes = os.popen(f"du -h /home/radio/mixes/").read().split("\n")[-2].split("\t")[0]
|
|
|
|
await send_message(target, f"Songs: {songs} | Mixes: {mixes} | Songs: {disk}")
|
|
|
|
@staticmethod
|
|
async def ban(*args, target=None, nick=None, **kwargs):
|
|
"""add (or remove) a YouTube ID ban (admins only)"""
|
|
if nick not in settings.irc_admins_nicknames:
|
|
await send_message(target, "You need to be an admin.")
|
|
return
|
|
|
|
from ircradio.models import Song, Ban
|
|
if not args or args[0] not in ["-", "+"]:
|
|
return await send_message(target, "usage: ban+ <youtube_id or nickname>")
|
|
|
|
try:
|
|
add: bool = args[0] == "+"
|
|
arg = args[1]
|
|
except:
|
|
return await send_message(target, "usage: ban+ <youtube_id or nickname>")
|
|
|
|
if add:
|
|
Ban.create(utube_id_or_nick=arg)
|
|
else:
|
|
Ban.delete().where(Ban.utube_id_or_nick == arg).execute()
|
|
await send_message(target, "Redemption")
|
|
|
|
@staticmethod
|
|
async def whoami(*args, target=None, nick=None, **kwargs):
|
|
if nick in settings.irc_admins_nicknames:
|
|
await send_message(target, "admin")
|
|
else:
|
|
await send_message(target, "user")
|
|
|
|
@staticmethod
|
|
async def url(*args, target=None, nick=None, **kwargs):
|
|
radio_station = await Commands._parse_radio_station(args, target)
|
|
if not radio_station:
|
|
return
|
|
|
|
msg = f"https://{settings.icecast2_hostname}/{radio_station.mount_point}"
|
|
await send_message(target, msg)
|
|
|
|
@staticmethod
|
|
async def urls(*args, target=None, nick=None, **kwargs):
|
|
url = f"https://{settings.icecast2_hostname}/{radio_stations['wow'].mount_point}"
|
|
msg = f"main programming: {url}"
|
|
await send_message(target, msg)
|
|
|
|
msg = "mixes: "
|
|
for _, radio_station in radio_stations.items():
|
|
if _ == "wow":
|
|
continue
|
|
url = f"https://{settings.icecast2_hostname}/{radio_station.mount_point}"
|
|
msg += f"{url} "
|
|
await send_message(target, msg)
|
|
|
|
@staticmethod
|
|
async def _parse_radio_station(args, target) -> Optional[Station]:
|
|
extras = " ".join(args).strip()
|
|
if not extras or len(args) >= 2:
|
|
return radio_default
|
|
|
|
err = f", available streams: " + " ".join(radio_stations.keys())
|
|
if not extras:
|
|
msg = "nothing not found (?) :-P" + err
|
|
return await send_message(target, msg)
|
|
extra = extras.strip()
|
|
if extra not in radio_stations:
|
|
msg = f"station \"{extra}\" not found" + err
|
|
return await send_message(target, msg)
|
|
return radio_stations[extra]
|
|
|
|
|
|
@bot.on('PRIVMSG')
|
|
async def message(nick, target, message, **kwargs):
|
|
from ircradio.factory import app
|
|
from ircradio.models import Ban
|
|
if nick == settings.irc_nick:
|
|
return
|
|
if settings.irc_ignore_pms and not target.startswith("#"):
|
|
return
|
|
|
|
if target == settings.irc_nick:
|
|
target = nick
|
|
|
|
msg = message
|
|
if not msg.startswith(settings.irc_command_prefix):
|
|
return
|
|
|
|
msg = msg[len(settings.irc_command_prefix):]
|
|
|
|
try:
|
|
if nick not in settings.irc_admins_nicknames:
|
|
banned = Ban.select().filter(utube_id_or_nick=nick).get()
|
|
if banned:
|
|
return
|
|
except:
|
|
pass
|
|
|
|
data = {
|
|
"nick": nick,
|
|
"target": target
|
|
}
|
|
|
|
spl = msg.split(" ")
|
|
cmd = spl[0].strip()
|
|
spl = spl[1:]
|
|
|
|
if cmd.endswith("+") or cmd.endswith("-"):
|
|
spl.insert(0, cmd[-1])
|
|
cmd = cmd[:-1]
|
|
|
|
if cmd in Commands.LOOKUP and hasattr(Commands, cmd):
|
|
attr = getattr(Commands, cmd)
|
|
try:
|
|
await attr(*spl, **data)
|
|
except Exception as ex:
|
|
app.logger.error(f"message_worker(): {ex}")
|
|
|
|
|
|
def start():
|
|
bot.loop.create_task(bot.connect())
|
|
|
|
|
|
async def send_message(target: str, message: str):
|
|
await msg_queue.put({"target": target, "message": message})
|