initial push
This commit is contained in:
6
util/__init__.py
Normal file
6
util/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python3.12
|
||||
|
||||
import importlib
|
||||
|
||||
from . import discord_helpers
|
||||
importlib.reload(discord_helpers)
|
52
util/discord_helpers.py
Normal file
52
util/discord_helpers.py
Normal file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3.12
|
||||
import re
|
||||
import discord
|
||||
from typing import Optional, Any
|
||||
|
||||
"""
|
||||
Discord Helper Methods
|
||||
"""
|
||||
|
||||
async def get_channel_by_name(bot: discord.Bot, channel: str,
|
||||
guild: int | None = None) -> Optional[Any]: # Optional[Any] used as pycord channel types can be ambigious
|
||||
"""
|
||||
Get Channel by Name
|
||||
Args:
|
||||
bot (discord.Bot)
|
||||
channel (str)
|
||||
guild (int|None)
|
||||
Returns:
|
||||
Optional[Any]
|
||||
"""
|
||||
channel: str = re.sub(r'^#', '', channel.strip())
|
||||
if not guild:
|
||||
return discord.utils.get(bot.get_all_channels(),
|
||||
name=channel)
|
||||
else:
|
||||
channels: list = bot.get_guild(guild).channels
|
||||
for _channel in channels:
|
||||
if _channel.name.lower() == channel.lower().strip():
|
||||
return _channel
|
||||
return
|
||||
|
||||
async def send_message(bot: discord.Bot, channel: str,
|
||||
message: str, guild: int | None = None) -> None:
|
||||
"""
|
||||
Send Message to the provided channel. If guild is provided, will limit to channels within that guild to ensure the correct
|
||||
channel is selected. Useful in the event a channel exists in more than one guild that the bot resides in.
|
||||
Args:
|
||||
bot (discord.Bot)
|
||||
channel (str)
|
||||
message (str)
|
||||
guild (int|None)
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if channel.isnumeric():
|
||||
channel: int = int(channel)
|
||||
_channel = bot.get_channel(channel)
|
||||
else:
|
||||
channel: str = re.sub(r'^#', '', channel.strip())
|
||||
_channel = await get_channel_by_name(bot=bot,
|
||||
channel=channel, guild=guild)
|
||||
await _channel.send(message)
|
162
util/lovehate_db.py
Normal file
162
util/lovehate_db.py
Normal file
@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3.12
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, LiteralString
|
||||
import aiosqlite as sqlite3
|
||||
from constructors import LoveHateException
|
||||
|
||||
class DB:
|
||||
"""LoveHate DB Utility Class"""
|
||||
def __init__(self, bot) -> None:
|
||||
self.db_path: str|LiteralString = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs", "lovehate.db")
|
||||
|
||||
async def get_wholovehates(self, thing: str, loves: bool = False,
|
||||
hates: bool = False) -> list[tuple]|bool:
|
||||
"""
|
||||
Get a list of users who have professed their love OR hatred for <thing>
|
||||
|
||||
Args:
|
||||
thing (str): The <thing> to check
|
||||
loves (bool): Are we looking for loves?
|
||||
hates (bool): ...or are we looking for hates?
|
||||
Returns:
|
||||
list[tuple]
|
||||
"""
|
||||
|
||||
query: str = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag = ?"
|
||||
params: tuple = tuple()
|
||||
flag: Optional[int] = None
|
||||
|
||||
if hates and loves:
|
||||
raise LoveHateException("Both hates and loves may not be True")
|
||||
elif hates:
|
||||
flag: int = -1
|
||||
elif loves:
|
||||
flag: int = 1
|
||||
elif not hates and not loves:
|
||||
raise LoveHateException("Neither loves nor hates were requested")
|
||||
|
||||
params: tuple = (thing, flag,)
|
||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||
async with await db_conn.execute(query, params) as db_cursor:
|
||||
result: list[tuple] = await db_cursor.fetchall()
|
||||
logging.debug("Result: %s", result)
|
||||
if not result:
|
||||
return False
|
||||
return result
|
||||
|
||||
async def get_lovehates(self, loves: bool = False, hates: bool = False,
|
||||
user: str = None, thing: str = None) -> list[tuple]|bool:
|
||||
"""
|
||||
Get a list of either 1) what {user} loves/hates, or who loves/hates {thing}, depending on bools loves, hates
|
||||
Args:
|
||||
loves (bool): Are we looking for loves?
|
||||
hates (bool): ...OR are we looking for hates?
|
||||
user (Optional[str]): the user to query against
|
||||
thing (Optional[str]): ... OR the thing to query against
|
||||
Returns:
|
||||
list[tuple]|bool
|
||||
"""
|
||||
|
||||
query: str = ""
|
||||
params: tuple = tuple()
|
||||
|
||||
if not user and not thing:
|
||||
raise LoveHateException("Neither a <user> or <thing> was specified to query against")
|
||||
|
||||
flag: Optional[int] = None
|
||||
|
||||
if hates and loves:
|
||||
raise LoveHateException("Both hates and loves may not be True")
|
||||
elif hates:
|
||||
flag: int = -1
|
||||
elif loves:
|
||||
flag: int = 1
|
||||
elif not hates and not loves:
|
||||
raise LoveHateException("Neither loves nor hates were requested")
|
||||
|
||||
if user:
|
||||
query: str = "SELECT thing FROM lovehate WHERE display_name LIKE ? AND flag == ?"
|
||||
params: tuple = (user, flag,)
|
||||
elif thing:
|
||||
query: str = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag == ?"
|
||||
params: tuple = (thing, flag,)
|
||||
|
||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||
async with await db_conn.execute(query, params) as db_cursor:
|
||||
result = await db_cursor.fetchall()
|
||||
if not result:
|
||||
return False
|
||||
return result
|
||||
|
||||
|
||||
async def check_existence(self, user: str, thing: str) -> Optional[int]:
|
||||
"""
|
||||
Determine whether a user is opinionated on a <thing>
|
||||
Args:
|
||||
user (str): The user to check
|
||||
thing (str): The thing to check if the user has an opinion on
|
||||
Returns:
|
||||
Optional[int]
|
||||
"""
|
||||
|
||||
params = (user, thing,)
|
||||
|
||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||
async with await db_conn.execute("SELECT id, flag FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?", params) as db_cursor:
|
||||
result = await db_cursor.fetchone()
|
||||
if not result:
|
||||
return None
|
||||
(_, flag) = result
|
||||
return flag
|
||||
|
||||
async def update(self, user: str, thing: str, flag: int) -> str:
|
||||
"""
|
||||
Updates the lovehate database, and returns an appropriate response
|
||||
Args:
|
||||
user (str): The user to update
|
||||
thing (str): The thing the user loves/hates/doesn't care about anymore
|
||||
flag (int): int representation of love (1), hate (-1), and dontcare (0)
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
if not flag in range(-1, 2):
|
||||
raise LoveHateException(f"Invalid flag {flag} specified, is this love (1), hate (-1), or dontcare? (0)")
|
||||
|
||||
db_query: str = ""
|
||||
params: tuple = (user, thing,)
|
||||
|
||||
already_opinionated: bool = await self.check_existence(user, thing)
|
||||
if already_opinionated:
|
||||
if flag == 0:
|
||||
db_query: str = "DELETE FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?"
|
||||
else:
|
||||
loves_or_hates: str = "loves"
|
||||
if already_opinionated == -1:
|
||||
loves_or_hates: str = "hates"
|
||||
raise LoveHateException(f"But {user} already {loves_or_hates} {thing}...")
|
||||
else:
|
||||
match flag:
|
||||
case -1:
|
||||
db_query: str = "INSERT INTO lovehate(display_name, flag, thing) VALUES(?, -1, ?)"
|
||||
case 1:
|
||||
db_query: str = "INSERT INTO lovehate(display_name, flag, thing) VALUES(?, 1, ?)"
|
||||
case _:
|
||||
raise LoveHateException("Unknown error, default case matched")
|
||||
|
||||
|
||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||
async with await db_conn.execute(db_query, params) as db_cursor:
|
||||
await db_conn.commit()
|
||||
if db_cursor.rowcount != 1:
|
||||
raise LoveHateException(f"DB Error - RowCount: {db_cursor.rowcount} for INSERT query")
|
||||
match flag:
|
||||
case -1:
|
||||
return f"We're done here, {user} hates {thing}."
|
||||
case 0:
|
||||
return f"We're done here, {user} no longer cares one way or the other about {thing}."
|
||||
case 1:
|
||||
return f"We're done here, {user} loves {thing}."
|
||||
case _:
|
||||
raise LoveHateException("Unknown error, default case matched [2]")
|
139
util/sing_util.py
Normal file
139
util/sing_util.py
Normal file
@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3.12
|
||||
import logging
|
||||
import regex
|
||||
import aiohttp
|
||||
import textwrap
|
||||
import traceback
|
||||
from typing import Optional
|
||||
from discord import Activity
|
||||
|
||||
class Utility:
|
||||
"""Sing Utility"""
|
||||
def __init__(self):
|
||||
self.api_url: str = "http://127.0.0.1:52111/lyric/search"
|
||||
self.api_src: str = "DISC-HAVOC"
|
||||
|
||||
def parse_song_input(self, song: Optional[str] = None,
|
||||
activity: Optional[Activity] = None) -> bool|tuple:
|
||||
"""Parse Song (Sing Command) Input
|
||||
Args:
|
||||
song (Optional[str]): Song to search
|
||||
activity (Optional[discord.Activity]): Discord activity, used to attempt lookup if no song is provided
|
||||
Returns:
|
||||
bool|tuple
|
||||
"""
|
||||
logging.debug("Activity? %s", activity)
|
||||
try:
|
||||
if (not song or len(song) < 2) and not activity:
|
||||
# pylint: disable=superfluous-parens
|
||||
return False
|
||||
# pylint: enable=superfluous-parens
|
||||
if not song and activity:
|
||||
match activity.name.lower():
|
||||
case "codey toons" | "cider" | "sonixd":
|
||||
search_artist: str = " ".join(str(activity.state)\
|
||||
.strip().split(" ")[1:])
|
||||
search_artist: str = regex.sub(r"(\s{0,})(\[(spotify|tidal|sonixd|browser|yt music)])$", "",
|
||||
search_artist.strip(), flags=regex.IGNORECASE)
|
||||
search_song: str = str(activity.details)
|
||||
song: str = f"{search_artist} : {search_song}"
|
||||
case "tidal hi-fi":
|
||||
search_artist: str = str(activity.state)
|
||||
search_song: str = str(activity.details)
|
||||
song: str = f"{search_artist} : {search_song}"
|
||||
case "spotify":
|
||||
search_artist: str = str(activity.title)
|
||||
search_song: str = str(activity.artist)
|
||||
song: str = f"{search_artist} : {search_song}"
|
||||
case "serious.fm" | "cocks.fm" | "something":
|
||||
if not activity.details:
|
||||
song: str = str(activity.state)
|
||||
else:
|
||||
search_artist: str = str(activity.state)
|
||||
search_song: str = str(activity.details)
|
||||
song: str = f"{search_artist} : {search_song}"
|
||||
case _:
|
||||
return False # Unsupported activity detected
|
||||
|
||||
search_split_by: str = ":" if not(song) or len(song.split(":")) > 1\
|
||||
else "-" # Support either : or - to separate artist/track
|
||||
search_artist: str = song.split(search_split_by)[0].strip()
|
||||
search_song: str = "".join(song.split(search_split_by)[1:]).strip()
|
||||
search_subsearch: Optional[str] = None
|
||||
if search_split_by == ":" and len(song.split(":")) > 2: # Support sub-search if : is used (per instructions)
|
||||
search_song: str = song.split(search_split_by)[1].strip() # Reduce search_song to only the 2nd split of : [the rest is meant to be lyric text]
|
||||
search_subsearch: str = "".join(song.split(search_split_by)[2:]) # Lyric text from split index 2 and beyond
|
||||
return (search_artist, search_song, search_subsearch)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
async def lyric_search(self, artist: str, song: str,
|
||||
sub: Optional[str] = None) -> list[str]:
|
||||
"""
|
||||
Lyric Search
|
||||
Args:
|
||||
artist (str): Artist to search
|
||||
song (str): Song to search
|
||||
sub (Optional[str]): Lyrics for subsearch
|
||||
"""
|
||||
try:
|
||||
if not artist or not song:
|
||||
return ["FAIL! Artist/Song not provided"]
|
||||
|
||||
search_obj: dict = {
|
||||
'a': artist.strip(),
|
||||
's': song.strip(),
|
||||
'extra': True,
|
||||
'src': self.api_src,
|
||||
}
|
||||
|
||||
if len(song.strip()) < 1:
|
||||
search_obj.pop('a')
|
||||
search_obj.pop('s')
|
||||
search_obj['t'] = artist.strip() # Parse failed, try title without sep
|
||||
|
||||
if sub and len(sub) >= 2:
|
||||
search_obj['sub'] = sub.strip()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with await session.post(self.api_url,
|
||||
json=search_obj,
|
||||
timeout=aiohttp.ClientTimeout(connect=5, sock_read=10)) as request:
|
||||
request.raise_for_status()
|
||||
response: dict = await request.json()
|
||||
if response.get('err'):
|
||||
return [f"ERR: {response.get('errorText')}"]
|
||||
|
||||
out_lyrics = regex.sub(r'<br>', '\u200B\n', response.get('lyrics'))
|
||||
response_obj: dict = {
|
||||
'artist': response.get('artist'),
|
||||
'song': response.get('song'),
|
||||
'lyrics': out_lyrics,
|
||||
'src': response.get('src'),
|
||||
'confidence': float(response.get('confidence')),
|
||||
'time': float(response.get('time')),
|
||||
}
|
||||
|
||||
lyrics = response_obj.get('lyrics')
|
||||
response_obj['lyrics'] = textwrap.wrap(text=lyrics.strip(),
|
||||
width=4000, drop_whitespace=False,
|
||||
replace_whitespace=False, break_long_words=True,
|
||||
break_on_hyphens=True, max_lines=8)
|
||||
response_obj['lyrics_short'] = textwrap.wrap(text=lyrics.strip(),
|
||||
width=750, drop_whitespace=False,
|
||||
replace_whitespace=False, break_long_words=True,
|
||||
break_on_hyphens=True, max_lines=1)
|
||||
|
||||
return [
|
||||
(
|
||||
response_obj.get('artist'), response_obj.get('song'), response_obj.get('src'),
|
||||
f"{int(response_obj.get('confidence'))}%",
|
||||
f"{response_obj.get('time', -666.0):.4f}s",
|
||||
),
|
||||
response_obj.get('lyrics'),
|
||||
response_obj.get('lyrics_short'),
|
||||
]
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return [f"Retrieval failed: {str(e)}"]
|
Reference in New Issue
Block a user