radio_util: open tracks SQLite DB in readonly mode; black: reformat files
This commit is contained in:
parent
96add377df
commit
6c88c23a4d
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,7 +12,6 @@ youtube*
|
|||||||
playlist_creator.py
|
playlist_creator.py
|
||||||
artist_genre_tag.py
|
artist_genre_tag.py
|
||||||
uv.lock
|
uv.lock
|
||||||
py.typed
|
|
||||||
pyproject.toml
|
pyproject.toml
|
||||||
mypy.ini
|
mypy.ini
|
||||||
.python-version
|
.python-version
|
79
base.py
79
base.py
@ -1,5 +1,6 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, ".")
|
sys.path.insert(0, ".")
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
@ -12,58 +13,57 @@ logger = logging.getLogger()
|
|||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
app = FastAPI(title="codey.lol API",
|
app = FastAPI(
|
||||||
version="1.0",
|
title="codey.lol API",
|
||||||
contact={
|
version="1.0",
|
||||||
'name': 'codey'
|
contact={"name": "codey"},
|
||||||
},
|
redirect_slashes=False,
|
||||||
redirect_slashes=False,
|
loop=loop,
|
||||||
loop=loop)
|
)
|
||||||
|
|
||||||
|
|
||||||
constants = importlib.import_module("constants").Constants()
|
constants = importlib.import_module("constants").Constants()
|
||||||
util = importlib.import_module("util").Utilities(app, constants)
|
util = importlib.import_module("util").Utilities(app, constants)
|
||||||
|
|
||||||
origins = [
|
origins = ["https://codey.lol", "https://api.codey.lol"]
|
||||||
"https://codey.lol",
|
|
||||||
"https://api.codey.lol"
|
|
||||||
]
|
|
||||||
|
|
||||||
app.add_middleware(CORSMiddleware, # type: ignore
|
app.add_middleware(
|
||||||
allow_origins=origins,
|
CORSMiddleware, # type: ignore
|
||||||
allow_credentials=True,
|
allow_origins=origins,
|
||||||
allow_methods=["POST", "GET", "HEAD"],
|
allow_credentials=True,
|
||||||
allow_headers=["*"]) # type: ignore
|
allow_methods=["POST", "GET", "HEAD"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Blacklisted routes
|
Blacklisted routes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", include_in_schema=False)
|
@app.get("/", include_in_schema=False)
|
||||||
def disallow_get():
|
def disallow_get():
|
||||||
return util.get_blocked_response()
|
return util.get_blocked_response()
|
||||||
|
|
||||||
|
|
||||||
@app.head("/", include_in_schema=False)
|
@app.head("/", include_in_schema=False)
|
||||||
def base_head():
|
def base_head():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@app.get("/{path}", include_in_schema=False)
|
@app.get("/{path}", include_in_schema=False)
|
||||||
def disallow_get_any(request: Request, var: Any = None):
|
def disallow_get_any(request: Request, var: Any = None):
|
||||||
path = request.path_params['path']
|
path = request.path_params["path"]
|
||||||
if not (
|
if not (isinstance(path, str) and path.split("/", maxsplit=1) == "widget"):
|
||||||
isinstance(path, str)
|
|
||||||
and
|
|
||||||
path.split("/", maxsplit=1) == "widget"
|
|
||||||
):
|
|
||||||
return util.get_blocked_response()
|
return util.get_blocked_response()
|
||||||
else:
|
else:
|
||||||
logging.info("OK, %s",
|
logging.info("OK, %s", path)
|
||||||
path)
|
|
||||||
|
|
||||||
@app.post("/", include_in_schema=False)
|
@app.post("/", include_in_schema=False)
|
||||||
def disallow_base_post():
|
def disallow_base_post():
|
||||||
return util.get_blocked_response()
|
return util.get_blocked_response()
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
End Blacklisted Routes
|
End Blacklisted Routes
|
||||||
"""
|
"""
|
||||||
@ -73,20 +73,28 @@ Actionable Routes
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
routes: dict = {
|
routes: dict = {
|
||||||
'randmsg': importlib.import_module("endpoints.rand_msg").RandMsg(app, util, constants),
|
"randmsg": importlib.import_module("endpoints.rand_msg").RandMsg(
|
||||||
'transcriptions': importlib.import_module("endpoints.transcriptions").Transcriptions(app, util, constants),
|
app, util, constants
|
||||||
'lyrics': importlib.import_module("endpoints.lyric_search").LyricSearch(app, util, constants),
|
),
|
||||||
'lastfm': importlib.import_module("endpoints.lastfm").LastFM(app, util, constants),
|
"transcriptions": importlib.import_module(
|
||||||
'yt': importlib.import_module("endpoints.yt").YT(app, util, constants),
|
"endpoints.transcriptions"
|
||||||
'karma': importlib.import_module("endpoints.karma").Karma(app, util, constants),
|
).Transcriptions(app, util, constants),
|
||||||
'radio': importlib.import_module("endpoints.radio").Radio(app, util, constants),
|
"lyrics": importlib.import_module("endpoints.lyric_search").LyricSearch(
|
||||||
'mgr': importlib.import_module("endpoints.mgr.mgr_test").Mgr(app, util, constants),
|
app, util, constants
|
||||||
|
),
|
||||||
|
"lastfm": importlib.import_module("endpoints.lastfm").LastFM(app, util, constants),
|
||||||
|
"yt": importlib.import_module("endpoints.yt").YT(app, util, constants),
|
||||||
|
"karma": importlib.import_module("endpoints.karma").Karma(app, util, constants),
|
||||||
|
"radio": importlib.import_module("endpoints.radio").Radio(app, util, constants),
|
||||||
|
"mgr": importlib.import_module("endpoints.mgr.mgr_test").Mgr(app, util, constants),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Misc endpoint depends on radio endpoint instance
|
# Misc endpoint depends on radio endpoint instance
|
||||||
radio_endpoint = routes.get('radio')
|
radio_endpoint = routes.get("radio")
|
||||||
if radio_endpoint:
|
if radio_endpoint:
|
||||||
routes['misc'] = importlib.import_module("endpoints.misc").Misc(app, util, constants, radio_endpoint)
|
routes["misc"] = importlib.import_module("endpoints.misc").Misc(
|
||||||
|
app, util, constants, radio_endpoint
|
||||||
|
)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
End Actionable Routes
|
End Actionable Routes
|
||||||
@ -98,5 +106,4 @@ Startup
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
redis = redis_cache.RedisCache()
|
redis = redis_cache.RedisCache()
|
||||||
loop.create_task(
|
loop.create_task(redis.create_index())
|
||||||
redis.create_index())
|
|
||||||
|
@ -5,6 +5,7 @@ from pydantic import BaseModel
|
|||||||
Karma
|
Karma
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ValidKarmaUpdateRequest(BaseModel):
|
class ValidKarmaUpdateRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
Requires authentication
|
Requires authentication
|
||||||
@ -25,35 +26,42 @@ class ValidKarmaRetrievalRequest(BaseModel):
|
|||||||
|
|
||||||
keyword: str
|
keyword: str
|
||||||
|
|
||||||
|
|
||||||
class ValidTopKarmaRequest(BaseModel):
|
class ValidTopKarmaRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **n**: Number of top results to return (default: 10)
|
- **n**: Number of top results to return (default: 10)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
n: Optional[int] = 10
|
n: Optional[int] = 10
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
LastFM
|
LastFM
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class LastFMException(Exception):
|
class LastFMException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ValidArtistSearchRequest(BaseModel):
|
class ValidArtistSearchRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **a**: artist name
|
- **a**: artist name
|
||||||
"""
|
"""
|
||||||
|
|
||||||
a: str
|
a: str
|
||||||
|
|
||||||
model_config = {
|
model_config = {
|
||||||
"json_schema_extra": {
|
"json_schema_extra": {
|
||||||
"examples": [
|
"examples": [
|
||||||
{
|
{
|
||||||
"a": "eminem",
|
"a": "eminem",
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ValidAlbumDetailRequest(BaseModel):
|
class ValidAlbumDetailRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **a**: artist name
|
- **a**: artist name
|
||||||
@ -64,15 +72,17 @@ class ValidAlbumDetailRequest(BaseModel):
|
|||||||
release: str
|
release: str
|
||||||
|
|
||||||
model_config = {
|
model_config = {
|
||||||
"json_schema_extra": {
|
"json_schema_extra": {
|
||||||
"examples": [
|
"examples": [
|
||||||
{
|
{
|
||||||
"a": "eminem",
|
"a": "eminem",
|
||||||
"release": "houdini",
|
"release": "houdini",
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ValidTrackInfoRequest(BaseModel):
|
class ValidTrackInfoRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **a**: artist name
|
- **a**: artist name
|
||||||
@ -81,21 +91,24 @@ class ValidTrackInfoRequest(BaseModel):
|
|||||||
|
|
||||||
a: str
|
a: str
|
||||||
t: str
|
t: str
|
||||||
|
|
||||||
model_config = {
|
model_config = {
|
||||||
"json_schema_extra": {
|
"json_schema_extra": {
|
||||||
"examples": [
|
"examples": [
|
||||||
{
|
{
|
||||||
"a": "eminem",
|
"a": "eminem",
|
||||||
"t": "rap god",
|
"t": "rap god",
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Rand Msg
|
Rand Msg
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class RandMsgRequest(BaseModel):
|
class RandMsgRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **short**: Short randmsg?
|
- **short**: Short randmsg?
|
||||||
@ -103,10 +116,12 @@ class RandMsgRequest(BaseModel):
|
|||||||
|
|
||||||
short: Optional[bool] = False
|
short: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
YT
|
YT
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ValidYTSearchRequest(BaseModel):
|
class ValidYTSearchRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **t**: title to search
|
- **t**: title to search
|
||||||
@ -114,10 +129,12 @@ class ValidYTSearchRequest(BaseModel):
|
|||||||
|
|
||||||
t: str = "rick astley - never gonna give you up"
|
t: str = "rick astley - never gonna give you up"
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
XC
|
XC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ValidXCRequest(BaseModel):
|
class ValidXCRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **key**: valid XC API key
|
- **key**: valid XC API key
|
||||||
@ -129,12 +146,14 @@ class ValidXCRequest(BaseModel):
|
|||||||
key: str
|
key: str
|
||||||
bid: int
|
bid: int
|
||||||
cmd: str
|
cmd: str
|
||||||
data: Optional[dict]
|
data: Optional[dict]
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Transcriptions
|
Transcriptions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ValidShowEpisodeListRequest(BaseModel):
|
class ValidShowEpisodeListRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **s**: show id
|
- **s**: show id
|
||||||
@ -142,6 +161,7 @@ class ValidShowEpisodeListRequest(BaseModel):
|
|||||||
|
|
||||||
s: int
|
s: int
|
||||||
|
|
||||||
|
|
||||||
class ValidShowEpisodeLineRequest(BaseModel):
|
class ValidShowEpisodeLineRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **s**: show id
|
- **s**: show id
|
||||||
@ -151,10 +171,12 @@ class ValidShowEpisodeLineRequest(BaseModel):
|
|||||||
s: int
|
s: int
|
||||||
e: int
|
e: int
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Lyric Search
|
Lyric Search
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ValidLyricRequest(BaseModel):
|
class ValidLyricRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **a**: artist
|
- **a**: artist
|
||||||
@ -167,7 +189,7 @@ class ValidLyricRequest(BaseModel):
|
|||||||
- **excluded_sources**: sources to exclude (new only)
|
- **excluded_sources**: sources to exclude (new only)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
a: Optional[str] = None
|
a: Optional[str] = None
|
||||||
s: Optional[str] = None
|
s: Optional[str] = None
|
||||||
t: Optional[str] = None
|
t: Optional[str] = None
|
||||||
sub: Optional[str] = None
|
sub: Optional[str] = None
|
||||||
@ -178,32 +200,37 @@ class ValidLyricRequest(BaseModel):
|
|||||||
|
|
||||||
model_config = {
|
model_config = {
|
||||||
"json_schema_extra": {
|
"json_schema_extra": {
|
||||||
"examples": [
|
"examples": [
|
||||||
{
|
{
|
||||||
"a": "eminem",
|
"a": "eminem",
|
||||||
"s": "rap god",
|
"s": "rap god",
|
||||||
"src": "WEB",
|
"src": "WEB",
|
||||||
"extra": True,
|
"extra": True,
|
||||||
"lrc": False,
|
"lrc": False,
|
||||||
"excluded_sources": [],
|
"excluded_sources": [],
|
||||||
}]
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ValidTypeAheadRequest(BaseModel):
|
class ValidTypeAheadRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **query**: query string
|
- **query**: query string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query: str
|
query: str
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Radio
|
Radio
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class RadioException(Exception):
|
class RadioException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ValidRadioSongRequest(BaseModel):
|
class ValidRadioSongRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **key**: API Key
|
- **key**: API Key
|
||||||
@ -212,55 +239,65 @@ class ValidRadioSongRequest(BaseModel):
|
|||||||
- **artistsong**: may be used IN PLACE OF artist/song to perform a combined/string search in the format "artist - song"
|
- **artistsong**: may be used IN PLACE OF artist/song to perform a combined/string search in the format "artist - song"
|
||||||
- **alsoSkip**: Whether to skip immediately to this track [not implemented]
|
- **alsoSkip**: Whether to skip immediately to this track [not implemented]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key: str
|
key: str
|
||||||
artist: Optional[str] = None
|
artist: Optional[str] = None
|
||||||
song: Optional[str] = None
|
song: Optional[str] = None
|
||||||
artistsong: Optional[str] = None
|
artistsong: Optional[str] = None
|
||||||
alsoSkip: Optional[bool] = False
|
alsoSkip: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
class ValidRadioTypeaheadRequest(BaseModel):
|
class ValidRadioTypeaheadRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **query**: Typeahead query
|
- **query**: Typeahead query
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query: str
|
query: str
|
||||||
|
|
||||||
|
|
||||||
class ValidRadioQueueGetRequest(BaseModel):
|
class ValidRadioQueueGetRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **key**: API key (optional, needed if specifying a non-default limit)
|
- **key**: API key (optional, needed if specifying a non-default limit)
|
||||||
- **limit**: optional, default: 15k
|
- **limit**: optional, default: 15k
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key: Optional[str] = None
|
key: Optional[str] = None
|
||||||
limit: Optional[int] = 15_000
|
limit: Optional[int] = 15_000
|
||||||
|
|
||||||
|
|
||||||
class ValidRadioNextRequest(BaseModel):
|
class ValidRadioNextRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **key**: API Key
|
- **key**: API Key
|
||||||
- **skipTo**: UUID to skip to [optional]
|
- **skipTo**: UUID to skip to [optional]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key: str
|
key: str
|
||||||
skipTo: Optional[str] = None
|
skipTo: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ValidRadioReshuffleRequest(ValidRadioNextRequest):
|
class ValidRadioReshuffleRequest(ValidRadioNextRequest):
|
||||||
"""
|
"""
|
||||||
- **key**: API Key
|
- **key**: API Key
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ValidRadioQueueShiftRequest(BaseModel):
|
class ValidRadioQueueShiftRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **key**: API Key
|
- **key**: API Key
|
||||||
- **uuid**: UUID to shift
|
- **uuid**: UUID to shift
|
||||||
- **next**: Play next if true, immediately if false, default False
|
- **next**: Play next if true, immediately if false, default False
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key: str
|
key: str
|
||||||
uuid: str
|
uuid: str
|
||||||
next: Optional[bool] = False
|
next: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
class ValidRadioQueueRemovalRequest(BaseModel):
|
class ValidRadioQueueRemovalRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **key**: API Key
|
- **key**: API Key
|
||||||
- **uuid**: UUID to remove
|
- **uuid**: UUID to remove
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key: str
|
key: str
|
||||||
uuid: str
|
uuid: str
|
||||||
|
@ -7,16 +7,22 @@ import aiosqlite as sqlite3
|
|||||||
from typing import LiteralString, Optional, Union
|
from typing import LiteralString, Optional, Union
|
||||||
from fastapi import FastAPI, Request, HTTPException
|
from fastapi import FastAPI, Request, HTTPException
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from .constructors import (ValidTopKarmaRequest, ValidKarmaRetrievalRequest,
|
from .constructors import (
|
||||||
ValidKarmaUpdateRequest)
|
ValidTopKarmaRequest,
|
||||||
|
ValidKarmaRetrievalRequest,
|
||||||
|
ValidKarmaUpdateRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class KarmaDB:
|
class KarmaDB:
|
||||||
"""Karma DB Util"""
|
"""Karma DB Util"""
|
||||||
def __init__(self) -> None:
|
|
||||||
self.db_path: LiteralString = os.path.join("/", "usr", "local", "share",
|
|
||||||
"sqlite_dbs", "karma.db")
|
|
||||||
|
|
||||||
async def get_karma(self, keyword: str) -> Union[int, dict]:
|
def __init__(self) -> None:
|
||||||
|
self.db_path: LiteralString = os.path.join(
|
||||||
|
"/", "usr", "local", "share", "sqlite_dbs", "karma.db"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_karma(self, keyword: str) -> Union[int, dict]:
|
||||||
"""Get Karma Value for Keyword
|
"""Get Karma Value for Keyword
|
||||||
Args:
|
Args:
|
||||||
keyword (str): The keyword to search
|
keyword (str): The keyword to search
|
||||||
@ -24,16 +30,18 @@ class KarmaDB:
|
|||||||
Union[int, dict]
|
Union[int, dict]
|
||||||
"""
|
"""
|
||||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||||
async with await db_conn.execute("SELECT score FROM karma WHERE keyword LIKE ? LIMIT 1", (keyword,)) as db_cursor:
|
async with await db_conn.execute(
|
||||||
|
"SELECT score FROM karma WHERE keyword LIKE ? LIMIT 1", (keyword,)
|
||||||
|
) as db_cursor:
|
||||||
try:
|
try:
|
||||||
(score,) = await db_cursor.fetchone()
|
(score,) = await db_cursor.fetchone()
|
||||||
return score
|
return score
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return {
|
return {
|
||||||
'err': True,
|
"err": True,
|
||||||
'errorText': f'No records for {keyword}',
|
"errorText": f"No records for {keyword}",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get_top(self, n: Optional[int] = 10) -> Optional[list[tuple]]:
|
async def get_top(self, n: Optional[int] = 10) -> Optional[list[tuple]]:
|
||||||
"""
|
"""
|
||||||
Get Top n=10 Karma Entries
|
Get Top n=10 Karma Entries
|
||||||
@ -44,14 +52,17 @@ class KarmaDB:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||||
async with await db_conn.execute("SELECT keyword, score FROM karma ORDER BY score DESC LIMIT ?", (n,)) as db_cursor:
|
async with await db_conn.execute(
|
||||||
|
"SELECT keyword, score FROM karma ORDER BY score DESC LIMIT ?", (n,)
|
||||||
|
) as db_cursor:
|
||||||
return await db_cursor.fetchall()
|
return await db_cursor.fetchall()
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def update_karma(self, granter: str, keyword: str,
|
async def update_karma(
|
||||||
flag: int) -> Optional[bool]:
|
self, granter: str, keyword: str, flag: int
|
||||||
|
) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
Update Karma for Keyword
|
Update Karma for Keyword
|
||||||
Args:
|
Args:
|
||||||
@ -61,42 +72,71 @@ class KarmaDB:
|
|||||||
Returns:
|
Returns:
|
||||||
Optional[bool]
|
Optional[bool]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not flag in [0, 1]:
|
if not flag in [0, 1]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
modifier: str = "score + 1" if not flag else "score - 1"
|
modifier: str = "score + 1" if not flag else "score - 1"
|
||||||
query: str = f"UPDATE karma SET score = {modifier}, last_change = ? WHERE keyword LIKE ?"
|
query: str = (
|
||||||
new_keyword_query: str = "INSERT INTO karma(keyword, score, last_change) VALUES(?, ?, ?)"
|
f"UPDATE karma SET score = {modifier}, last_change = ? WHERE keyword LIKE ?"
|
||||||
|
)
|
||||||
|
new_keyword_query: str = (
|
||||||
|
"INSERT INTO karma(keyword, score, last_change) VALUES(?, ?, ?)"
|
||||||
|
)
|
||||||
friendly_flag: str = "++" if not flag else "--"
|
friendly_flag: str = "++" if not flag else "--"
|
||||||
audit_message: str = f"{granter} adjusted karma for {keyword} @ {datetime.datetime.now().isoformat()}: {friendly_flag}"
|
audit_message: str = (
|
||||||
audit_query: str = "INSERT INTO karma_audit(impacted_keyword, comment) VALUES(?, ?)"
|
f"{granter} adjusted karma for {keyword} @ {datetime.datetime.now().isoformat()}: {friendly_flag}"
|
||||||
|
)
|
||||||
|
audit_query: str = (
|
||||||
|
"INSERT INTO karma_audit(impacted_keyword, comment) VALUES(?, ?)"
|
||||||
|
)
|
||||||
now: int = int(time.time())
|
now: int = int(time.time())
|
||||||
|
|
||||||
logging.debug("Audit message: %s{audit_message}\nKeyword: %s{keyword}")
|
logging.debug("Audit message: %s{audit_message}\nKeyword: %s{keyword}")
|
||||||
|
|
||||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||||
async with await db_conn.execute(audit_query, (keyword, audit_message,)) as db_cursor:
|
async with await db_conn.execute(
|
||||||
await db_conn.commit()
|
audit_query,
|
||||||
async with await db_conn.execute(query, (now, keyword,)) as db_cursor:
|
(
|
||||||
|
keyword,
|
||||||
|
audit_message,
|
||||||
|
),
|
||||||
|
) as db_cursor:
|
||||||
|
await db_conn.commit()
|
||||||
|
async with await db_conn.execute(
|
||||||
|
query,
|
||||||
|
(
|
||||||
|
now,
|
||||||
|
keyword,
|
||||||
|
),
|
||||||
|
) as db_cursor:
|
||||||
if db_cursor.rowcount:
|
if db_cursor.rowcount:
|
||||||
await db_conn.commit()
|
await db_conn.commit()
|
||||||
return True
|
return True
|
||||||
if db_cursor.rowcount < 1: # Keyword does not already exist
|
if db_cursor.rowcount < 1: # Keyword does not already exist
|
||||||
await db_cursor.close()
|
await db_cursor.close()
|
||||||
new_val = 1 if not flag else -1
|
new_val = 1 if not flag else -1
|
||||||
async with await db_conn.execute(new_keyword_query, (keyword, new_val, now,)) as db_cursor:
|
async with await db_conn.execute(
|
||||||
|
new_keyword_query,
|
||||||
|
(
|
||||||
|
keyword,
|
||||||
|
new_val,
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
) as db_cursor:
|
||||||
if db_cursor.rowcount >= 1:
|
if db_cursor.rowcount >= 1:
|
||||||
await db_conn.commit()
|
await db_conn.commit()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Karma(FastAPI):
|
class Karma(FastAPI):
|
||||||
"""
|
"""
|
||||||
Karma Endpoints
|
Karma Endpoints
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, app: FastAPI, util, constants) -> None:
|
def __init__(self, app: FastAPI, util, constants) -> None:
|
||||||
self.app: FastAPI = app
|
self.app: FastAPI = app
|
||||||
self.util = util
|
self.util = util
|
||||||
@ -107,86 +147,111 @@ class Karma(FastAPI):
|
|||||||
"karma/get": self.get_karma_handler,
|
"karma/get": self.get_karma_handler,
|
||||||
"karma/modify": self.modify_karma_handler,
|
"karma/modify": self.modify_karma_handler,
|
||||||
"karma/top": self.top_karma_handler,
|
"karma/top": self.top_karma_handler,
|
||||||
}
|
}
|
||||||
|
|
||||||
for endpoint, handler in self.endpoints.items():
|
for endpoint, handler in self.endpoints.items():
|
||||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
|
app.add_api_route(
|
||||||
include_in_schema=True)
|
f"/{endpoint}", handler, methods=["POST"], include_in_schema=True
|
||||||
|
)
|
||||||
|
|
||||||
|
async def top_karma_handler(
|
||||||
async def top_karma_handler(self, request: Request,
|
self, request: Request, data: Optional[ValidTopKarmaRequest] = None
|
||||||
data: Optional[ValidTopKarmaRequest] = None) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get top keywords for karma
|
Get top keywords for karma
|
||||||
- **n**: Number of top results to return (default: 10)
|
- **n**: Number of top results to return (default: 10)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
|
if not self.util.check_key(
|
||||||
|
request.url.path, request.headers.get("X-Authd-With")
|
||||||
|
):
|
||||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||||
|
|
||||||
n: int = 10
|
n: int = 10
|
||||||
if data and data.n:
|
if data and data.n:
|
||||||
n = int(data.n)
|
n = int(data.n)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
top10: Optional[list[tuple]] = await self.db.get_top(n=n)
|
top10: Optional[list[tuple]] = await self.db.get_top(n=n)
|
||||||
if not top10:
|
if not top10:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'General failure',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "General failure",
|
||||||
|
},
|
||||||
|
)
|
||||||
return JSONResponse(content=top10)
|
return JSONResponse(content=top10)
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Exception occurred.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Exception occurred.",
|
||||||
async def get_karma_handler(self, data: ValidKarmaRetrievalRequest,
|
},
|
||||||
request: Request) -> JSONResponse:
|
)
|
||||||
|
|
||||||
|
async def get_karma_handler(
|
||||||
|
self, data: ValidKarmaRetrievalRequest, request: Request
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get current karma value
|
Get current karma value
|
||||||
- **keyword**: Keyword to retrieve karma value for
|
- **keyword**: Keyword to retrieve karma value for
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
|
if not self.util.check_key(
|
||||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
request.url.path, request.headers.get("X-Authd-With")
|
||||||
|
):
|
||||||
|
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||||
|
|
||||||
keyword: str = data.keyword
|
keyword: str = data.keyword
|
||||||
try:
|
try:
|
||||||
count: Union[int, dict] = await self.db.get_karma(keyword)
|
count: Union[int, dict] = await self.db.get_karma(keyword)
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'keyword': keyword,
|
content={
|
||||||
'count': count,
|
"keyword": keyword,
|
||||||
})
|
"count": count,
|
||||||
|
}
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': "Exception occurred.",
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Exception occurred.",
|
||||||
async def modify_karma_handler(self, data: ValidKarmaUpdateRequest,
|
},
|
||||||
request: Request) -> JSONResponse:
|
)
|
||||||
|
|
||||||
|
async def modify_karma_handler(
|
||||||
|
self, data: ValidKarmaUpdateRequest, request: Request
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Update karma count
|
Update karma count
|
||||||
- **granter**: User who granted the karma
|
- **granter**: User who granted the karma
|
||||||
- **keyword**: The keyword to modify
|
- **keyword**: The keyword to modify
|
||||||
- **flag**: 0 to decrement (--), 1 to increment (++)
|
- **flag**: 0 to decrement (--), 1 to increment (++)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With'), 2):
|
if not self.util.check_key(
|
||||||
|
request.url.path, request.headers.get("X-Authd-With"), 2
|
||||||
|
):
|
||||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||||
|
|
||||||
if not data.flag in [0, 1]:
|
if not data.flag in [0, 1]:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Invalid request",
|
||||||
return JSONResponse(content={
|
},
|
||||||
'success': await self.db.update_karma(data.granter,
|
)
|
||||||
data.keyword, data.flag)
|
|
||||||
})
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": await self.db.update_karma(
|
||||||
|
data.granter, data.keyword, data.flag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -3,13 +3,18 @@ import traceback
|
|||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from .constructors import (ValidArtistSearchRequest, ValidAlbumDetailRequest,
|
from .constructors import (
|
||||||
ValidTrackInfoRequest, LastFMException)
|
ValidArtistSearchRequest,
|
||||||
|
ValidAlbumDetailRequest,
|
||||||
|
ValidTrackInfoRequest,
|
||||||
|
LastFMException,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LastFM(FastAPI):
|
class LastFM(FastAPI):
|
||||||
"""Last.FM Endpoints"""
|
"""Last.FM Endpoints"""
|
||||||
def __init__(self, app: FastAPI,
|
|
||||||
util, constants) -> None:
|
def __init__(self, app: FastAPI, util, constants) -> None:
|
||||||
self.app: FastAPI = app
|
self.app: FastAPI = app
|
||||||
self.util = util
|
self.util = util
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
@ -21,72 +26,94 @@ class LastFM(FastAPI):
|
|||||||
"lastfm/get_release": self.release_detail_handler,
|
"lastfm/get_release": self.release_detail_handler,
|
||||||
"lastfm/get_release_tracklist": self.release_tracklist_handler,
|
"lastfm/get_release_tracklist": self.release_tracklist_handler,
|
||||||
"lastfm/get_track_info": self.track_info_handler,
|
"lastfm/get_track_info": self.track_info_handler,
|
||||||
#tbd
|
# tbd
|
||||||
}
|
}
|
||||||
|
|
||||||
for endpoint, handler in self.endpoints.items():
|
for endpoint, handler in self.endpoints.items():
|
||||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
|
app.add_api_route(
|
||||||
include_in_schema=True)
|
f"/{endpoint}", handler, methods=["POST"], include_in_schema=True
|
||||||
|
)
|
||||||
async def artist_by_name_handler(self, data: ValidArtistSearchRequest) -> JSONResponse:
|
|
||||||
|
async def artist_by_name_handler(
|
||||||
|
self, data: ValidArtistSearchRequest
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get artist info
|
Get artist info
|
||||||
- **a**: Artist to search
|
- **a**: Artist to search
|
||||||
"""
|
"""
|
||||||
artist: Optional[str] = data.a.strip()
|
artist: Optional[str] = data.a.strip()
|
||||||
if not artist:
|
if not artist:
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'err': True,
|
content={
|
||||||
'errorText': 'No artist specified',
|
"err": True,
|
||||||
})
|
"errorText": "No artist specified",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
artist_result = await self.lastfm.search_artist(artist=artist)
|
artist_result = await self.lastfm.search_artist(artist=artist)
|
||||||
if not artist_result or not artist_result.get('bio')\
|
if (
|
||||||
or "err" in artist_result.keys():
|
not artist_result
|
||||||
return JSONResponse(status_code=500, content={
|
or not artist_result.get("bio")
|
||||||
'err': True,
|
or "err" in artist_result.keys()
|
||||||
'errorText': 'Search failed (no results?)',
|
):
|
||||||
})
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
return JSONResponse(content={
|
content={
|
||||||
'success': True,
|
"err": True,
|
||||||
'result': artist_result,
|
"errorText": "Search failed (no results?)",
|
||||||
})
|
},
|
||||||
|
)
|
||||||
async def artist_album_handler(self, data: ValidArtistSearchRequest) -> JSONResponse:
|
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": True,
|
||||||
|
"result": artist_result,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def artist_album_handler(
|
||||||
|
self, data: ValidArtistSearchRequest
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get artist's albums/releases
|
Get artist's albums/releases
|
||||||
- **a**: Artist to search
|
- **a**: Artist to search
|
||||||
"""
|
"""
|
||||||
artist: str = data.a.strip()
|
artist: str = data.a.strip()
|
||||||
if not artist:
|
if not artist:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request: No artist specified',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Invalid request: No artist specified",
|
||||||
album_result: Union[dict, list[dict]] = await self.lastfm.get_artist_albums(artist=artist)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
album_result: Union[dict, list[dict]] = await self.lastfm.get_artist_albums(
|
||||||
|
artist=artist
|
||||||
|
)
|
||||||
if isinstance(album_result, dict):
|
if isinstance(album_result, dict):
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'General failure.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "General failure.",
|
||||||
|
},
|
||||||
|
)
|
||||||
album_result_out: list = []
|
album_result_out: list = []
|
||||||
seen_release_titles: list = []
|
seen_release_titles: list = []
|
||||||
|
|
||||||
for release in album_result:
|
for release in album_result:
|
||||||
release_title: str = release.get('title', 'Unknown')
|
release_title: str = release.get("title", "Unknown")
|
||||||
if release_title.lower() in seen_release_titles:
|
if release_title.lower() in seen_release_titles:
|
||||||
continue
|
continue
|
||||||
seen_release_titles.append(release_title.lower())
|
seen_release_titles.append(release_title.lower())
|
||||||
album_result_out.append(release)
|
album_result_out.append(release)
|
||||||
|
|
||||||
return JSONResponse(content={
|
return JSONResponse(content={"success": True, "result": album_result_out})
|
||||||
'success': True,
|
|
||||||
'result': album_result_out
|
|
||||||
})
|
|
||||||
|
|
||||||
async def release_detail_handler(self, data: ValidAlbumDetailRequest) -> JSONResponse:
|
async def release_detail_handler(
|
||||||
|
self, data: ValidAlbumDetailRequest
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get details of a particular release by an artist
|
Get details of a particular release by an artist
|
||||||
- **a**: Artist to search
|
- **a**: Artist to search
|
||||||
@ -94,85 +121,105 @@ class LastFM(FastAPI):
|
|||||||
"""
|
"""
|
||||||
artist: str = data.a.strip()
|
artist: str = data.a.strip()
|
||||||
release: str = data.release.strip()
|
release: str = data.release.strip()
|
||||||
|
|
||||||
if not artist or not release:
|
if not artist or not release:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Invalid request",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
release_result = await self.lastfm.get_release(artist=artist, album=release)
|
release_result = await self.lastfm.get_release(artist=artist, album=release)
|
||||||
ret_obj = {
|
ret_obj = {
|
||||||
'id': release_result.get('id'),
|
"id": release_result.get("id"),
|
||||||
'artists': release_result.get('artists'),
|
"artists": release_result.get("artists"),
|
||||||
'title': release_result.get('title'),
|
"title": release_result.get("title"),
|
||||||
'summary': release_result.get('summary'),
|
"summary": release_result.get("summary"),
|
||||||
'tracks': release_result.get('tracks'),
|
"tracks": release_result.get("tracks"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'success': True,
|
content={
|
||||||
'result': ret_obj,
|
"success": True,
|
||||||
})
|
"result": ret_obj,
|
||||||
|
}
|
||||||
async def release_tracklist_handler(self, data: ValidAlbumDetailRequest) -> JSONResponse:
|
)
|
||||||
|
|
||||||
|
async def release_tracklist_handler(
|
||||||
|
self, data: ValidAlbumDetailRequest
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get track list for a particular release by an artist
|
Get track list for a particular release by an artist
|
||||||
- **a**: Artist to search
|
- **a**: Artist to search
|
||||||
- **release**: Release title to search
|
- **release**: Release title to search
|
||||||
"""
|
"""
|
||||||
artist: str = data.a.strip()
|
artist: str = data.a.strip()
|
||||||
release: str = data.release.strip()
|
release: str = data.release.strip()
|
||||||
|
|
||||||
if not artist or not release:
|
if not artist or not release:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Invalid request",
|
||||||
tracklist_result: dict = await self.lastfm.get_album_tracklist(artist=artist, album=release)
|
},
|
||||||
return JSONResponse(content={
|
)
|
||||||
'success': True,
|
|
||||||
'id': tracklist_result.get('id'),
|
tracklist_result: dict = await self.lastfm.get_album_tracklist(
|
||||||
'artists': tracklist_result.get('artists'),
|
artist=artist, album=release
|
||||||
'title': tracklist_result.get('title'),
|
)
|
||||||
'summary': tracklist_result.get('summary'),
|
return JSONResponse(
|
||||||
'tracks': tracklist_result.get('tracks'),
|
content={
|
||||||
})
|
"success": True,
|
||||||
|
"id": tracklist_result.get("id"),
|
||||||
|
"artists": tracklist_result.get("artists"),
|
||||||
|
"title": tracklist_result.get("title"),
|
||||||
|
"summary": tracklist_result.get("summary"),
|
||||||
|
"tracks": tracklist_result.get("tracks"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def track_info_handler(self, data: ValidTrackInfoRequest) -> JSONResponse:
|
async def track_info_handler(self, data: ValidTrackInfoRequest) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get track info from Last.FM given an artist/track
|
Get track info from Last.FM given an artist/track
|
||||||
- **a**: Artist to search
|
- **a**: Artist to search
|
||||||
- **t**: Track title to search
|
- **t**: Track title to search
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
artist: str = data.a
|
artist: str = data.a
|
||||||
track: str = data.t
|
track: str = data.t
|
||||||
|
|
||||||
if not artist or not track:
|
if not artist or not track:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request'
|
content={"err": True, "errorText": "Invalid request"},
|
||||||
})
|
)
|
||||||
|
|
||||||
track_info_result: Optional[dict] = await self.lastfm.get_track_info(artist=artist,
|
track_info_result: Optional[dict] = await self.lastfm.get_track_info(
|
||||||
track=track)
|
artist=artist, track=track
|
||||||
|
)
|
||||||
if not track_info_result:
|
if not track_info_result:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Not found.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Not found.",
|
||||||
|
},
|
||||||
|
)
|
||||||
if "err" in track_info_result:
|
if "err" in track_info_result:
|
||||||
raise LastFMException("Unknown error occurred: %s",
|
raise LastFMException(
|
||||||
track_info_result.get('errorText', '??'))
|
"Unknown error occurred: %s",
|
||||||
return JSONResponse(content={
|
track_info_result.get("errorText", "??"),
|
||||||
'success': True,
|
)
|
||||||
'result': track_info_result
|
return JSONResponse(content={"success": True, "result": track_info_result})
|
||||||
})
|
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'General error',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "General error",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@ -9,23 +9,25 @@ from typing import LiteralString, Optional, Union, Iterable
|
|||||||
from regex import Pattern
|
from regex import Pattern
|
||||||
from .constructors import ValidTypeAheadRequest, ValidLyricRequest
|
from .constructors import ValidTypeAheadRequest, ValidLyricRequest
|
||||||
from lyric_search.constructors import LyricsResult
|
from lyric_search.constructors import LyricsResult
|
||||||
from lyric_search.sources import aggregate
|
from lyric_search.sources import aggregate
|
||||||
from lyric_search import notifier
|
from lyric_search import notifier
|
||||||
|
|
||||||
|
|
||||||
class CacheUtils:
|
class CacheUtils:
|
||||||
"""
|
"""
|
||||||
Lyrics Cache DB Utils
|
Lyrics Cache DB Utils
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.lyrics_db_path: LiteralString = os.path.join("/usr/local/share",
|
self.lyrics_db_path: LiteralString = os.path.join(
|
||||||
"sqlite_dbs", "cached_lyrics.db")
|
"/usr/local/share", "sqlite_dbs", "cached_lyrics.db"
|
||||||
|
)
|
||||||
|
|
||||||
async def check_typeahead(self, query: str) -> Optional[list[str]]:
|
async def check_typeahead(self, query: str) -> Optional[list[str]]:
|
||||||
"""Lyric Search Typeahead DB Handler"""
|
"""Lyric Search Typeahead DB Handler"""
|
||||||
if not query:
|
if not query:
|
||||||
return None
|
return None
|
||||||
async with sqlite3.connect(self.lyrics_db_path,
|
async with sqlite3.connect(self.lyrics_db_path, timeout=1) as _db:
|
||||||
timeout=1) as _db:
|
|
||||||
_db.row_factory = sqlite3.Row
|
_db.row_factory = sqlite3.Row
|
||||||
db_query: str = """SELECT DISTINCT(LOWER(TRIM(artist) || " - " || TRIM(song))),\
|
db_query: str = """SELECT DISTINCT(LOWER(TRIM(artist) || " - " || TRIM(song))),\
|
||||||
(TRIM(artist) || " - " || TRIM(song)) as ret FROM lyrics WHERE\
|
(TRIM(artist) || " - " || TRIM(song)) as ret FROM lyrics WHERE\
|
||||||
@ -33,9 +35,7 @@ class CacheUtils:
|
|||||||
db_params: tuple[str] = (f"%%%{query}%%%",)
|
db_params: tuple[str] = (f"%%%{query}%%%",)
|
||||||
async with _db.execute(db_query, db_params) as _cursor:
|
async with _db.execute(db_query, db_params) as _cursor:
|
||||||
result: Iterable[sqlite3.Row] = await _cursor.fetchall()
|
result: Iterable[sqlite3.Row] = await _cursor.fetchall()
|
||||||
out_result = [
|
out_result = [str(r["ret"]) for r in result]
|
||||||
str(r['ret']) for r in result
|
|
||||||
]
|
|
||||||
return out_result
|
return out_result
|
||||||
|
|
||||||
|
|
||||||
@ -43,18 +43,17 @@ class LyricSearch(FastAPI):
|
|||||||
"""
|
"""
|
||||||
Lyric Search Endpoint
|
Lyric Search Endpoint
|
||||||
"""
|
"""
|
||||||
def __init__(self, app: FastAPI,
|
|
||||||
util, constants) -> None:
|
def __init__(self, app: FastAPI, util, constants) -> None:
|
||||||
self.app: FastAPI = app
|
self.app: FastAPI = app
|
||||||
self.util = util
|
self.util = util
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
self.cache_utils = CacheUtils()
|
self.cache_utils = CacheUtils()
|
||||||
self.notifier = notifier.DiscordNotifier()
|
self.notifier = notifier.DiscordNotifier()
|
||||||
|
|
||||||
|
|
||||||
self.endpoints: dict = {
|
self.endpoints: dict = {
|
||||||
"typeahead/lyrics": self.typeahead_handler,
|
"typeahead/lyrics": self.typeahead_handler,
|
||||||
"lyric_search": self.lyric_search_handler, # Preserving old endpoint path temporarily
|
"lyric_search": self.lyric_search_handler, # Preserving old endpoint path temporarily
|
||||||
"lyric/search": self.lyric_search_handler,
|
"lyric/search": self.lyric_search_handler,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,11 +65,18 @@ class LyricSearch(FastAPI):
|
|||||||
"IRC-SHARED",
|
"IRC-SHARED",
|
||||||
]
|
]
|
||||||
|
|
||||||
self.lrc_regex: Pattern = regex.compile(r'\[([0-9]{2}:[0-9]{2})\.[0-9]{1,3}\](\s(.*)){0,}')
|
self.lrc_regex: Pattern = regex.compile(
|
||||||
|
r"\[([0-9]{2}:[0-9]{2})\.[0-9]{1,3}\](\s(.*)){0,}"
|
||||||
|
)
|
||||||
|
|
||||||
for endpoint, handler in self.endpoints.items():
|
for endpoint, handler in self.endpoints.items():
|
||||||
_schema_include = endpoint in ["lyric/search"]
|
_schema_include = endpoint in ["lyric/search"]
|
||||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"], include_in_schema=_schema_include)
|
app.add_api_route(
|
||||||
|
f"/{endpoint}",
|
||||||
|
handler,
|
||||||
|
methods=["POST"],
|
||||||
|
include_in_schema=_schema_include,
|
||||||
|
)
|
||||||
|
|
||||||
async def typeahead_handler(self, data: ValidTypeAheadRequest) -> JSONResponse:
|
async def typeahead_handler(self, data: ValidTypeAheadRequest) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
@ -78,104 +84,133 @@ class LyricSearch(FastAPI):
|
|||||||
- **query**: Typeahead query
|
- **query**: Typeahead query
|
||||||
"""
|
"""
|
||||||
if not isinstance(data.query, str):
|
if not isinstance(data.query, str):
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
typeahead: Optional[list[str]] = await self.cache_utils.check_typeahead(data.query)
|
"errorText": "Invalid request.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
typeahead: Optional[list[str]] = await self.cache_utils.check_typeahead(
|
||||||
|
data.query
|
||||||
|
)
|
||||||
if not typeahead:
|
if not typeahead:
|
||||||
return JSONResponse(content=[])
|
return JSONResponse(content=[])
|
||||||
return JSONResponse(content=typeahead)
|
return JSONResponse(content=typeahead)
|
||||||
|
|
||||||
|
|
||||||
async def lyric_search_handler(self, data: ValidLyricRequest) -> JSONResponse:
|
async def lyric_search_handler(self, data: ValidLyricRequest) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Search for lyrics
|
Search for lyrics
|
||||||
- **a**: artist
|
- **a**: artist
|
||||||
- **s**: song
|
- **s**: song
|
||||||
- **t**: track (artist and song combined) [used only if a & s are not used]
|
- **t**: track (artist and song combined) [used only if a & s are not used]
|
||||||
- **extra**: include extra details in response [optional, default: false]
|
- **extra**: include extra details in response [optional, default: false]
|
||||||
- **lrc**: Request LRCs?
|
- **lrc**: Request LRCs?
|
||||||
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional, default: none]
|
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional, default: none]
|
||||||
- **src**: the script/utility which initiated the request
|
- **src**: the script/utility which initiated the request
|
||||||
- **excluded_sources**: sources to exclude [optional, default: none]
|
- **excluded_sources**: sources to exclude [optional, default: none]
|
||||||
"""
|
"""
|
||||||
if (not data.a or not data.s) and not data.t or not data.src:
|
if (not data.a or not data.s) and not data.t or not data.src:
|
||||||
raise HTTPException(detail="Invalid request", status_code=500)
|
raise HTTPException(detail="Invalid request", status_code=500)
|
||||||
|
|
||||||
if data.src.upper() not in self.acceptable_request_sources:
|
if data.src.upper() not in self.acceptable_request_sources:
|
||||||
await self.notifier.send(f"ERROR @ {__file__.rsplit("/", maxsplit=1)[-1]}",
|
await self.notifier.send(
|
||||||
f"Unknown request source: {data.src}")
|
f"ERROR @ {__file__.rsplit("/", maxsplit=1)[-1]}",
|
||||||
return JSONResponse(status_code=500, content={
|
f"Unknown request source: {data.src}",
|
||||||
'err': True,
|
)
|
||||||
'errorText': f'Unknown request source: {data.src}',
|
return JSONResponse(
|
||||||
})
|
status_code=500,
|
||||||
|
content={
|
||||||
|
"err": True,
|
||||||
|
"errorText": f"Unknown request source: {data.src}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if not data.t:
|
if not data.t:
|
||||||
search_artist: Optional[str] = data.a
|
search_artist: Optional[str] = data.a
|
||||||
search_song: Optional[str] = data.s
|
search_song: Optional[str] = data.s
|
||||||
else:
|
else:
|
||||||
t_split: tuple = tuple(data.t.split(" - ", maxsplit=1))
|
t_split: tuple = tuple(data.t.split(" - ", maxsplit=1))
|
||||||
(search_artist, search_song) = t_split
|
(search_artist, search_song) = t_split
|
||||||
|
|
||||||
if search_artist and search_song:
|
if search_artist and search_song:
|
||||||
search_artist = str(self.constants.DOUBLE_SPACE_REGEX.sub(" ", search_artist.strip()))
|
search_artist = str(
|
||||||
search_song = str(self.constants.DOUBLE_SPACE_REGEX.sub(" ", search_song.strip()))
|
self.constants.DOUBLE_SPACE_REGEX.sub(" ", search_artist.strip())
|
||||||
|
)
|
||||||
|
search_song = str(
|
||||||
|
self.constants.DOUBLE_SPACE_REGEX.sub(" ", search_song.strip())
|
||||||
|
)
|
||||||
search_artist = urllib.parse.unquote(search_artist)
|
search_artist = urllib.parse.unquote(search_artist)
|
||||||
search_song = urllib.parse.unquote(search_song)
|
search_song = urllib.parse.unquote(search_song)
|
||||||
|
|
||||||
if not isinstance(search_artist, str) or not isinstance(search_song, str):
|
if not isinstance(search_artist, str) or not isinstance(search_song, str):
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Invalid request",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
excluded_sources: Optional[list] = data.excluded_sources
|
excluded_sources: Optional[list] = data.excluded_sources
|
||||||
aggregate_search = aggregate.Aggregate(exclude_methods=excluded_sources)
|
aggregate_search = aggregate.Aggregate(exclude_methods=excluded_sources)
|
||||||
plain_lyrics: bool = not data.lrc
|
plain_lyrics: bool = not data.lrc
|
||||||
result: Optional[Union[LyricsResult, dict]] = await aggregate_search.search(search_artist, search_song, plain_lyrics)
|
result: Optional[Union[LyricsResult, dict]] = await aggregate_search.search(
|
||||||
|
search_artist, search_song, plain_lyrics
|
||||||
|
)
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'err': True,
|
content={
|
||||||
'errorText': 'Sources exhausted, lyrics not located.',
|
"err": True,
|
||||||
})
|
"errorText": "Sources exhausted, lyrics not located.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
result = vars(result)
|
result = vars(result)
|
||||||
|
|
||||||
if data.sub and not data.lrc:
|
if data.sub and not data.lrc:
|
||||||
seeked_found_line: Optional[int] = None
|
seeked_found_line: Optional[int] = None
|
||||||
lyric_lines: list[str] = result['lyrics'].strip().split(" / ")
|
lyric_lines: list[str] = result["lyrics"].strip().split(" / ")
|
||||||
for i, line in enumerate(lyric_lines):
|
for i, line in enumerate(lyric_lines):
|
||||||
line = regex.sub(r'\u2064', '', line.strip())
|
line = regex.sub(r"\u2064", "", line.strip())
|
||||||
if data.sub.strip().lower() in line.strip().lower():
|
if data.sub.strip().lower() in line.strip().lower():
|
||||||
seeked_found_line = i
|
seeked_found_line = i
|
||||||
logging.debug("Found %s at %s, match for %s!",
|
logging.debug(
|
||||||
line, seeked_found_line, data.sub) # REMOVEME: DEBUG
|
"Found %s at %s, match for %s!",
|
||||||
|
line,
|
||||||
|
seeked_found_line,
|
||||||
|
data.sub,
|
||||||
|
) # REMOVEME: DEBUG
|
||||||
break
|
break
|
||||||
|
|
||||||
if not seeked_found_line:
|
if not seeked_found_line:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Seek (a.k.a. subsearch) failed.',
|
content={
|
||||||
'failed_seek': True,
|
"err": True,
|
||||||
})
|
"errorText": "Seek (a.k.a. subsearch) failed.",
|
||||||
result['lyrics'] = " / ".join(lyric_lines[seeked_found_line:])
|
"failed_seek": True,
|
||||||
|
},
|
||||||
result['confidence'] = int(result['confidence'])
|
)
|
||||||
result['time'] = f'{float(result['time']):.4f}'
|
result["lyrics"] = " / ".join(lyric_lines[seeked_found_line:])
|
||||||
|
|
||||||
|
result["confidence"] = int(result["confidence"])
|
||||||
|
result["time"] = f"{float(result['time']):.4f}"
|
||||||
|
|
||||||
if plain_lyrics:
|
if plain_lyrics:
|
||||||
result['lyrics'] = regex.sub(r'(\s/\s|\n)', '<br>', result['lyrics']).strip()
|
result["lyrics"] = regex.sub(
|
||||||
|
r"(\s/\s|\n)", "<br>", result["lyrics"]
|
||||||
|
).strip()
|
||||||
else:
|
else:
|
||||||
# Swap lyrics key for 'lrc'
|
# Swap lyrics key for 'lrc'
|
||||||
result['lrc'] = result['lyrics']
|
result["lrc"] = result["lyrics"]
|
||||||
result.pop('lyrics')
|
result.pop("lyrics")
|
||||||
|
|
||||||
if "cache" in result['src']:
|
if "cache" in result["src"]:
|
||||||
result['from_cache'] = True
|
result["from_cache"] = True
|
||||||
|
|
||||||
if not data.extra:
|
if not data.extra:
|
||||||
result.pop('src')
|
result.pop("src")
|
||||||
|
|
||||||
return JSONResponse(content=result)
|
return JSONResponse(content=result)
|
||||||
|
@ -2,20 +2,18 @@ import logging
|
|||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
from typing import Optional, Annotated
|
from typing import Optional, Annotated
|
||||||
from fastapi import (
|
from fastapi import FastAPI, Request, UploadFile, Response, HTTPException, Form
|
||||||
FastAPI, Request, UploadFile,
|
|
||||||
Response, HTTPException, Form
|
|
||||||
)
|
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
import redis.asyncio as redis
|
import redis.asyncio as redis
|
||||||
from lyric_search.sources import private, cache as LyricsCache, redis_cache
|
from lyric_search.sources import private, cache as LyricsCache, redis_cache
|
||||||
|
|
||||||
|
|
||||||
class Misc(FastAPI):
|
class Misc(FastAPI):
|
||||||
"""
|
"""
|
||||||
Misc Endpoints
|
Misc Endpoints
|
||||||
"""
|
"""
|
||||||
def __init__(self, app: FastAPI, my_util,
|
|
||||||
constants, radio) -> None:
|
def __init__(self, app: FastAPI, my_util, constants, radio) -> None:
|
||||||
self.app: FastAPI = app
|
self.app: FastAPI = app
|
||||||
self.util = my_util
|
self.util = my_util
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
@ -28,47 +26,55 @@ class Misc(FastAPI):
|
|||||||
"widget/redis": self.homepage_redis_widget,
|
"widget/redis": self.homepage_redis_widget,
|
||||||
"widget/sqlite": self.homepage_sqlite_widget,
|
"widget/sqlite": self.homepage_sqlite_widget,
|
||||||
"widget/lyrics": self.homepage_lyrics_widget,
|
"widget/lyrics": self.homepage_lyrics_widget,
|
||||||
"widget/radio": self.homepage_radio_widget,
|
"widget/radio": self.homepage_radio_widget,
|
||||||
"misc/get_activity_image": self.get_activity_image,
|
"misc/get_activity_image": self.get_activity_image,
|
||||||
}
|
}
|
||||||
|
|
||||||
for endpoint, handler in self.endpoints.items():
|
for endpoint, handler in self.endpoints.items():
|
||||||
app.add_api_route(f"/{endpoint}", handler, methods=["GET"],
|
app.add_api_route(
|
||||||
include_in_schema=True)
|
f"/{endpoint}", handler, methods=["GET"], include_in_schema=True
|
||||||
|
)
|
||||||
app.add_api_route("/misc/upload_activity_image",
|
|
||||||
self.upload_activity_image, methods=["POST"])
|
app.add_api_route(
|
||||||
|
"/misc/upload_activity_image", self.upload_activity_image, methods=["POST"]
|
||||||
async def upload_activity_image(self,
|
)
|
||||||
image: UploadFile,
|
|
||||||
key: Annotated[str, Form()], request: Request) -> Response:
|
async def upload_activity_image(
|
||||||
if not key or not isinstance(key, str)\
|
self, image: UploadFile, key: Annotated[str, Form()], request: Request
|
||||||
or not self.util.check_key(path=request.url.path, req_type=2, key=key):
|
) -> Response:
|
||||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
if (
|
||||||
|
not key
|
||||||
|
or not isinstance(key, str)
|
||||||
|
or not self.util.check_key(path=request.url.path, req_type=2, key=key)
|
||||||
|
):
|
||||||
|
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||||
if not image:
|
if not image:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Invalid request",
|
||||||
|
},
|
||||||
|
)
|
||||||
self.activity_image = await image.read()
|
self.activity_image = await image.read()
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'success': True,
|
content={
|
||||||
})
|
"success": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def get_activity_image(self, request: Request) -> Response:
|
async def get_activity_image(self, request: Request) -> Response:
|
||||||
if isinstance(self.activity_image, bytes):
|
if isinstance(self.activity_image, bytes):
|
||||||
return Response(content=self.activity_image,
|
return Response(content=self.activity_image, media_type="image/png")
|
||||||
media_type="image/png")
|
|
||||||
|
|
||||||
|
|
||||||
# Fallback
|
# Fallback
|
||||||
fallback_path = os.path.join("/var/www/codey.lol/public",
|
fallback_path = os.path.join(
|
||||||
"images", "plex_placeholder.png")
|
"/var/www/codey.lol/public", "images", "plex_placeholder.png"
|
||||||
|
)
|
||||||
with open(fallback_path, 'rb') as f:
|
|
||||||
return Response(content=f.read(),
|
with open(fallback_path, "rb") as f:
|
||||||
media_type="image/png")
|
return Response(content=f.read(), media_type="image/png")
|
||||||
|
|
||||||
async def get_radio_np(self) -> tuple[str, str, str]:
|
async def get_radio_np(self) -> tuple[str, str, str]:
|
||||||
"""
|
"""
|
||||||
Get radio now playing
|
Get radio now playing
|
||||||
@ -77,76 +83,94 @@ class Misc(FastAPI):
|
|||||||
Returns:
|
Returns:
|
||||||
str: Radio now playing in artist - song format
|
str: Radio now playing in artist - song format
|
||||||
"""
|
"""
|
||||||
|
|
||||||
np: dict = self.radio.radio_util.now_playing
|
np: dict = self.radio.radio_util.now_playing
|
||||||
artistsong: str = np.get('artistsong', 'N/A - N/A')
|
artistsong: str = np.get("artistsong", "N/A - N/A")
|
||||||
album: str = np.get('album', 'N/A')
|
album: str = np.get("album", "N/A")
|
||||||
genre: str = np.get('genre', 'N/A')
|
genre: str = np.get("genre", "N/A")
|
||||||
return (artistsong, album, genre)
|
return (artistsong, album, genre)
|
||||||
|
|
||||||
|
|
||||||
async def homepage_redis_widget(self) -> JSONResponse:
|
async def homepage_redis_widget(self) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Homepage Redis Widget Handler
|
Homepage Redis Widget Handler
|
||||||
"""
|
"""
|
||||||
# Measure response time w/ test lyric search
|
# Measure response time w/ test lyric search
|
||||||
time_start: float = time.time() # Start time for response_time
|
time_start: float = time.time() # Start time for response_time
|
||||||
test_lyrics_result = await self.redis_client.ft().search("@artist: test @song: test")
|
test_lyrics_result = await self.redis_client.ft().search(
|
||||||
|
"@artist: test @song: test"
|
||||||
|
)
|
||||||
time_end: float = time.time()
|
time_end: float = time.time()
|
||||||
# End response time test
|
# End response time test
|
||||||
total_keys = await self.redis_client.dbsize()
|
total_keys = await self.redis_client.dbsize()
|
||||||
response_time: float = time_end - time_start
|
response_time: float = time_end - time_start
|
||||||
(_, ci_keys) = await self.redis_client.scan(cursor=0, match="ci_session*", count=10000000)
|
(_, ci_keys) = await self.redis_client.scan(
|
||||||
|
cursor=0, match="ci_session*", count=10000000
|
||||||
|
)
|
||||||
num_ci_keys = len(ci_keys)
|
num_ci_keys = len(ci_keys)
|
||||||
index_info = await self.redis_client.ft().info()
|
index_info = await self.redis_client.ft().info()
|
||||||
indexed_lyrics: int = index_info.get('num_docs')
|
indexed_lyrics: int = index_info.get("num_docs")
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'responseTime': round(response_time, 7),
|
content={
|
||||||
'storedKeys': total_keys,
|
"responseTime": round(response_time, 7),
|
||||||
'indexedLyrics': indexed_lyrics,
|
"storedKeys": total_keys,
|
||||||
'sessions': num_ci_keys,
|
"indexedLyrics": indexed_lyrics,
|
||||||
})
|
"sessions": num_ci_keys,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def homepage_sqlite_widget(self) -> JSONResponse:
|
async def homepage_sqlite_widget(self) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Homepage SQLite Widget Handler
|
Homepage SQLite Widget Handler
|
||||||
"""
|
"""
|
||||||
row_count: int = await self.lyr_cache.sqlite_rowcount()
|
row_count: int = await self.lyr_cache.sqlite_rowcount()
|
||||||
distinct_artists: int = await self.lyr_cache.sqlite_distinct("artist")
|
distinct_artists: int = await self.lyr_cache.sqlite_distinct("artist")
|
||||||
lyrics_length: int = await self.lyr_cache.sqlite_lyrics_length()
|
lyrics_length: int = await self.lyr_cache.sqlite_lyrics_length()
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'storedRows': row_count,
|
content={
|
||||||
'distinctArtists': distinct_artists,
|
"storedRows": row_count,
|
||||||
'lyricsLength': lyrics_length,
|
"distinctArtists": distinct_artists,
|
||||||
})
|
"lyricsLength": lyrics_length,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def homepage_lyrics_widget(self) -> JSONResponse:
|
async def homepage_lyrics_widget(self) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Homepage Lyrics Widget Handler
|
Homepage Lyrics Widget Handler
|
||||||
"""
|
"""
|
||||||
found_counts: Optional[dict] = await self.redis_cache.get_found_counts()
|
found_counts: Optional[dict] = await self.redis_cache.get_found_counts()
|
||||||
if not isinstance(found_counts, dict):
|
if not isinstance(found_counts, dict):
|
||||||
logging.info("DEBUG: Type of found counts from redis: %s\nContents: %s",
|
logging.info(
|
||||||
type(found_counts), found_counts)
|
"DEBUG: Type of found counts from redis: %s\nContents: %s",
|
||||||
return JSONResponse(status_code=500, content={
|
type(found_counts),
|
||||||
'err': True,
|
found_counts,
|
||||||
'errorText': 'General failure.',
|
)
|
||||||
})
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={
|
||||||
|
"err": True,
|
||||||
|
"errorText": "General failure.",
|
||||||
|
},
|
||||||
|
)
|
||||||
return JSONResponse(content=found_counts)
|
return JSONResponse(content=found_counts)
|
||||||
|
|
||||||
async def homepage_radio_widget(self) -> JSONResponse:
|
async def homepage_radio_widget(self) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Homepage Radio Widget Handler
|
Homepage Radio Widget Handler
|
||||||
"""
|
"""
|
||||||
radio_np: tuple = await self.get_radio_np()
|
radio_np: tuple = await self.get_radio_np()
|
||||||
if not radio_np:
|
if not radio_np:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'General failure.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "General failure.",
|
||||||
|
},
|
||||||
|
)
|
||||||
(artistsong, album, genre) = radio_np
|
(artistsong, album, genre) = radio_np
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'now_playing': artistsong,
|
content={
|
||||||
'album': album,
|
"now_playing": artistsong,
|
||||||
'genre': genre,
|
"album": album,
|
||||||
})
|
"genre": genre,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -4,21 +4,26 @@ import time
|
|||||||
import random
|
import random
|
||||||
import asyncio
|
import asyncio
|
||||||
from utils import radio_util
|
from utils import radio_util
|
||||||
from .constructors import (ValidRadioNextRequest, ValidRadioReshuffleRequest,
|
from .constructors import (
|
||||||
ValidRadioQueueShiftRequest, ValidRadioQueueRemovalRequest,
|
ValidRadioNextRequest,
|
||||||
ValidRadioSongRequest, ValidRadioTypeaheadRequest,
|
ValidRadioReshuffleRequest,
|
||||||
RadioException)
|
ValidRadioQueueShiftRequest,
|
||||||
|
ValidRadioQueueRemovalRequest,
|
||||||
|
ValidRadioSongRequest,
|
||||||
|
ValidRadioTypeaheadRequest,
|
||||||
|
RadioException,
|
||||||
|
)
|
||||||
|
|
||||||
from uuid import uuid4 as uuid
|
from uuid import uuid4 as uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi import (FastAPI, BackgroundTasks, Request,
|
from fastapi import FastAPI, BackgroundTasks, Request, Response, HTTPException
|
||||||
Response, HTTPException)
|
|
||||||
from fastapi.responses import RedirectResponse, JSONResponse
|
from fastapi.responses import RedirectResponse, JSONResponse
|
||||||
|
|
||||||
|
|
||||||
class Radio(FastAPI):
|
class Radio(FastAPI):
|
||||||
"""Radio Endpoints"""
|
"""Radio Endpoints"""
|
||||||
def __init__(self, app: FastAPI,
|
|
||||||
my_util, constants) -> None:
|
def __init__(self, app: FastAPI, my_util, constants) -> None:
|
||||||
self.app: FastAPI = app
|
self.app: FastAPI = app
|
||||||
self.util = my_util
|
self.util = my_util
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
@ -33,22 +38,28 @@ class Radio(FastAPI):
|
|||||||
"radio/queue_shift": self.radio_queue_shift,
|
"radio/queue_shift": self.radio_queue_shift,
|
||||||
"radio/reshuffle": self.radio_reshuffle,
|
"radio/reshuffle": self.radio_reshuffle,
|
||||||
"radio/queue_remove": self.radio_queue_remove,
|
"radio/queue_remove": self.radio_queue_remove,
|
||||||
"radio/ls._next_": self.radio_get_next,
|
"radio/ls._next_": self.radio_get_next,
|
||||||
}
|
}
|
||||||
|
|
||||||
for endpoint, handler in self.endpoints.items():
|
for endpoint, handler in self.endpoints.items():
|
||||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
|
app.add_api_route(
|
||||||
include_in_schema=True)
|
f"/{endpoint}", handler, methods=["POST"], include_in_schema=True
|
||||||
|
)
|
||||||
# NOTE: Not in loop because method is GET for this endpoint
|
|
||||||
app.add_api_route("/radio/album_art", self.album_art_handler, methods=["GET"],
|
# NOTE: Not in loop because method is GET for this endpoint
|
||||||
include_in_schema=True)
|
app.add_api_route(
|
||||||
|
"/radio/album_art",
|
||||||
|
self.album_art_handler,
|
||||||
|
methods=["GET"],
|
||||||
|
include_in_schema=True,
|
||||||
|
)
|
||||||
|
|
||||||
asyncio.get_event_loop().run_until_complete(self.radio_util.load_playlist())
|
asyncio.get_event_loop().run_until_complete(self.radio_util.load_playlist())
|
||||||
asyncio.get_event_loop().run_until_complete(self.radio_util._ls_skip())
|
asyncio.get_event_loop().run_until_complete(self.radio_util._ls_skip())
|
||||||
|
|
||||||
async def radio_skip(self, data: ValidRadioNextRequest,
|
async def radio_skip(
|
||||||
request: Request) -> JSONResponse:
|
self, data: ValidRadioNextRequest, request: Request
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Skip to the next track in the queue, or to uuid specified in skipTo if provided
|
Skip to the next track in the queue, or to uuid specified in skipTo if provided
|
||||||
- **key**: API key
|
- **key**: API key
|
||||||
@ -58,45 +69,54 @@ class Radio(FastAPI):
|
|||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
||||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||||
if data.skipTo:
|
if data.skipTo:
|
||||||
queue_item = self.radio_util.get_queue_item_by_uuid(data.skipTo)
|
queue_item = self.radio_util.get_queue_item_by_uuid(data.skipTo)
|
||||||
if not queue_item:
|
if not queue_item:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'No such queue item.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
self.radio_util.active_playlist = self.radio_util.active_playlist[queue_item[0]:]
|
"errorText": "No such queue item.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.radio_util.active_playlist = self.radio_util.active_playlist[
|
||||||
|
queue_item[0] :
|
||||||
|
]
|
||||||
if not self.radio_util.active_playlist:
|
if not self.radio_util.active_playlist:
|
||||||
await self.radio_util.load_playlist()
|
await self.radio_util.load_playlist()
|
||||||
skip_result: bool = await self.radio_util._ls_skip()
|
skip_result: bool = await self.radio_util._ls_skip()
|
||||||
status_code = 200 if skip_result else 500
|
status_code = 200 if skip_result else 500
|
||||||
return JSONResponse(status_code=status_code, content={
|
return JSONResponse(
|
||||||
'success': skip_result,
|
status_code=status_code,
|
||||||
})
|
content={
|
||||||
|
"success": skip_result,
|
||||||
|
},
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'General failure.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "General failure.",
|
||||||
|
},
|
||||||
async def radio_reshuffle(self, data: ValidRadioReshuffleRequest,
|
)
|
||||||
request: Request) -> JSONResponse:
|
|
||||||
|
async def radio_reshuffle(
|
||||||
|
self, data: ValidRadioReshuffleRequest, request: Request
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Reshuffle the play queue
|
Reshuffle the play queue
|
||||||
- **key**: API key
|
- **key**: API key
|
||||||
"""
|
"""
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
||||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||||
|
|
||||||
random.shuffle(self.radio_util.active_playlist)
|
random.shuffle(self.radio_util.active_playlist)
|
||||||
return JSONResponse(content={
|
return JSONResponse(content={"ok": True})
|
||||||
'ok': True
|
|
||||||
})
|
async def radio_get_queue(
|
||||||
|
self, request: Request, limit: Optional[int] = 15_000
|
||||||
|
) -> JSONResponse:
|
||||||
async def radio_get_queue(self, request: Request,
|
|
||||||
limit: Optional[int] = 15_000) -> JSONResponse:
|
|
||||||
"""
|
"""
|
||||||
Get current play queue, up to limit [default: 15k]
|
Get current play queue, up to limit [default: 15k]
|
||||||
- **limit**: Number of queue items to return, default 15k
|
- **limit**: Number of queue items to return, default 15k
|
||||||
@ -104,23 +124,24 @@ class Radio(FastAPI):
|
|||||||
queue: list = self.radio_util.active_playlist[0:limit]
|
queue: list = self.radio_util.active_playlist[0:limit]
|
||||||
queue_out: list[dict] = []
|
queue_out: list[dict] = []
|
||||||
for x, item in enumerate(queue):
|
for x, item in enumerate(queue):
|
||||||
queue_out.append({
|
queue_out.append(
|
||||||
'pos': x,
|
{
|
||||||
'id': item.get('id'),
|
"pos": x,
|
||||||
'uuid': item.get('uuid'),
|
"id": item.get("id"),
|
||||||
'artist': item.get('artist'),
|
"uuid": item.get("uuid"),
|
||||||
'song': item.get('song'),
|
"artist": item.get("artist"),
|
||||||
'album': item.get('album', 'N/A'),
|
"song": item.get("song"),
|
||||||
'genre': item.get('genre', 'N/A'),
|
"album": item.get("album", "N/A"),
|
||||||
'artistsong': item.get('artistsong'),
|
"genre": item.get("genre", "N/A"),
|
||||||
'duration': item.get('duration'),
|
"artistsong": item.get("artistsong"),
|
||||||
})
|
"duration": item.get("duration"),
|
||||||
return JSONResponse(content={
|
}
|
||||||
'items': queue_out
|
)
|
||||||
})
|
return JSONResponse(content={"items": queue_out})
|
||||||
|
|
||||||
async def radio_queue_shift(self, data: ValidRadioQueueShiftRequest,
|
async def radio_queue_shift(
|
||||||
request: Request) -> JSONResponse:
|
self, data: ValidRadioQueueShiftRequest, request: Request
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Shift position of a UUID within the queue
|
Shift position of a UUID within the queue
|
||||||
[currently limited to playing next or immediately]
|
[currently limited to playing next or immediately]
|
||||||
@ -130,24 +151,30 @@ class Radio(FastAPI):
|
|||||||
"""
|
"""
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
||||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||||
|
|
||||||
queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid)
|
queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid)
|
||||||
if not queue_item:
|
if not queue_item:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Queue item not found.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Queue item not found.",
|
||||||
|
},
|
||||||
|
)
|
||||||
(x, item) = queue_item
|
(x, item) = queue_item
|
||||||
self.radio_util.active_playlist.pop(x)
|
self.radio_util.active_playlist.pop(x)
|
||||||
self.radio_util.active_playlist.insert(0, item)
|
self.radio_util.active_playlist.insert(0, item)
|
||||||
if not data.next:
|
if not data.next:
|
||||||
await self.radio_util._ls_skip()
|
await self.radio_util._ls_skip()
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'ok': True,
|
content={
|
||||||
})
|
"ok": True,
|
||||||
|
}
|
||||||
async def radio_queue_remove(self, data: ValidRadioQueueRemovalRequest,
|
)
|
||||||
request: Request) -> JSONResponse:
|
|
||||||
|
async def radio_queue_remove(
|
||||||
|
self, data: ValidRadioQueueRemovalRequest, request: Request
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Remove an item from the current play queue
|
Remove an item from the current play queue
|
||||||
- **key**: API key
|
- **key**: API key
|
||||||
@ -155,19 +182,26 @@ class Radio(FastAPI):
|
|||||||
"""
|
"""
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
||||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||||
|
|
||||||
queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid)
|
queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid)
|
||||||
if not queue_item:
|
if not queue_item:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Queue item not found.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Queue item not found.",
|
||||||
|
},
|
||||||
|
)
|
||||||
self.radio_util.active_playlist.pop(queue_item[0])
|
self.radio_util.active_playlist.pop(queue_item[0])
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'ok': True,
|
content={
|
||||||
})
|
"ok": True,
|
||||||
|
}
|
||||||
async def album_art_handler(self, request: Request, track_id: Optional[int] = None) -> Response:
|
)
|
||||||
|
|
||||||
|
async def album_art_handler(
|
||||||
|
self, request: Request, track_id: Optional[int] = None
|
||||||
|
) -> Response:
|
||||||
"""
|
"""
|
||||||
Get album art, optional parameter track_id may be specified.
|
Get album art, optional parameter track_id may be specified.
|
||||||
Otherwise, current track album art will be pulled.
|
Otherwise, current track album art will be pulled.
|
||||||
@ -175,35 +209,42 @@ class Radio(FastAPI):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not track_id:
|
if not track_id:
|
||||||
track_id = self.radio_util.now_playing.get('id')
|
track_id = self.radio_util.now_playing.get("id")
|
||||||
logging.debug("Seeking album art with trackId: %s", track_id)
|
logging.debug("Seeking album art with trackId: %s", track_id)
|
||||||
album_art: Optional[bytes] = await self.radio_util.get_album_art(track_id=track_id)
|
album_art: Optional[bytes] = await self.radio_util.get_album_art(
|
||||||
|
track_id=track_id
|
||||||
|
)
|
||||||
if not album_art:
|
if not album_art:
|
||||||
return RedirectResponse(url="https://codey.lol/images/radio_art_default.jpg",
|
return RedirectResponse(
|
||||||
status_code=302)
|
url="https://codey.lol/images/radio_art_default.jpg",
|
||||||
return Response(content=album_art,
|
status_code=302,
|
||||||
media_type="image/png")
|
)
|
||||||
|
return Response(content=album_art, media_type="image/png")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return RedirectResponse(url="https://codey.lol/images/radio_art_default.jpg",
|
return RedirectResponse(
|
||||||
status_code=302)
|
url="https://codey.lol/images/radio_art_default.jpg", status_code=302
|
||||||
|
)
|
||||||
|
|
||||||
async def radio_now_playing(self, request: Request) -> JSONResponse:
|
async def radio_now_playing(self, request: Request) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get currently playing track info
|
Get currently playing track info
|
||||||
"""
|
"""
|
||||||
ret_obj: dict = {**self.radio_util.now_playing}
|
ret_obj: dict = {**self.radio_util.now_playing}
|
||||||
try:
|
try:
|
||||||
ret_obj['elapsed'] = int(time.time()) - ret_obj['start']
|
ret_obj["elapsed"] = int(time.time()) - ret_obj["start"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
ret_obj['elapsed'] = 0
|
ret_obj["elapsed"] = 0
|
||||||
ret_obj.pop('file_path')
|
ret_obj.pop("file_path")
|
||||||
return JSONResponse(content=ret_obj)
|
return JSONResponse(content=ret_obj)
|
||||||
|
|
||||||
|
async def radio_get_next(
|
||||||
async def radio_get_next(self, data: ValidRadioNextRequest, request: Request,
|
self,
|
||||||
background_tasks: BackgroundTasks) -> JSONResponse:
|
data: ValidRadioNextRequest,
|
||||||
|
request: Request,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get next track
|
Get next track
|
||||||
Track will be removed from the queue in the process.
|
Track will be removed from the queue in the process.
|
||||||
@ -211,54 +252,65 @@ class Radio(FastAPI):
|
|||||||
- **skipTo**: Optional UUID to skip to
|
- **skipTo**: Optional UUID to skip to
|
||||||
"""
|
"""
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
||||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||||
if not isinstance(self.radio_util.active_playlist, list) or not self.radio_util.active_playlist:
|
if (
|
||||||
|
not isinstance(self.radio_util.active_playlist, list)
|
||||||
|
or not self.radio_util.active_playlist
|
||||||
|
):
|
||||||
await self.radio_util.load_playlist()
|
await self.radio_util.load_playlist()
|
||||||
await self.radio_util._ls_skip()
|
await self.radio_util._ls_skip()
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'General failure occurred, prompting playlist reload.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "General failure occurred, prompting playlist reload.",
|
||||||
|
},
|
||||||
|
)
|
||||||
next = self.radio_util.active_playlist.pop(0)
|
next = self.radio_util.active_playlist.pop(0)
|
||||||
if not isinstance(next, dict):
|
if not isinstance(next, dict):
|
||||||
logging.critical("next is of type: %s, reloading playlist...", type(next))
|
logging.critical("next is of type: %s, reloading playlist...", type(next))
|
||||||
await self.radio_util.load_playlist()
|
await self.radio_util.load_playlist()
|
||||||
await self.radio_util._ls_skip()
|
await self.radio_util._ls_skip()
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'General failure occurred, prompting playlist reload.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "General failure occurred, prompting playlist reload.",
|
||||||
duration: int = next['duration']
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
duration: int = next["duration"]
|
||||||
time_started: int = int(time.time())
|
time_started: int = int(time.time())
|
||||||
time_ends: int = int(time_started + duration)
|
time_ends: int = int(time_started + duration)
|
||||||
|
|
||||||
if len(self.radio_util.active_playlist) > 1:
|
if len(self.radio_util.active_playlist) > 1:
|
||||||
self.radio_util.active_playlist.append(next) # Push to end of playlist
|
self.radio_util.active_playlist.append(next) # Push to end of playlist
|
||||||
else:
|
else:
|
||||||
await self.radio_util.load_playlist()
|
await self.radio_util.load_playlist()
|
||||||
|
|
||||||
self.radio_util.now_playing = next
|
self.radio_util.now_playing = next
|
||||||
next['start'] = time_started
|
next["start"] = time_started
|
||||||
next['end'] = time_ends
|
next["end"] = time_ends
|
||||||
try:
|
try:
|
||||||
background_tasks.add_task(self.radio_util.webhook_song_change, next)
|
background_tasks.add_task(self.radio_util.webhook_song_change, next)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
try:
|
try:
|
||||||
if not await self.radio_util.get_album_art(file_path=next['file_path']):
|
if not await self.radio_util.get_album_art(file_path=next["file_path"]):
|
||||||
album_art = await self.radio_util.get_album_art(file_path=next['file_path'])
|
album_art = await self.radio_util.get_album_art(
|
||||||
|
file_path=next["file_path"]
|
||||||
|
)
|
||||||
if album_art:
|
if album_art:
|
||||||
await self.radio_util.cache_album_art(next['id'], album_art)
|
await self.radio_util.cache_album_art(next["id"], album_art)
|
||||||
else:
|
else:
|
||||||
logging.debug("Could not read album art for %s",
|
logging.debug("Could not read album art for %s", next["file_path"])
|
||||||
next['file_path'])
|
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return JSONResponse(content=next)
|
return JSONResponse(content=next)
|
||||||
|
|
||||||
|
async def radio_request(
|
||||||
async def radio_request(self, data: ValidRadioSongRequest, request: Request) -> JSONResponse:
|
self, data: ValidRadioSongRequest, request: Request
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Song request handler
|
Song request handler
|
||||||
- **key**: API key
|
- **key**: API key
|
||||||
@ -268,42 +320,52 @@ class Radio(FastAPI):
|
|||||||
- **alsoSkip**: If True, skips to the track; otherwise, track will be placed next up in queue
|
- **alsoSkip**: If True, skips to the track; otherwise, track will be placed next up in queue
|
||||||
"""
|
"""
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
||||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||||
artistsong: Optional[str] = data.artistsong
|
artistsong: Optional[str] = data.artistsong
|
||||||
artist: Optional[str] = data.artist
|
artist: Optional[str] = data.artist
|
||||||
song: Optional[str] = data.song
|
song: Optional[str] = data.song
|
||||||
if artistsong and (artist or song):
|
if artistsong and (artist or song):
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Invalid request",
|
||||||
|
},
|
||||||
|
)
|
||||||
if not artistsong and (not artist or not song):
|
if not artistsong and (not artist or not song):
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Invalid request",
|
||||||
search: bool = await self.radio_util.search_playlist(artistsong=artistsong,
|
},
|
||||||
artist=artist,
|
)
|
||||||
song=song)
|
|
||||||
|
search: bool = await self.radio_util.search_playlist(
|
||||||
|
artistsong=artistsong, artist=artist, song=song
|
||||||
|
)
|
||||||
if data.alsoSkip:
|
if data.alsoSkip:
|
||||||
await self.radio_util._ls_skip()
|
await self.radio_util._ls_skip()
|
||||||
return JSONResponse(content={
|
return JSONResponse(content={"result": search})
|
||||||
'result': search
|
|
||||||
})
|
async def radio_typeahead(
|
||||||
|
self, data: ValidRadioTypeaheadRequest, request: Request
|
||||||
async def radio_typeahead(self, data: ValidRadioTypeaheadRequest,
|
) -> JSONResponse:
|
||||||
request: Request) -> JSONResponse:
|
|
||||||
"""
|
"""
|
||||||
Radio typeahead handler
|
Radio typeahead handler
|
||||||
- **query**: Typeahead query
|
- **query**: Typeahead query
|
||||||
"""
|
"""
|
||||||
if not isinstance(data.query, str):
|
if not isinstance(data.query, str):
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
typeahead: Optional[list[str]] = await self.radio_util.trackdb_typeahead(data.query)
|
"errorText": "Invalid request.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
typeahead: Optional[list[str]] = await self.radio_util.trackdb_typeahead(
|
||||||
|
data.query
|
||||||
|
)
|
||||||
if not typeahead:
|
if not typeahead:
|
||||||
return JSONResponse(content=[])
|
return JSONResponse(content=[])
|
||||||
return JSONResponse(content=typeahead)
|
return JSONResponse(content=typeahead)
|
||||||
|
@ -6,20 +6,25 @@ from fastapi import FastAPI
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from .constructors import RandMsgRequest
|
from .constructors import RandMsgRequest
|
||||||
|
|
||||||
|
|
||||||
class RandMsg(FastAPI):
|
class RandMsg(FastAPI):
|
||||||
"""
|
"""
|
||||||
Random Message Endpoint
|
Random Message Endpoint
|
||||||
"""
|
"""
|
||||||
def __init__(self, app: FastAPI,
|
|
||||||
util, constants) -> None:
|
def __init__(self, app: FastAPI, util, constants) -> None:
|
||||||
self.app: FastAPI = app
|
self.app: FastAPI = app
|
||||||
self.util = util
|
self.util = util
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
self.endpoint_name = "randmsg"
|
self.endpoint_name = "randmsg"
|
||||||
|
|
||||||
app.add_api_route(f"/{self.endpoint_name}", self.randmsg_handler, methods=["POST"])
|
app.add_api_route(
|
||||||
|
f"/{self.endpoint_name}", self.randmsg_handler, methods=["POST"]
|
||||||
async def randmsg_handler(self, data: Optional[RandMsgRequest] = None) -> JSONResponse:
|
)
|
||||||
|
|
||||||
|
async def randmsg_handler(
|
||||||
|
self, data: Optional[RandMsgRequest] = None
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get a randomly generated message
|
Get a randomly generated message
|
||||||
- **short**: Optional, if True, will limit length of returned random messages to <=126 characters (Discord restriction related)
|
- **short**: Optional, if True, will limit length of returned random messages to <=126 characters (Discord restriction related)
|
||||||
@ -35,58 +40,61 @@ class RandMsg(FastAPI):
|
|||||||
|
|
||||||
match db_rand_selected:
|
match db_rand_selected:
|
||||||
case 0:
|
case 0:
|
||||||
randmsg_db_path: Union[str, LiteralString] = os.path.join("/usr/local/share",
|
randmsg_db_path: Union[str, LiteralString] = os.path.join(
|
||||||
"sqlite_dbs", "qajoke.db") # For qajoke db
|
"/usr/local/share", "sqlite_dbs", "qajoke.db"
|
||||||
db_query: str = "SELECT id, ('<b>Q:</b> ' || question || '<br/><b>A:</b> ' \
|
) # For qajoke db
|
||||||
|| answer) FROM jokes ORDER BY RANDOM() LIMIT 1" # For qajoke db
|
db_query: str = (
|
||||||
|
"SELECT id, ('<b>Q:</b> ' || question || '<br/><b>A:</b> ' \
|
||||||
|
|| answer) FROM jokes ORDER BY RANDOM() LIMIT 1" # For qajoke db
|
||||||
|
)
|
||||||
title_attr = "QA Joke DB"
|
title_attr = "QA Joke DB"
|
||||||
case 1 | 9:
|
case 1 | 9:
|
||||||
randmsg_db_path = os.path.join("/usr/local/share",
|
randmsg_db_path = os.path.join(
|
||||||
"sqlite_dbs",
|
"/usr/local/share", "sqlite_dbs", "randmsg.db"
|
||||||
"randmsg.db") # For randmsg db
|
) # For randmsg db
|
||||||
db_query = "SELECT id, msg FROM msgs WHERE \
|
db_query = "SELECT id, msg FROM msgs WHERE \
|
||||||
LENGTH(msg) <= 180 ORDER BY RANDOM() LIMIT 1" # For randmsg db
|
LENGTH(msg) <= 180 ORDER BY RANDOM() LIMIT 1" # For randmsg db
|
||||||
if db_rand_selected == 9:
|
if db_rand_selected == 9:
|
||||||
db_query = db_query.replace("<= 180", "<= 126")
|
db_query = db_query.replace("<= 180", "<= 126")
|
||||||
title_attr = "Random Msg DB"
|
title_attr = "Random Msg DB"
|
||||||
case 2:
|
case 2:
|
||||||
randmsg_db_path = os.path.join("/usr/local/share",
|
randmsg_db_path = os.path.join(
|
||||||
"sqlite_dbs",
|
"/usr/local/share", "sqlite_dbs", "trump.db"
|
||||||
"trump.db") # For Trump Tweet DB
|
) # For Trump Tweet DB
|
||||||
db_query = "SELECT id, content FROM tweets \
|
db_query = "SELECT id, content FROM tweets \
|
||||||
ORDER BY RANDOM() LIMIT 1" # For Trump Tweet DB
|
ORDER BY RANDOM() LIMIT 1" # For Trump Tweet DB
|
||||||
title_attr = "Trump Tweet DB"
|
title_attr = "Trump Tweet DB"
|
||||||
case 3:
|
case 3:
|
||||||
randmsg_db_path = os.path.join("/usr/local/share",
|
randmsg_db_path = os.path.join(
|
||||||
"sqlite_dbs",
|
"/usr/local/share", "sqlite_dbs", "philo.db"
|
||||||
"philo.db") # For Philo DB
|
) # For Philo DB
|
||||||
db_query = "SELECT id, (content || '<br> - ' || speaker) FROM quotes \
|
db_query = "SELECT id, (content || '<br> - ' || speaker) FROM quotes \
|
||||||
ORDER BY RANDOM() LIMIT 1" # For Philo DB
|
ORDER BY RANDOM() LIMIT 1" # For Philo DB
|
||||||
title_attr = "Philosophical Quotes DB"
|
title_attr = "Philosophical Quotes DB"
|
||||||
case 4:
|
case 4:
|
||||||
randmsg_db_path = os.path.join("/usr/local/share",
|
randmsg_db_path = os.path.join(
|
||||||
"sqlite_dbs",
|
"/usr/local/share", "sqlite_dbs", "hate.db"
|
||||||
"hate.db") # For Hate DB
|
) # For Hate DB
|
||||||
db_query = """SELECT id, ("<font color='#FF0000'>" || comment) FROM hate_speech \
|
db_query = """SELECT id, ("<font color='#FF0000'>" || comment) FROM hate_speech \
|
||||||
WHERE length(comment) <= 180 ORDER BY RANDOM() LIMIT 1"""
|
WHERE length(comment) <= 180 ORDER BY RANDOM() LIMIT 1"""
|
||||||
title_attr = "Hate Speech DB"
|
title_attr = "Hate Speech DB"
|
||||||
case 5:
|
case 5:
|
||||||
randmsg_db_path = os.path.join("/usr/local/share",
|
randmsg_db_path = os.path.join(
|
||||||
"sqlite_dbs",
|
"/usr/local/share", "sqlite_dbs", "rjokes.db"
|
||||||
"rjokes.db") # r/jokes DB
|
) # r/jokes DB
|
||||||
db_query = """SELECT id, (title || "<br>" || body) FROM jokes \
|
db_query = """SELECT id, (title || "<br>" || body) FROM jokes \
|
||||||
WHERE score >= 10000 ORDER BY RANDOM() LIMIT 1"""
|
WHERE score >= 10000 ORDER BY RANDOM() LIMIT 1"""
|
||||||
title_attr = "r/jokes DB"
|
title_attr = "r/jokes DB"
|
||||||
|
|
||||||
async with sqlite3.connect(database=randmsg_db_path, timeout=1) as _db:
|
async with sqlite3.connect(database=randmsg_db_path, timeout=1) as _db:
|
||||||
async with await _db.execute(db_query) as _cursor:
|
async with await _db.execute(db_query) as _cursor:
|
||||||
result: sqlite3.Row = await _cursor.fetchone()
|
result: sqlite3.Row = await _cursor.fetchone()
|
||||||
(result_id, result_msg) = result
|
(result_id, result_msg) = result
|
||||||
result_msg = result_msg.strip()
|
result_msg = result_msg.strip()
|
||||||
return JSONResponse(content=
|
return JSONResponse(
|
||||||
{
|
content={
|
||||||
"id": result_id,
|
"id": result_id,
|
||||||
"msg": result_msg,
|
"msg": result_msg,
|
||||||
"title": title_attr,
|
"title": title_attr,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
@ -5,10 +5,12 @@ from fastapi.responses import JSONResponse
|
|||||||
from typing import Optional, LiteralString, Union
|
from typing import Optional, LiteralString, Union
|
||||||
from .constructors import ValidShowEpisodeLineRequest, ValidShowEpisodeListRequest
|
from .constructors import ValidShowEpisodeLineRequest, ValidShowEpisodeListRequest
|
||||||
|
|
||||||
|
|
||||||
class Transcriptions(FastAPI):
|
class Transcriptions(FastAPI):
|
||||||
"""
|
"""
|
||||||
Transcription Endpoints
|
Transcription Endpoints
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, app: FastAPI, util, constants) -> None:
|
def __init__(self, app: FastAPI, util, constants) -> None:
|
||||||
self.app: FastAPI = app
|
self.app: FastAPI = app
|
||||||
self.util = util
|
self.util = util
|
||||||
@ -17,14 +19,17 @@ class Transcriptions(FastAPI):
|
|||||||
self.endpoints: dict = {
|
self.endpoints: dict = {
|
||||||
"transcriptions/get_episodes": self.get_episodes_handler,
|
"transcriptions/get_episodes": self.get_episodes_handler,
|
||||||
"transcriptions/get_episode_lines": self.get_episode_lines_handler,
|
"transcriptions/get_episode_lines": self.get_episode_lines_handler,
|
||||||
#tbd
|
# tbd
|
||||||
}
|
}
|
||||||
|
|
||||||
for endpoint, handler in self.endpoints.items():
|
for endpoint, handler in self.endpoints.items():
|
||||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
|
app.add_api_route(
|
||||||
include_in_schema=True)
|
f"/{endpoint}", handler, methods=["POST"], include_in_schema=True
|
||||||
|
)
|
||||||
async def get_episodes_handler(self, data: ValidShowEpisodeListRequest) -> JSONResponse:
|
|
||||||
|
async def get_episodes_handler(
|
||||||
|
self, data: ValidShowEpisodeListRequest
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get list of episodes by show id
|
Get list of episodes by show id
|
||||||
- **s**: Show ID to query
|
- **s**: Show ID to query
|
||||||
@ -33,56 +38,68 @@ class Transcriptions(FastAPI):
|
|||||||
db_path: Optional[Union[str, LiteralString]] = None
|
db_path: Optional[Union[str, LiteralString]] = None
|
||||||
db_query: Optional[str] = None
|
db_query: Optional[str] = None
|
||||||
show_title: Optional[str] = None
|
show_title: Optional[str] = None
|
||||||
|
|
||||||
if not isinstance(show_id, int):
|
if not isinstance(show_id, int):
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Invalid request',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Invalid request",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
show_id = int(show_id)
|
show_id = int(show_id)
|
||||||
|
|
||||||
if not(str(show_id).isnumeric()) or show_id not in [0, 1, 2]:
|
if not (str(show_id).isnumeric()) or show_id not in [0, 1, 2]:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Show not found.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Show not found.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
match show_id:
|
match show_id:
|
||||||
case 0:
|
case 0:
|
||||||
db_path = os.path.join("/usr/local/share",
|
db_path = os.path.join("/usr/local/share", "sqlite_dbs", "sp.db")
|
||||||
"sqlite_dbs", "sp.db")
|
|
||||||
db_query = """SELECT DISTINCT(("S" || Season || "E" || Episode || " " || Title)), ID FROM SP_DAT ORDER BY Season, Episode"""
|
db_query = """SELECT DISTINCT(("S" || Season || "E" || Episode || " " || Title)), ID FROM SP_DAT ORDER BY Season, Episode"""
|
||||||
show_title = "South Park"
|
show_title = "South Park"
|
||||||
case 1:
|
case 1:
|
||||||
db_path = os.path.join("/usr/local/share",
|
db_path = os.path.join("/usr/local/share", "sqlite_dbs", "futur.db")
|
||||||
"sqlite_dbs", "futur.db")
|
|
||||||
db_query = """SELECT DISTINCT(("S" || EP_S || "E" || EP_EP || " " || EP_TITLE)), EP_ID FROM clean_dialog ORDER BY EP_S, EP_EP"""
|
db_query = """SELECT DISTINCT(("S" || EP_S || "E" || EP_EP || " " || EP_TITLE)), EP_ID FROM clean_dialog ORDER BY EP_S, EP_EP"""
|
||||||
show_title = "Futurama"
|
show_title = "Futurama"
|
||||||
case 2:
|
case 2:
|
||||||
db_path = os.path.join("/usr/local/share",
|
db_path = os.path.join("/usr/local/share", "sqlite_dbs", "parks.db")
|
||||||
"sqlite_dbs", "parks.db")
|
|
||||||
db_query = """SELECT DISTINCT(("S" || EP_S || "E" || EP_EP || " " || EP_TITLE)), EP_ID FROM clean_dialog ORDER BY EP_S, EP_EP"""
|
db_query = """SELECT DISTINCT(("S" || EP_S || "E" || EP_EP || " " || EP_TITLE)), EP_ID FROM clean_dialog ORDER BY EP_S, EP_EP"""
|
||||||
show_title = "Parks And Rec"
|
show_title = "Parks And Rec"
|
||||||
case _:
|
case _:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Unknown error.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Unknown error.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async with sqlite3.connect(database=db_path, timeout=1) as _db:
|
async with sqlite3.connect(database=db_path, timeout=1) as _db:
|
||||||
async with await _db.execute(db_query) as _cursor:
|
async with await _db.execute(db_query) as _cursor:
|
||||||
result: list[tuple] = await _cursor.fetchall()
|
result: list[tuple] = await _cursor.fetchall()
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
"show_title": show_title,
|
content={
|
||||||
"episodes": [
|
"show_title": show_title,
|
||||||
{
|
"episodes": [
|
||||||
'id': item[1],
|
{
|
||||||
'ep_friendly': item[0],
|
"id": item[1],
|
||||||
} for item in result],
|
"ep_friendly": item[0],
|
||||||
})
|
}
|
||||||
|
for item in result
|
||||||
async def get_episode_lines_handler(self, data: ValidShowEpisodeLineRequest) -> JSONResponse:
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_episode_lines_handler(
|
||||||
|
self, data: ValidShowEpisodeLineRequest
|
||||||
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get lines for a particular episode
|
Get lines for a particular episode
|
||||||
- **s**: Show ID to query
|
- **s**: Show ID to query
|
||||||
@ -90,39 +107,46 @@ class Transcriptions(FastAPI):
|
|||||||
"""
|
"""
|
||||||
show_id: int = int(data.s)
|
show_id: int = int(data.s)
|
||||||
episode_id: int = int(data.e)
|
episode_id: int = int(data.e)
|
||||||
|
|
||||||
match show_id:
|
match show_id:
|
||||||
case 0:
|
case 0:
|
||||||
db_path: Union[str, LiteralString] = os.path.join("/usr/local/share",
|
db_path: Union[str, LiteralString] = os.path.join(
|
||||||
"sqlite_dbs", "sp.db")
|
"/usr/local/share", "sqlite_dbs", "sp.db"
|
||||||
db_query: str = """SELECT ("S" || Season || "E" || Episode || " " || Title), Character, Line FROM SP_DAT WHERE ID = ?"""
|
)
|
||||||
|
db_query: str = (
|
||||||
|
"""SELECT ("S" || Season || "E" || Episode || " " || Title), Character, Line FROM SP_DAT WHERE ID = ?"""
|
||||||
|
)
|
||||||
case 1:
|
case 1:
|
||||||
db_path = os.path.join("/usr/local/share",
|
db_path = os.path.join("/usr/local/share", "sqlite_dbs", "futur.db")
|
||||||
"sqlite_dbs", "futur.db")
|
|
||||||
db_query = """SELECT ("S" || EP_S || "E" || EP_EP || " " || EP_TITLE || "<br><em>Opener: " || EP_OPENER || "</em>"), EP_LINE_SPEAKER, EP_LINE FROM clean_dialog WHERE EP_ID = ? ORDER BY LINE_ID ASC"""
|
db_query = """SELECT ("S" || EP_S || "E" || EP_EP || " " || EP_TITLE || "<br><em>Opener: " || EP_OPENER || "</em>"), EP_LINE_SPEAKER, EP_LINE FROM clean_dialog WHERE EP_ID = ? ORDER BY LINE_ID ASC"""
|
||||||
case 2:
|
case 2:
|
||||||
db_path = os.path.join("/usr/local/share",
|
db_path = os.path.join("/usr/local/share", "sqlite_dbs", "parks.db")
|
||||||
"sqlite_dbs", "parks.db")
|
|
||||||
db_query = """SELECT ("S" || EP_S || "E" || EP_EP || " " || EP_TITLE), EP_LINE_SPEAKER, EP_LINE FROM clean_dialog WHERE EP_ID = ? ORDER BY id ASC"""
|
db_query = """SELECT ("S" || EP_S || "E" || EP_EP || " " || EP_TITLE), EP_LINE_SPEAKER, EP_LINE FROM clean_dialog WHERE EP_ID = ? ORDER BY id ASC"""
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
return JSONResponse(status_code=500, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=500,
|
||||||
'errorText': 'Unknown error',
|
content={
|
||||||
})
|
"err": True,
|
||||||
|
"errorText": "Unknown error",
|
||||||
async with sqlite3.connect(database=db_path,
|
},
|
||||||
timeout=1) as _db:
|
)
|
||||||
|
|
||||||
|
async with sqlite3.connect(database=db_path, timeout=1) as _db:
|
||||||
params: tuple = (episode_id,)
|
params: tuple = (episode_id,)
|
||||||
async with await _db.execute(db_query, params) as _cursor:
|
async with await _db.execute(db_query, params) as _cursor:
|
||||||
result: list[tuple] = await _cursor.fetchall()
|
result: list[tuple] = await _cursor.fetchall()
|
||||||
first_result: tuple = result[0]
|
first_result: tuple = result[0]
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'episode_id': episode_id,
|
content={
|
||||||
'ep_friendly': first_result[0].strip(),
|
"episode_id": episode_id,
|
||||||
'lines': [
|
"ep_friendly": first_result[0].strip(),
|
||||||
{
|
"lines": [
|
||||||
'speaker': item[1].strip(),
|
{
|
||||||
'line': item[2].strip(),
|
"speaker": item[1].strip(),
|
||||||
} for item in result],
|
"line": item[2].strip(),
|
||||||
})
|
}
|
||||||
|
for item in result
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -4,12 +4,13 @@ from fastapi.responses import JSONResponse
|
|||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from .constructors import ValidYTSearchRequest
|
from .constructors import ValidYTSearchRequest
|
||||||
|
|
||||||
|
|
||||||
class YT(FastAPI):
|
class YT(FastAPI):
|
||||||
"""
|
"""
|
||||||
YT Endpoints
|
YT Endpoints
|
||||||
"""
|
"""
|
||||||
def __init__(self, app: FastAPI, util,
|
|
||||||
constants) -> None:
|
def __init__(self, app: FastAPI, util, constants) -> None:
|
||||||
self.app: FastAPI = app
|
self.app: FastAPI = app
|
||||||
self.util = util
|
self.util = util
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
@ -17,28 +18,34 @@ class YT(FastAPI):
|
|||||||
|
|
||||||
self.endpoints: dict = {
|
self.endpoints: dict = {
|
||||||
"yt/search": self.yt_video_search_handler,
|
"yt/search": self.yt_video_search_handler,
|
||||||
}
|
}
|
||||||
|
|
||||||
for endpoint, handler in self.endpoints.items():
|
for endpoint, handler in self.endpoints.items():
|
||||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
|
app.add_api_route(
|
||||||
include_in_schema=True)
|
f"/{endpoint}", handler, methods=["POST"], include_in_schema=True
|
||||||
|
)
|
||||||
|
|
||||||
async def yt_video_search_handler(self, data: ValidYTSearchRequest) -> JSONResponse:
|
async def yt_video_search_handler(self, data: ValidYTSearchRequest) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Search for YT Video by Title (closest match returned)
|
Search for YT Video by Title (closest match returned)
|
||||||
- **t**: Title to search
|
- **t**: Title to search
|
||||||
"""
|
"""
|
||||||
|
|
||||||
title: str = data.t
|
title: str = data.t
|
||||||
yts_res: Optional[list[dict]] = await self.ytsearch.search(title)
|
yts_res: Optional[list[dict]] = await self.ytsearch.search(title)
|
||||||
if not yts_res:
|
if not yts_res:
|
||||||
return JSONResponse(status_code=404, content={
|
return JSONResponse(
|
||||||
'err': True,
|
status_code=404,
|
||||||
'errorText': 'No result.',
|
content={
|
||||||
})
|
"err": True,
|
||||||
yt_video_id: Union[str, bool] = yts_res[0].get('id', False)
|
"errorText": "No result.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
yt_video_id: Union[str, bool] = yts_res[0].get("id", False)
|
||||||
|
|
||||||
return JSONResponse(content={
|
return JSONResponse(
|
||||||
'video_id': yt_video_id,
|
content={
|
||||||
'extras': yts_res[0],
|
"video_id": yt_video_id,
|
||||||
})
|
"extras": yts_res[0],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
|
|
||||||
|
|
||||||
class GPT:
|
class GPT:
|
||||||
def __init__(self, constants) -> None:
|
def __init__(self, constants) -> None:
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
@ -12,8 +13,9 @@ class GPT:
|
|||||||
self.default_system_prompt: str = """You are a helpful assistant who will provide only totally accurate tidbits of \
|
self.default_system_prompt: str = """You are a helpful assistant who will provide only totally accurate tidbits of \
|
||||||
info on the specific songs the user may listen to."""
|
info on the specific songs the user may listen to."""
|
||||||
|
|
||||||
async def get_completion(self, prompt: str,
|
async def get_completion(
|
||||||
system_prompt: Optional[str] = None) -> Optional[str]:
|
self, prompt: str, system_prompt: Optional[str] = None
|
||||||
|
) -> Optional[str]:
|
||||||
if not system_prompt:
|
if not system_prompt:
|
||||||
system_prompt = self.default_system_prompt
|
system_prompt = self.default_system_prompt
|
||||||
chat_completion = await self.client.chat.completions.create(
|
chat_completion = await self.client.chat.completions.create(
|
||||||
@ -25,10 +27,10 @@ class GPT:
|
|||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": prompt,
|
"content": prompt,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
model="gpt-4o-mini",
|
model="gpt-4o-mini",
|
||||||
temperature=0.35,
|
temperature=0.35,
|
||||||
)
|
)
|
||||||
response: Optional[str] = chat_completion.choices[0].message.content
|
response: Optional[str] = chat_completion.choices[0].message.content
|
||||||
return response
|
return response
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LyricsResult:
|
class LyricsResult:
|
||||||
"""
|
"""
|
||||||
@ -12,31 +13,37 @@ class LyricsResult:
|
|||||||
lyrics (Union[str, list]): str if plain lyrics, list for lrc
|
lyrics (Union[str, list]): str if plain lyrics, list for lrc
|
||||||
time (float): time taken to retrieve lyrics from source
|
time (float): time taken to retrieve lyrics from source
|
||||||
"""
|
"""
|
||||||
|
|
||||||
artist: str
|
artist: str
|
||||||
song: str
|
song: str
|
||||||
src: str
|
src: str
|
||||||
lyrics: Union[str, list]
|
lyrics: Union[str, list]
|
||||||
confidence: int
|
confidence: int
|
||||||
time: float = 0.00
|
time: float = 0.00
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Generic
|
Generic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class InvalidLyricSearchResponseException(Exception):
|
class InvalidLyricSearchResponseException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Genius
|
Genius
|
||||||
"""
|
"""
|
||||||
class InvalidGeniusResponseException(
|
|
||||||
InvalidLyricSearchResponseException):
|
|
||||||
|
class InvalidGeniusResponseException(InvalidLyricSearchResponseException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
LRCLib
|
LRCLib
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class InvalidLRCLibResponseException(
|
|
||||||
InvalidLyricSearchResponseException):
|
class InvalidLRCLibResponseException(InvalidLyricSearchResponseException):
|
||||||
pass
|
pass
|
||||||
|
@ -4,23 +4,26 @@ from lyric_search import notifier
|
|||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
sys.path.insert(1,'..')
|
|
||||||
|
sys.path.insert(1, "..")
|
||||||
from . import cache, redis_cache, genius, lrclib
|
from . import cache, redis_cache, genius, lrclib
|
||||||
|
|
||||||
|
|
||||||
class Aggregate:
|
class Aggregate:
|
||||||
"""
|
"""
|
||||||
Aggregate all source methods
|
Aggregate all source methods
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, exclude_methods=None) -> None:
|
def __init__(self, exclude_methods=None) -> None:
|
||||||
if not exclude_methods:
|
if not exclude_methods:
|
||||||
exclude_methods: list = []
|
exclude_methods: list = []
|
||||||
self.exclude_methods = exclude_methods
|
self.exclude_methods = exclude_methods
|
||||||
self.redis_cache = redis_cache.RedisCache()
|
self.redis_cache = redis_cache.RedisCache()
|
||||||
self.notifier = notifier.DiscordNotifier()
|
self.notifier = notifier.DiscordNotifier()
|
||||||
|
|
||||||
async def search(self, artist: str, song: str,
|
async def search(
|
||||||
plain: Optional[bool] = True) -> Optional[LyricsResult]:
|
self, artist: str, song: str, plain: Optional[bool] = True
|
||||||
|
) -> Optional[LyricsResult]:
|
||||||
"""
|
"""
|
||||||
Aggregate Search
|
Aggregate Search
|
||||||
Args:
|
Args:
|
||||||
@ -41,37 +44,41 @@ class Aggregate:
|
|||||||
cache_search,
|
cache_search,
|
||||||
lrclib_search,
|
lrclib_search,
|
||||||
genius_search,
|
genius_search,
|
||||||
]
|
]
|
||||||
if not plain:
|
if not plain:
|
||||||
sources = [lrclib_search] # Only LRCLib supported for synced lyrics
|
sources = [lrclib_search] # Only LRCLib supported for synced lyrics
|
||||||
search_result: Optional[LyricsResult] = None
|
search_result: Optional[LyricsResult] = None
|
||||||
for source in sources:
|
for source in sources:
|
||||||
if source.label.lower() in self.exclude_methods:
|
if source.label.lower() in self.exclude_methods:
|
||||||
if not plain:
|
if not plain:
|
||||||
logging.info("Exclude conditions rejected - source requested to exclude: %s, plain: %s",
|
logging.info(
|
||||||
source.label, plain)
|
"Exclude conditions rejected - source requested to exclude: %s, plain: %s",
|
||||||
|
source.label,
|
||||||
|
plain,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if plain:
|
if plain:
|
||||||
logging.info("Skipping source: %s, excluded.", source.label)
|
logging.info("Skipping source: %s, excluded.", source.label)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
search_result = await source.search(artist=artist, song=song,
|
search_result = await source.search(artist=artist, song=song, plain=plain)
|
||||||
plain=plain)
|
|
||||||
if search_result:
|
if search_result:
|
||||||
break
|
break
|
||||||
logging.info("%s: NOT FOUND!", source.label)
|
logging.info("%s: NOT FOUND!", source.label)
|
||||||
if not search_result:
|
if not search_result:
|
||||||
logging.info("%s - %s: all sources exhausted, not found.",
|
logging.info("%s - %s: all sources exhausted, not found.", artist, song)
|
||||||
artist, song)
|
if plain: # do not record LRC fails
|
||||||
if plain: # do not record LRC fails
|
try:
|
||||||
try:
|
|
||||||
await self.redis_cache.increment_found_count("failed")
|
await self.redis_cache.increment_found_count("failed")
|
||||||
self.notifier.send("WARNING",
|
self.notifier.send(
|
||||||
f"Could not find {artist} - {song} via queried sources.")
|
"WARNING",
|
||||||
|
f"Could not find {artist} - {song} via queried sources.",
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
logging.info("Could not increment redis failed counter: %s",
|
logging.info("Could not increment redis failed counter: %s", str(e))
|
||||||
str(e))
|
self.notifier.send(
|
||||||
self.notifier.send(f"ERROR @ {__file__.rsplit("/", maxsplit=1)[-1]}",
|
f"ERROR @ {__file__.rsplit("/", maxsplit=1)[-1]}",
|
||||||
f"Could not increment redis failed counter: {str(e)}")
|
f"Could not increment redis failed counter: {str(e)}",
|
||||||
return search_result
|
)
|
||||||
|
return search_result
|
||||||
|
@ -4,8 +4,9 @@ import regex
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
sys.path.insert(1,'..')
|
|
||||||
sys.path.insert(1,'.')
|
sys.path.insert(1, "..")
|
||||||
|
sys.path.insert(1, ".")
|
||||||
from typing import Optional, Union, LiteralString
|
from typing import Optional, Union, LiteralString
|
||||||
import aiosqlite as sqlite3
|
import aiosqlite as sqlite3
|
||||||
from . import redis_cache
|
from . import redis_cache
|
||||||
@ -15,27 +16,38 @@ from lyric_search.constructors import LyricsResult
|
|||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
log_level = logging.getLevelName(logger.level)
|
log_level = logging.getLevelName(logger.level)
|
||||||
|
|
||||||
|
|
||||||
class Cache:
|
class Cache:
|
||||||
"""Cache Search Module"""
|
"""Cache Search Module"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.cache_db: Union[str, LiteralString] = os.path.join("/", "usr", "local", "share",
|
self.cache_db: Union[str, LiteralString] = os.path.join(
|
||||||
"sqlite_dbs", "cached_lyrics.db")
|
"/", "usr", "local", "share", "sqlite_dbs", "cached_lyrics.db"
|
||||||
|
)
|
||||||
self.redis_cache = redis_cache.RedisCache()
|
self.redis_cache = redis_cache.RedisCache()
|
||||||
self.notifier = notifier.DiscordNotifier()
|
self.notifier = notifier.DiscordNotifier()
|
||||||
|
|
||||||
self.cache_pre_query: str = "pragma journal_mode = WAL; pragma synchronous = normal;\
|
self.cache_pre_query: str = (
|
||||||
|
"pragma journal_mode = WAL; pragma synchronous = normal;\
|
||||||
pragma temp_store = memory; pragma mmap_size = 30000000000;"
|
pragma temp_store = memory; pragma mmap_size = 30000000000;"
|
||||||
self.sqlite_exts: list[str] = ['/home/api/api/solibs/spellfix1.cpython-311-x86_64-linux-gnu.so']
|
)
|
||||||
|
self.sqlite_exts: list[str] = [
|
||||||
|
"/home/api/api/solibs/spellfix1.cpython-311-x86_64-linux-gnu.so"
|
||||||
|
]
|
||||||
self.label: str = "Cache"
|
self.label: str = "Cache"
|
||||||
|
|
||||||
def get_matched(self, matched_candidate: tuple, confidence: int,
|
def get_matched(
|
||||||
sqlite_rows: Optional[list[sqlite3.Row]] = None,
|
self,
|
||||||
redis_results: Optional[list] = None) -> Optional[LyricsResult]:
|
matched_candidate: tuple,
|
||||||
|
confidence: int,
|
||||||
|
sqlite_rows: Optional[list[sqlite3.Row]] = None,
|
||||||
|
redis_results: Optional[list] = None,
|
||||||
|
) -> Optional[LyricsResult]:
|
||||||
"""
|
"""
|
||||||
Get Matched Result
|
Get Matched Result
|
||||||
Args:
|
Args:
|
||||||
matched_candidate (tuple): the correctly matched candidate returned by matcher.best_match
|
matched_candidate (tuple): the correctly matched candidate returned by matcher.best_match
|
||||||
confidence (int): % confidence
|
confidence (int): % confidence
|
||||||
sqlite_rows (Optional[list[sqlite3.Row]]): List of returned rows from SQLite DB, or None if Redis
|
sqlite_rows (Optional[list[sqlite3.Row]]): List of returned rows from SQLite DB, or None if Redis
|
||||||
redis_results (Any): List of Redis returned data, or None if SQLite
|
redis_results (Any): List of Redis returned data, or None if SQLite
|
||||||
Returns:
|
Returns:
|
||||||
@ -47,11 +59,11 @@ class Cache:
|
|||||||
(key, row) = res
|
(key, row) = res
|
||||||
if key == matched_id:
|
if key == matched_id:
|
||||||
return LyricsResult(
|
return LyricsResult(
|
||||||
artist=row['artist'],
|
artist=row["artist"],
|
||||||
song=row['song'],
|
song=row["song"],
|
||||||
lyrics=row['lyrics'],
|
lyrics=row["lyrics"],
|
||||||
src=f"{row['src']} (redis cache, id: {key})",
|
src=f"{row['src']} (redis cache, id: {key})",
|
||||||
confidence=row['confidence']
|
confidence=row["confidence"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for row in sqlite_rows:
|
for row in sqlite_rows:
|
||||||
@ -62,9 +74,10 @@ class Cache:
|
|||||||
song=song,
|
song=song,
|
||||||
lyrics=lyrics,
|
lyrics=lyrics,
|
||||||
src=f"{original_src} (cached, id: {_id})",
|
src=f"{original_src} (cached, id: {_id})",
|
||||||
confidence=confidence)
|
confidence=confidence,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def check_existence(self, artistsong: str) -> Optional[bool]:
|
async def check_existence(self, artistsong: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
Check whether lyrics are already stored for track
|
Check whether lyrics are already stored for track
|
||||||
@ -73,10 +86,13 @@ class Cache:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: Whether track was found in cache
|
bool: Whether track was found in cache
|
||||||
"""
|
"""
|
||||||
logging.debug("Checking whether %s is already stored",
|
logging.debug(
|
||||||
artistsong.replace("\n", " - "))
|
"Checking whether %s is already stored", artistsong.replace("\n", " - ")
|
||||||
check_query: str = 'SELECT id, artist, song FROM lyrics WHERE editdist3((lower(artist) || " " || lower(song)), (? || " " || ?))\
|
)
|
||||||
|
check_query: str = (
|
||||||
|
'SELECT id, artist, song FROM lyrics WHERE editdist3((lower(artist) || " " || lower(song)), (? || " " || ?))\
|
||||||
<= 410 ORDER BY editdist3((lower(artist) || " " || lower(song)), ?) ASC LIMIT 1'
|
<= 410 ORDER BY editdist3((lower(artist) || " " || lower(song)), ?) ASC LIMIT 1'
|
||||||
|
)
|
||||||
artistsong_split = artistsong.split("\n", maxsplit=1)
|
artistsong_split = artistsong.split("\n", maxsplit=1)
|
||||||
artist = artistsong_split[0].lower()
|
artist = artistsong_split[0].lower()
|
||||||
song = artistsong_split[1].lower()
|
song = artistsong_split[1].lower()
|
||||||
@ -84,39 +100,45 @@ class Cache:
|
|||||||
async with sqlite3.connect(self.cache_db, timeout=2) as db_conn:
|
async with sqlite3.connect(self.cache_db, timeout=2) as db_conn:
|
||||||
await db_conn.enable_load_extension(True)
|
await db_conn.enable_load_extension(True)
|
||||||
for ext in self.sqlite_exts:
|
for ext in self.sqlite_exts:
|
||||||
await db_conn.load_extension(ext)
|
await db_conn.load_extension(ext)
|
||||||
async with await db_conn.executescript(self.cache_pre_query) as _db_cursor:
|
async with await db_conn.executescript(self.cache_pre_query) as _db_cursor:
|
||||||
async with await db_conn.execute(check_query, params) as db_cursor:
|
async with await db_conn.execute(check_query, params) as db_cursor:
|
||||||
result = await db_cursor.fetchone()
|
result = await db_cursor.fetchone()
|
||||||
if result:
|
if result:
|
||||||
logging.debug("%s is already stored.",
|
logging.debug(
|
||||||
artistsong.replace("\n", " - "))
|
"%s is already stored.", artistsong.replace("\n", " - ")
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
logging.debug("%s cleared to be stored.",
|
logging.debug("%s cleared to be stored.", artistsong)
|
||||||
artistsong)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def store(self, lyr_result: LyricsResult) -> None:
|
async def store(self, lyr_result: LyricsResult) -> None:
|
||||||
"""
|
"""
|
||||||
Store lyrics (SQLite, then Redis)
|
Store lyrics (SQLite, then Redis)
|
||||||
Args:
|
Args:
|
||||||
lyr_result (LyricsResult): the returned lyrics to cache
|
lyr_result (LyricsResult): the returned lyrics to cache
|
||||||
Returns: None
|
Returns: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sqlite_insert_id = await self.sqlite_store(lyr_result)
|
sqlite_insert_id = await self.sqlite_store(lyr_result)
|
||||||
if sqlite_insert_id:
|
if sqlite_insert_id:
|
||||||
await self.redis_cache.redis_store(sqlite_insert_id, lyr_result)
|
await self.redis_cache.redis_store(sqlite_insert_id, lyr_result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
logging.error("ERROR @ %s: %s",
|
logging.error(
|
||||||
__file__.rsplit("/", maxsplit=1)[-1], f"cache::store >> {str(e)}")
|
"ERROR @ %s: %s",
|
||||||
await self.notifier.send(f"ERROR @ {__file__.rsplit("/", maxsplit=1)[-1]}",
|
__file__.rsplit("/", maxsplit=1)[-1],
|
||||||
f"cache::store >> `{str(e)}`")
|
f"cache::store >> {str(e)}",
|
||||||
|
)
|
||||||
async def sqlite_rowcount(self, where: Optional[str] = None,
|
await self.notifier.send(
|
||||||
params: Optional[tuple] = None) -> int:
|
f"ERROR @ {__file__.rsplit("/", maxsplit=1)[-1]}",
|
||||||
|
f"cache::store >> `{str(e)}`",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def sqlite_rowcount(
|
||||||
|
self, where: Optional[str] = None, params: Optional[tuple] = None
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Get rowcount for cached_lyrics DB
|
Get rowcount for cached_lyrics DB
|
||||||
Args:
|
Args:
|
||||||
@ -130,8 +152,8 @@ class Cache:
|
|||||||
query = f"SELECT count(id) AS rowcount FROM lyrics {where}".strip()
|
query = f"SELECT count(id) AS rowcount FROM lyrics {where}".strip()
|
||||||
async with await db_conn.execute(query, params) as db_cursor:
|
async with await db_conn.execute(query, params) as db_cursor:
|
||||||
result = await db_cursor.fetchone()
|
result = await db_cursor.fetchone()
|
||||||
return result['rowcount']
|
return result["rowcount"]
|
||||||
|
|
||||||
async def sqlite_distinct(self, column: str) -> int:
|
async def sqlite_distinct(self, column: str) -> int:
|
||||||
"""
|
"""
|
||||||
Get count of distinct values for a column
|
Get count of distinct values for a column
|
||||||
@ -145,8 +167,8 @@ class Cache:
|
|||||||
query = f"SELECT COUNT(DISTINCT {column}) as distinct_items FROM lyrics"
|
query = f"SELECT COUNT(DISTINCT {column}) as distinct_items FROM lyrics"
|
||||||
async with await db_conn.execute(query) as db_cursor:
|
async with await db_conn.execute(query) as db_cursor:
|
||||||
result = await db_cursor.fetchone()
|
result = await db_cursor.fetchone()
|
||||||
return result['distinct_items']
|
return result["distinct_items"]
|
||||||
|
|
||||||
async def sqlite_lyrics_length(self) -> int:
|
async def sqlite_lyrics_length(self) -> int:
|
||||||
"""
|
"""
|
||||||
Get total length of text stored for lyrics
|
Get total length of text stored for lyrics
|
||||||
@ -160,9 +182,8 @@ class Cache:
|
|||||||
query = "SELECT SUM(LENGTH(lyrics)) as lyrics_len FROM lyrics"
|
query = "SELECT SUM(LENGTH(lyrics)) as lyrics_len FROM lyrics"
|
||||||
async with await db_conn.execute(query) as db_cursor:
|
async with await db_conn.execute(query) as db_cursor:
|
||||||
result = await db_cursor.fetchone()
|
result = await db_cursor.fetchone()
|
||||||
return result['lyrics_len']
|
return result["lyrics_len"]
|
||||||
|
|
||||||
|
|
||||||
async def sqlite_store(self, lyr_result: LyricsResult) -> int:
|
async def sqlite_store(self, lyr_result: LyricsResult) -> int:
|
||||||
"""
|
"""
|
||||||
Store lyrics to SQLite Cache
|
Store lyrics to SQLite Cache
|
||||||
@ -172,30 +193,42 @@ class Cache:
|
|||||||
int: the inserted row id
|
int: the inserted row id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logging.info("Storing %s",
|
logging.info("Storing %s", f"{lyr_result.artist} - {lyr_result.song}")
|
||||||
f"{lyr_result.artist} - {lyr_result.song}")
|
|
||||||
|
|
||||||
if lyr_result.src.lower() == "cache":
|
if lyr_result.src.lower() == "cache":
|
||||||
logging.info("Skipping cache storage - returned LyricsResult originated from cache")
|
logging.info(
|
||||||
|
"Skipping cache storage - returned LyricsResult originated from cache"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
artistsong = f"{lyr_result.artist}\n{lyr_result.song}"
|
artistsong = f"{lyr_result.artist}\n{lyr_result.song}"
|
||||||
if await self.check_existence(artistsong):
|
if await self.check_existence(artistsong):
|
||||||
logging.info("Skipping cache storage - %s is already stored.",
|
logging.info(
|
||||||
artistsong.replace("\n", " - "))
|
"Skipping cache storage - %s is already stored.",
|
||||||
|
artistsong.replace("\n", " - "),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
lyrics = regex.sub(r'(<br>|\n|\r\n)', ' / ', lyr_result.lyrics.strip())
|
lyrics = regex.sub(r"(<br>|\n|\r\n)", " / ", lyr_result.lyrics.strip())
|
||||||
lyrics = regex.sub(r'\s{2,}', ' ', lyrics)
|
lyrics = regex.sub(r"\s{2,}", " ", lyrics)
|
||||||
|
|
||||||
insert_query = "INSERT INTO lyrics (src, date_retrieved, artist, song, artistsong, confidence, lyrics)\
|
insert_query = "INSERT INTO lyrics (src, date_retrieved, artist, song, artistsong, confidence, lyrics)\
|
||||||
VALUES(?, ?, ?, ?, ?, ?, ?)"
|
VALUES(?, ?, ?, ?, ?, ?, ?)"
|
||||||
params = (lyr_result.src, time.time(), lyr_result.artist,
|
params = (
|
||||||
lyr_result.song, artistsong, lyr_result.confidence, lyrics)
|
lyr_result.src,
|
||||||
|
time.time(),
|
||||||
|
lyr_result.artist,
|
||||||
|
lyr_result.song,
|
||||||
|
artistsong,
|
||||||
|
lyr_result.confidence,
|
||||||
|
lyrics,
|
||||||
|
)
|
||||||
|
|
||||||
async with sqlite3.connect(self.cache_db, timeout=2) as db_conn:
|
async with sqlite3.connect(self.cache_db, timeout=2) as db_conn:
|
||||||
async with await db_conn.executescript(self.cache_pre_query) as _db_cursor:
|
async with await db_conn.executescript(
|
||||||
|
self.cache_pre_query
|
||||||
|
) as _db_cursor:
|
||||||
async with await db_conn.execute(insert_query, params) as _cursor:
|
async with await db_conn.execute(insert_query, params) as _cursor:
|
||||||
await db_conn.commit()
|
await db_conn.commit()
|
||||||
logging.info("Stored %s to SQLite!", artistsong.replace("\n", " - "))
|
logging.info("Stored %s to SQLite!", artistsong.replace("\n", " - "))
|
||||||
@ -203,7 +236,7 @@ class Cache:
|
|||||||
except:
|
except:
|
||||||
logging.critical("Cache storage error!")
|
logging.critical("Cache storage error!")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def search(self, artist: str, song: str, **kwargs) -> Optional[LyricsResult]:
|
async def search(self, artist: str, song: str, **kwargs) -> Optional[LyricsResult]:
|
||||||
"""
|
"""
|
||||||
Cache Search
|
Cache Search
|
||||||
@ -214,8 +247,8 @@ class Cache:
|
|||||||
Optional[LyricsResult]: The result, if found - None otherwise.
|
Optional[LyricsResult]: The result, if found - None otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
artist: str = artist.strip().lower()
|
artist: str = artist.strip().lower()
|
||||||
song: str = song.strip().lower()
|
song: str = song.strip().lower()
|
||||||
input_track: str = f"{artist} - {song}"
|
input_track: str = f"{artist} - {song}"
|
||||||
search_query = None
|
search_query = None
|
||||||
search_params: Optional[tuple] = None
|
search_params: Optional[tuple] = None
|
||||||
@ -225,87 +258,105 @@ class Cache:
|
|||||||
|
|
||||||
if artist == "!" and song == "!":
|
if artist == "!" and song == "!":
|
||||||
random_search = True
|
random_search = True
|
||||||
search_query: str = 'SELECT id, artist, song, lyrics, src, confidence\
|
search_query: str = (
|
||||||
FROM lyrics ORDER BY RANDOM() LIMIT 1'
|
"SELECT id, artist, song, lyrics, src, confidence\
|
||||||
|
FROM lyrics ORDER BY RANDOM() LIMIT 1"
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info("Searching %s - %s on %s", artist, song, self.label)
|
||||||
|
|
||||||
logging.info("Searching %s - %s on %s",
|
|
||||||
artist, song, self.label)
|
|
||||||
|
|
||||||
"""Check Redis First"""
|
"""Check Redis First"""
|
||||||
|
|
||||||
logging.debug("Checking redis cache for %s...",
|
logging.debug("Checking redis cache for %s...", f"{artist} - {song}")
|
||||||
f"{artist} - {song}")
|
|
||||||
try:
|
try:
|
||||||
redis_result = await self.redis_cache.search(artist=artist,
|
redis_result = await self.redis_cache.search(artist=artist, song=song)
|
||||||
song=song)
|
|
||||||
|
|
||||||
if redis_result:
|
if redis_result:
|
||||||
result_tracks: list = []
|
result_tracks: list = []
|
||||||
for returned in redis_result:
|
for returned in redis_result:
|
||||||
(key, track) = returned
|
(key, track) = returned
|
||||||
result_tracks.append((key, f"{track['artist']} - {track['song']}"))
|
result_tracks.append(
|
||||||
|
(key, f"{track['artist']} - {track['song']}")
|
||||||
|
)
|
||||||
|
|
||||||
if not random_search:
|
if not random_search:
|
||||||
best_match: Optional[tuple] = matcher.find_best_match(input_track=input_track,
|
best_match: Optional[tuple] = matcher.find_best_match(
|
||||||
candidate_tracks=result_tracks)
|
input_track=input_track, candidate_tracks=result_tracks
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
best_match = (result_tracks[0], 100)
|
best_match = (result_tracks[0], 100)
|
||||||
|
|
||||||
|
|
||||||
if best_match:
|
if best_match:
|
||||||
(candidate, confidence) = best_match
|
(candidate, confidence) = best_match
|
||||||
matched = self.get_matched(redis_results=redis_result, matched_candidate=candidate,
|
matched = self.get_matched(
|
||||||
confidence=confidence)
|
redis_results=redis_result,
|
||||||
|
matched_candidate=candidate,
|
||||||
|
confidence=confidence,
|
||||||
|
)
|
||||||
|
|
||||||
if matched and confidence >= 90:
|
if matched and confidence >= 90:
|
||||||
time_end: float = time.time()
|
time_end: float = time.time()
|
||||||
time_diff: float = time_end - time_start
|
time_diff: float = time_end - time_start
|
||||||
matched.confidence = confidence
|
matched.confidence = confidence
|
||||||
matched.time = time_diff
|
matched.time = time_diff
|
||||||
|
|
||||||
logging.info("Found %s on redis cache, skipping SQLite...",
|
logging.info(
|
||||||
f"{artist} - {song}")
|
"Found %s on redis cache, skipping SQLite...",
|
||||||
await self.redis_cache.increment_found_count(self.label)
|
f"{artist} - {song}",
|
||||||
|
)
|
||||||
|
await self.redis_cache.increment_found_count(self.label)
|
||||||
return matched
|
return matched
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
"""SQLite: Fallback"""
|
"""SQLite: Fallback"""
|
||||||
|
|
||||||
async with sqlite3.connect(self.cache_db, timeout=2) as db_conn:
|
async with sqlite3.connect(self.cache_db, timeout=2) as db_conn:
|
||||||
await db_conn.enable_load_extension(True)
|
await db_conn.enable_load_extension(True)
|
||||||
for ext in self.sqlite_exts:
|
for ext in self.sqlite_exts:
|
||||||
await db_conn.load_extension(ext)
|
await db_conn.load_extension(ext)
|
||||||
async with await db_conn.executescript(self.cache_pre_query) as _db_cursor:
|
async with await db_conn.executescript(
|
||||||
|
self.cache_pre_query
|
||||||
|
) as _db_cursor:
|
||||||
if not random_search:
|
if not random_search:
|
||||||
search_query: str = 'SELECT id, artist, song, lyrics, src, confidence FROM lyrics\
|
search_query: str = (
|
||||||
|
'SELECT id, artist, song, lyrics, src, confidence FROM lyrics\
|
||||||
WHERE editdist3((lower(artist) || " " || lower(song)), (? || " " || ?))\
|
WHERE editdist3((lower(artist) || " " || lower(song)), (? || " " || ?))\
|
||||||
<= 410 ORDER BY editdist3((lower(artist) || " " || lower(song)), ?) ASC LIMIT 10'
|
<= 410 ORDER BY editdist3((lower(artist) || " " || lower(song)), ?) ASC LIMIT 10'
|
||||||
search_params: tuple = (artist.strip(), song.strip(),
|
)
|
||||||
f"{artist.strip()} {song.strip()}")
|
search_params: tuple = (
|
||||||
|
artist.strip(),
|
||||||
async with await _db_cursor.execute(search_query, search_params) as db_cursor:
|
song.strip(),
|
||||||
|
f"{artist.strip()} {song.strip()}",
|
||||||
|
)
|
||||||
|
|
||||||
|
async with await _db_cursor.execute(
|
||||||
|
search_query, search_params
|
||||||
|
) as db_cursor:
|
||||||
results: list = await db_cursor.fetchall()
|
results: list = await db_cursor.fetchall()
|
||||||
result_tracks: list = []
|
result_tracks: list = []
|
||||||
for track in results:
|
for track in results:
|
||||||
(_id, _artist, _song, _lyrics, _src, _confidence) = track
|
(_id, _artist, _song, _lyrics, _src, _confidence) = track
|
||||||
result_tracks.append((_id, f"{_artist} - {_song}"))
|
result_tracks.append((_id, f"{_artist} - {_song}"))
|
||||||
if not random_search:
|
if not random_search:
|
||||||
best_match: Optional[tuple] = matcher.find_best_match(input_track=input_track,
|
best_match: Optional[tuple] = matcher.find_best_match(
|
||||||
candidate_tracks=result_tracks)
|
input_track=input_track, candidate_tracks=result_tracks
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
best_match = (result_tracks[0], 100)
|
best_match = (result_tracks[0], 100)
|
||||||
if not best_match or confidence < 90:
|
if not best_match or confidence < 90:
|
||||||
return None
|
return None
|
||||||
(candidate, confidence) = best_match
|
(candidate, confidence) = best_match
|
||||||
logging.info("Result found on %s", self.label)
|
logging.info("Result found on %s", self.label)
|
||||||
matched = self.get_matched(sqlite_rows=results,
|
matched = self.get_matched(
|
||||||
matched_candidate=candidate,
|
sqlite_rows=results,
|
||||||
confidence=confidence)
|
matched_candidate=candidate,
|
||||||
|
confidence=confidence,
|
||||||
|
)
|
||||||
time_end: float = time.time()
|
time_end: float = time.time()
|
||||||
time_diff: float = time_end - time_start
|
time_diff: float = time_end - time_start
|
||||||
matched.time = time_diff
|
matched.time = time_diff
|
||||||
await self.redis_cache.increment_found_count(self.label)
|
await self.redis_cache.increment_found_count(self.label)
|
||||||
return matched
|
return matched
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
SCRAPE_HEADERS: dict[str, str] = {
|
SCRAPE_HEADERS: dict[str, str] = {
|
||||||
'accept': '*/*',
|
"accept": "*/*",
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0',
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0",
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,31 @@
|
|||||||
import sys
|
import sys
|
||||||
sys.path.insert(1,'..')
|
|
||||||
|
sys.path.insert(1, "..")
|
||||||
import traceback
|
import traceback
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from aiohttp import ClientTimeout, ClientSession
|
from aiohttp import ClientTimeout, ClientSession
|
||||||
from bs4 import BeautifulSoup, ResultSet # type: ignore
|
from bs4 import BeautifulSoup, ResultSet # type: ignore
|
||||||
import html as htm
|
import html as htm
|
||||||
from . import private, common, cache, redis_cache
|
from . import private, common, cache, redis_cache
|
||||||
from lyric_search import utils
|
from lyric_search import utils
|
||||||
from lyric_search.constructors import (
|
from lyric_search.constructors import LyricsResult, InvalidGeniusResponseException
|
||||||
LyricsResult, InvalidGeniusResponseException)
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
log_level = logging.getLevelName(logger.level)
|
log_level = logging.getLevelName(logger.level)
|
||||||
|
|
||||||
|
|
||||||
class Genius:
|
class Genius:
|
||||||
"""
|
"""
|
||||||
Genius Search Module
|
Genius Search Module
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.label: str = "Genius"
|
self.label: str = "Genius"
|
||||||
self.genius_url: str = private.GENIUS_URL
|
self.genius_url: str = private.GENIUS_URL
|
||||||
self.genius_search_url: str = f'{self.genius_url}api/search/song?q='
|
self.genius_search_url: str = f"{self.genius_url}api/search/song?q="
|
||||||
self.headers: dict = common.SCRAPE_HEADERS
|
self.headers: dict = common.SCRAPE_HEADERS
|
||||||
self.timeout = ClientTimeout(connect=3, sock_read=5)
|
self.timeout = ClientTimeout(connect=3, sock_read=5)
|
||||||
self.datautils = utils.DataUtils()
|
self.datautils = utils.DataUtils()
|
||||||
@ -31,8 +33,7 @@ class Genius:
|
|||||||
self.cache = cache.Cache()
|
self.cache = cache.Cache()
|
||||||
self.redis_cache = redis_cache.RedisCache()
|
self.redis_cache = redis_cache.RedisCache()
|
||||||
|
|
||||||
async def search(self, artist: str, song: str,
|
async def search(self, artist: str, song: str, **kwargs) -> Optional[LyricsResult]:
|
||||||
**kwargs) -> Optional[LyricsResult]:
|
|
||||||
"""
|
"""
|
||||||
Genius Search
|
Genius Search
|
||||||
Args:
|
Args:
|
||||||
@ -45,96 +46,125 @@ class Genius:
|
|||||||
artist: str = artist.strip().lower()
|
artist: str = artist.strip().lower()
|
||||||
song: str = song.strip().lower()
|
song: str = song.strip().lower()
|
||||||
time_start: float = time.time()
|
time_start: float = time.time()
|
||||||
logging.info("Searching %s - %s on %s",
|
logging.info("Searching %s - %s on %s", artist, song, self.label)
|
||||||
artist, song, self.label)
|
search_term: str = f"{artist}%20{song}"
|
||||||
search_term: str = f'{artist}%20{song}'
|
returned_lyrics: str = ""
|
||||||
returned_lyrics: str = ''
|
|
||||||
async with ClientSession() as client:
|
async with ClientSession() as client:
|
||||||
async with client.get(f'{self.genius_search_url}{search_term}',
|
async with client.get(
|
||||||
timeout=self.timeout,
|
f"{self.genius_search_url}{search_term}",
|
||||||
headers=self.headers) as request:
|
timeout=self.timeout,
|
||||||
|
headers=self.headers,
|
||||||
|
) as request:
|
||||||
request.raise_for_status()
|
request.raise_for_status()
|
||||||
text: Optional[str] = await request.text()
|
text: Optional[str] = await request.text()
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
raise InvalidGeniusResponseException("No search response.")
|
raise InvalidGeniusResponseException("No search response.")
|
||||||
|
|
||||||
if len(text) < 100:
|
if len(text) < 100:
|
||||||
raise InvalidGeniusResponseException("Search response text was invalid (len < 100 chars.)")
|
raise InvalidGeniusResponseException(
|
||||||
|
"Search response text was invalid (len < 100 chars.)"
|
||||||
|
)
|
||||||
search_data = await request.json()
|
search_data = await request.json()
|
||||||
|
|
||||||
if not isinstance(search_data, dict):
|
if not isinstance(search_data, dict):
|
||||||
raise InvalidGeniusResponseException("Invalid JSON.")
|
raise InvalidGeniusResponseException("Invalid JSON.")
|
||||||
|
|
||||||
if not isinstance(search_data['response'], dict):
|
if not isinstance(search_data["response"], dict):
|
||||||
raise InvalidGeniusResponseException(f"Invalid JSON: Cannot find response key.\n{search_data}")
|
raise InvalidGeniusResponseException(
|
||||||
|
f"Invalid JSON: Cannot find response key.\n{search_data}"
|
||||||
if not isinstance(search_data['response']['sections'], list):
|
)
|
||||||
raise InvalidGeniusResponseException(f"Invalid JSON: Cannot find response->sections key.\n{search_data}")
|
|
||||||
|
if not isinstance(search_data["response"]["sections"], list):
|
||||||
if not isinstance(search_data['response']['sections'][0]['hits'], list):
|
raise InvalidGeniusResponseException(
|
||||||
raise InvalidGeniusResponseException("Invalid JSON: Cannot find response->sections[0]->hits key.")
|
f"Invalid JSON: Cannot find response->sections key.\n{search_data}"
|
||||||
|
)
|
||||||
possible_matches: list = search_data['response']['sections'][0]['hits']
|
|
||||||
|
if not isinstance(
|
||||||
|
search_data["response"]["sections"][0]["hits"], list
|
||||||
|
):
|
||||||
|
raise InvalidGeniusResponseException(
|
||||||
|
"Invalid JSON: Cannot find response->sections[0]->hits key."
|
||||||
|
)
|
||||||
|
|
||||||
|
possible_matches: list = search_data["response"]["sections"][0][
|
||||||
|
"hits"
|
||||||
|
]
|
||||||
to_scrape: list[tuple] = [
|
to_scrape: list[tuple] = [
|
||||||
(
|
(
|
||||||
returned['result']['path'],
|
returned["result"]["path"],
|
||||||
f'{returned['result']['artist_names']} - {returned['result']['title']}',
|
f"{returned['result']['artist_names']} - {returned['result']['title']}",
|
||||||
) for returned in possible_matches
|
)
|
||||||
|
for returned in possible_matches
|
||||||
]
|
]
|
||||||
searched: str = f"{artist} - {song}"
|
searched: str = f"{artist} - {song}"
|
||||||
best_match: tuple = self.matcher.find_best_match(input_track=searched,
|
best_match: tuple = self.matcher.find_best_match(
|
||||||
candidate_tracks=to_scrape)
|
input_track=searched, candidate_tracks=to_scrape
|
||||||
|
)
|
||||||
((scrape_stub, track), confidence) = best_match
|
((scrape_stub, track), confidence) = best_match
|
||||||
scrape_url: str = f'{self.genius_url}{scrape_stub[1:]}'
|
scrape_url: str = f"{self.genius_url}{scrape_stub[1:]}"
|
||||||
|
|
||||||
async with client.get(scrape_url,
|
async with client.get(
|
||||||
timeout=self.timeout,
|
scrape_url, timeout=self.timeout, headers=self.headers
|
||||||
headers=self.headers) as scrape_request:
|
) as scrape_request:
|
||||||
scrape_request.raise_for_status()
|
scrape_request.raise_for_status()
|
||||||
scrape_text: Optional[str] = await scrape_request.text()
|
scrape_text: Optional[str] = await scrape_request.text()
|
||||||
|
|
||||||
if not scrape_text:
|
if not scrape_text:
|
||||||
raise InvalidGeniusResponseException("No scrape response.")
|
raise InvalidGeniusResponseException("No scrape response.")
|
||||||
|
|
||||||
if len(scrape_text) < 100:
|
if len(scrape_text) < 100:
|
||||||
raise InvalidGeniusResponseException("Scrape response was invalid (len < 100 chars.)")
|
raise InvalidGeniusResponseException(
|
||||||
|
"Scrape response was invalid (len < 100 chars.)"
|
||||||
|
)
|
||||||
html = BeautifulSoup(htm.unescape(scrape_text).replace('<br/>', '\n'), "html.parser")
|
|
||||||
|
html = BeautifulSoup(
|
||||||
header_tags_genius: Optional[ResultSet] = html.find_all(class_=re.compile(r'.*Header.*'))
|
htm.unescape(scrape_text).replace("<br/>", "\n"),
|
||||||
|
"html.parser",
|
||||||
|
)
|
||||||
|
|
||||||
|
header_tags_genius: Optional[ResultSet] = html.find_all(
|
||||||
|
class_=re.compile(r".*Header.*")
|
||||||
|
)
|
||||||
if header_tags_genius:
|
if header_tags_genius:
|
||||||
for tag in header_tags_genius:
|
for tag in header_tags_genius:
|
||||||
tag.extract()
|
tag.extract()
|
||||||
|
|
||||||
divs: Optional[ResultSet] = html.find_all("div", {"data-lyrics-container": "true"})
|
divs: Optional[ResultSet] = html.find_all(
|
||||||
|
"div", {"data-lyrics-container": "true"}
|
||||||
|
)
|
||||||
|
|
||||||
if not divs:
|
if not divs:
|
||||||
return
|
return
|
||||||
|
|
||||||
for div in divs:
|
for div in divs:
|
||||||
header_tags: Optional[ResultSet] = div.find_all(['h1', 'h2', 'h3', 'h4', 'h5'])
|
header_tags: Optional[ResultSet] = div.find_all(
|
||||||
|
["h1", "h2", "h3", "h4", "h5"]
|
||||||
|
)
|
||||||
if header_tags:
|
if header_tags:
|
||||||
for tag in header_tags:
|
for tag in header_tags:
|
||||||
tag.extract()
|
tag.extract()
|
||||||
|
|
||||||
returned_lyrics += div.get_text()
|
returned_lyrics += div.get_text()
|
||||||
|
|
||||||
returned_lyrics: str = self.datautils.scrub_lyrics(returned_lyrics)
|
returned_lyrics: str = self.datautils.scrub_lyrics(
|
||||||
|
returned_lyrics
|
||||||
|
)
|
||||||
artist: str = track.split(" - ", maxsplit=1)[0]
|
artist: str = track.split(" - ", maxsplit=1)[0]
|
||||||
song: str = track.split(" - ", maxsplit=1)[1]
|
song: str = track.split(" - ", maxsplit=1)[1]
|
||||||
logging.info("Result found on %s", self.label)
|
logging.info("Result found on %s", self.label)
|
||||||
time_end: float = time.time()
|
time_end: float = time.time()
|
||||||
time_diff: float = time_end - time_start
|
time_diff: float = time_end - time_start
|
||||||
matched = LyricsResult(artist=artist,
|
matched = LyricsResult(
|
||||||
song=song,
|
artist=artist,
|
||||||
src=self.label,
|
song=song,
|
||||||
lyrics=returned_lyrics,
|
src=self.label,
|
||||||
confidence=confidence,
|
lyrics=returned_lyrics,
|
||||||
time=time_diff)
|
confidence=confidence,
|
||||||
|
time=time_diff,
|
||||||
|
)
|
||||||
await self.redis_cache.increment_found_count(self.label)
|
await self.redis_cache.increment_found_count(self.label)
|
||||||
await self.cache.store(matched)
|
await self.cache.store(matched)
|
||||||
return matched
|
return matched
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
sys.path.insert(1,'..')
|
|
||||||
|
sys.path.insert(1, "..")
|
||||||
import traceback
|
import traceback
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
@ -13,20 +14,23 @@ from lyric_search.constructors import InvalidLRCLibResponseException
|
|||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
log_level = logging.getLevelName(logger.level)
|
log_level = logging.getLevelName(logger.level)
|
||||||
|
|
||||||
|
|
||||||
class LRCLib:
|
class LRCLib:
|
||||||
"""LRCLib Search Module"""
|
"""LRCLib Search Module"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.label: str = "LRCLib"
|
self.label: str = "LRCLib"
|
||||||
self.lrclib_url: str = "https://lrclib.net/api/search"
|
self.lrclib_url: str = "https://lrclib.net/api/search"
|
||||||
self.headers: dict = common.SCRAPE_HEADERS
|
self.headers: dict = common.SCRAPE_HEADERS
|
||||||
self.timeout = ClientTimeout(connect=2, sock_read=4)
|
self.timeout = ClientTimeout(connect=2, sock_read=4)
|
||||||
self.datautils = utils.DataUtils()
|
self.datautils = utils.DataUtils()
|
||||||
self.matcher = utils.TrackMatcher()
|
self.matcher = utils.TrackMatcher()
|
||||||
self.cache = cache.Cache()
|
self.cache = cache.Cache()
|
||||||
self.redis_cache = redis_cache.RedisCache()
|
self.redis_cache = redis_cache.RedisCache()
|
||||||
|
|
||||||
async def search(self, artist: str, song: str,
|
async def search(
|
||||||
plain: Optional[bool] = True) -> Optional[LyricsResult]:
|
self, artist: str, song: str, plain: Optional[bool] = True
|
||||||
|
) -> Optional[LyricsResult]:
|
||||||
"""
|
"""
|
||||||
LRCLib Search
|
LRCLib Search
|
||||||
Args:
|
Args:
|
||||||
@ -35,92 +39,124 @@ class LRCLib:
|
|||||||
Returns:
|
Returns:
|
||||||
Optional[LyricsResult]: The result, if found - None otherwise.
|
Optional[LyricsResult]: The result, if found - None otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
artist: str = artist.strip().lower()
|
artist: str = artist.strip().lower()
|
||||||
song: str = song.strip().lower()
|
song: str = song.strip().lower()
|
||||||
time_start: float = time.time()
|
time_start: float = time.time()
|
||||||
lrc_obj: Optional[list[dict]] = None
|
lrc_obj: Optional[list[dict]] = None
|
||||||
|
|
||||||
logging.info("Searching %s - %s on %s",
|
logging.info("Searching %s - %s on %s", artist, song, self.label)
|
||||||
artist, song, self.label)
|
|
||||||
|
input_track: str = f"{artist} - {song}"
|
||||||
input_track: str = f"{artist} - {song}"
|
returned_lyrics: str = ""
|
||||||
returned_lyrics: str = ''
|
|
||||||
async with ClientSession() as client:
|
async with ClientSession() as client:
|
||||||
async with await client.get(self.lrclib_url,
|
async with await client.get(
|
||||||
params = {
|
self.lrclib_url,
|
||||||
'artist_name': artist,
|
params={
|
||||||
'track_name': song,
|
"artist_name": artist,
|
||||||
},
|
"track_name": song,
|
||||||
timeout=self.timeout,
|
},
|
||||||
headers=self.headers) as request:
|
timeout=self.timeout,
|
||||||
|
headers=self.headers,
|
||||||
|
) as request:
|
||||||
request.raise_for_status()
|
request.raise_for_status()
|
||||||
|
|
||||||
text: Optional[str] = await request.text()
|
text: Optional[str] = await request.text()
|
||||||
if not text:
|
if not text:
|
||||||
raise InvalidLRCLibResponseException("No search response.")
|
raise InvalidLRCLibResponseException("No search response.")
|
||||||
if len(text) < 100:
|
if len(text) < 100:
|
||||||
raise InvalidLRCLibResponseException("Search response text was invalid (len < 100 chars.)")
|
raise InvalidLRCLibResponseException(
|
||||||
|
"Search response text was invalid (len < 100 chars.)"
|
||||||
|
)
|
||||||
|
|
||||||
search_data: Optional[Union[list, dict]] = await request.json()
|
search_data: Optional[Union[list, dict]] = await request.json()
|
||||||
if not isinstance(search_data, list|dict):
|
if not isinstance(search_data, list | dict):
|
||||||
raise InvalidLRCLibResponseException("No JSON search data.")
|
raise InvalidLRCLibResponseException("No JSON search data.")
|
||||||
|
|
||||||
# logging.info("Search Data:\n%s", search_data)
|
# logging.info("Search Data:\n%s", search_data)
|
||||||
|
|
||||||
if not isinstance(search_data, list):
|
if not isinstance(search_data, list):
|
||||||
raise InvalidLRCLibResponseException("Invalid JSON.")
|
raise InvalidLRCLibResponseException("Invalid JSON.")
|
||||||
|
|
||||||
if plain:
|
if plain:
|
||||||
possible_matches = [(x, f"{result.get('artistName')} - {result.get('trackName')}")
|
possible_matches = [
|
||||||
for x, result in enumerate(search_data)]
|
(
|
||||||
|
x,
|
||||||
|
f"{result.get('artistName')} - {result.get('trackName')}",
|
||||||
|
)
|
||||||
|
for x, result in enumerate(search_data)
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
logging.info("Limiting possible matches to only those with non-null syncedLyrics")
|
logging.info(
|
||||||
possible_matches = [(x, f"{result.get('artistName')} - {result.get('trackName')}")
|
"Limiting possible matches to only those with non-null syncedLyrics"
|
||||||
for x, result in enumerate(search_data) if isinstance(result['syncedLyrics'], str)]
|
)
|
||||||
|
possible_matches = [
|
||||||
|
(
|
||||||
|
x,
|
||||||
|
f"{result.get('artistName')} - {result.get('trackName')}",
|
||||||
|
)
|
||||||
|
for x, result in enumerate(search_data)
|
||||||
|
if isinstance(result["syncedLyrics"], str)
|
||||||
|
]
|
||||||
|
|
||||||
|
best_match = self.matcher.find_best_match(
|
||||||
|
input_track, possible_matches
|
||||||
best_match = self.matcher.find_best_match(input_track,
|
)[0]
|
||||||
possible_matches)[0]
|
|
||||||
if not best_match:
|
if not best_match:
|
||||||
return
|
return
|
||||||
best_match_id = best_match[0]
|
best_match_id = best_match[0]
|
||||||
|
|
||||||
if not isinstance(search_data[best_match_id]['artistName'], str):
|
if not isinstance(search_data[best_match_id]["artistName"], str):
|
||||||
raise InvalidLRCLibResponseException(f"Invalid JSON: Cannot find artistName key.\n{search_data}")
|
raise InvalidLRCLibResponseException(
|
||||||
|
f"Invalid JSON: Cannot find artistName key.\n{search_data}"
|
||||||
if not isinstance(search_data[best_match_id]['trackName'], str):
|
)
|
||||||
raise InvalidLRCLibResponseException(f"Invalid JSON: Cannot find trackName key.\n{search_data}")
|
|
||||||
|
if not isinstance(search_data[best_match_id]["trackName"], str):
|
||||||
returned_artist: str = search_data[best_match_id]['artistName']
|
raise InvalidLRCLibResponseException(
|
||||||
returned_song: str = search_data[best_match_id]['trackName']
|
f"Invalid JSON: Cannot find trackName key.\n{search_data}"
|
||||||
|
)
|
||||||
|
|
||||||
|
returned_artist: str = search_data[best_match_id]["artistName"]
|
||||||
|
returned_song: str = search_data[best_match_id]["trackName"]
|
||||||
if plain:
|
if plain:
|
||||||
if not isinstance(search_data[best_match_id]['plainLyrics'], str):
|
if not isinstance(
|
||||||
raise InvalidLRCLibResponseException(f"Invalid JSON: Cannot find plainLyrics key.\n{search_data}")
|
search_data[best_match_id]["plainLyrics"], str
|
||||||
returned_lyrics: str = search_data[best_match_id]['plainLyrics']
|
):
|
||||||
|
raise InvalidLRCLibResponseException(
|
||||||
|
f"Invalid JSON: Cannot find plainLyrics key.\n{search_data}"
|
||||||
|
)
|
||||||
|
returned_lyrics: str = search_data[best_match_id]["plainLyrics"]
|
||||||
returned_lyrics = self.datautils.scrub_lyrics(returned_lyrics)
|
returned_lyrics = self.datautils.scrub_lyrics(returned_lyrics)
|
||||||
else:
|
else:
|
||||||
if not isinstance(search_data[best_match_id]['syncedLyrics'], str):
|
if not isinstance(
|
||||||
raise InvalidLRCLibResponseException(f"Invalid JSON: Cannot find syncedLyrics key.\n{search_data}")
|
search_data[best_match_id]["syncedLyrics"], str
|
||||||
returned_lyrics: str = search_data[best_match_id]['syncedLyrics']
|
):
|
||||||
|
raise InvalidLRCLibResponseException(
|
||||||
|
f"Invalid JSON: Cannot find syncedLyrics key.\n{search_data}"
|
||||||
|
)
|
||||||
|
returned_lyrics: str = search_data[best_match_id][
|
||||||
|
"syncedLyrics"
|
||||||
|
]
|
||||||
lrc_obj = self.datautils.create_lrc_object(returned_lyrics)
|
lrc_obj = self.datautils.create_lrc_object(returned_lyrics)
|
||||||
returned_track: str = f"{returned_artist} - {returned_song}"
|
returned_track: str = f"{returned_artist} - {returned_song}"
|
||||||
(_matched, confidence) = self.matcher.find_best_match(input_track=input_track,
|
(_matched, confidence) = self.matcher.find_best_match(
|
||||||
candidate_tracks=[(0, returned_track)])
|
input_track=input_track, candidate_tracks=[(0, returned_track)]
|
||||||
|
)
|
||||||
if not confidence:
|
if not confidence:
|
||||||
return # No suitable match found
|
return # No suitable match found
|
||||||
logging.info("Result found on %s", self.label)
|
logging.info("Result found on %s", self.label)
|
||||||
time_end: float = time.time()
|
time_end: float = time.time()
|
||||||
time_diff: float = time_end - time_start
|
time_diff: float = time_end - time_start
|
||||||
matched = LyricsResult(artist=returned_artist,
|
matched = LyricsResult(
|
||||||
song=returned_song,
|
artist=returned_artist,
|
||||||
src=self.label,
|
song=returned_song,
|
||||||
lyrics=returned_lyrics if plain else lrc_obj,
|
src=self.label,
|
||||||
confidence=confidence,
|
lyrics=returned_lyrics if plain else lrc_obj,
|
||||||
time=time_diff)
|
confidence=confidence,
|
||||||
|
time=time_diff,
|
||||||
|
)
|
||||||
await self.redis_cache.increment_found_count(self.label)
|
await self.redis_cache.increment_found_count(self.label)
|
||||||
await self.cache.store(matched)
|
await self.cache.store(matched)
|
||||||
return matched
|
return matched
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
@ -7,24 +7,27 @@ import regex
|
|||||||
from regex import Pattern
|
from regex import Pattern
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Union, Optional
|
from typing import Union, Optional
|
||||||
sys.path.insert(1,'..')
|
|
||||||
|
sys.path.insert(1, "..")
|
||||||
from lyric_search import notifier
|
from lyric_search import notifier
|
||||||
from lyric_search.constructors import LyricsResult
|
from lyric_search.constructors import LyricsResult
|
||||||
import redis.asyncio as redis
|
import redis.asyncio as redis
|
||||||
from redis.commands.search.query import Query # type: ignore
|
from redis.commands.search.query import Query # type: ignore
|
||||||
from redis.commands.search.indexDefinition import IndexDefinition, IndexType # type: ignore
|
from redis.commands.search.indexDefinition import IndexDefinition, IndexType # type: ignore
|
||||||
from redis.commands.search.field import TextField, TagField # type: ignore
|
from redis.commands.search.field import TextField, TagField # type: ignore
|
||||||
from redis.commands.json.path import Path # type: ignore
|
from redis.commands.json.path import Path # type: ignore
|
||||||
from . import private
|
from . import private
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
log_level = logging.getLevelName(logger.level)
|
log_level = logging.getLevelName(logger.level)
|
||||||
|
|
||||||
|
|
||||||
class RedisException(Exception):
|
class RedisException(Exception):
|
||||||
"""
|
"""
|
||||||
Redis Exception
|
Redis Exception
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class RedisCache:
|
class RedisCache:
|
||||||
"""
|
"""
|
||||||
Redis Cache Methods
|
Redis Cache Methods
|
||||||
@ -35,34 +38,37 @@ class RedisCache:
|
|||||||
self.notifier = notifier.DiscordNotifier()
|
self.notifier = notifier.DiscordNotifier()
|
||||||
self.notify_warnings = False
|
self.notify_warnings = False
|
||||||
self.regexes: list[Pattern] = [
|
self.regexes: list[Pattern] = [
|
||||||
regex.compile(r'\-'),
|
regex.compile(r"\-"),
|
||||||
regex.compile(r'[^a-zA-Z0-9\s]'),
|
regex.compile(r"[^a-zA-Z0-9\s]"),
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
asyncio.get_event_loop().create_task(self.create_index())
|
asyncio.get_event_loop().create_task(self.create_index())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug("Failed to create redis create_index task: %s",
|
logging.debug("Failed to create redis create_index task: %s", str(e))
|
||||||
str(e))
|
|
||||||
|
|
||||||
async def create_index(self) -> None:
|
async def create_index(self) -> None:
|
||||||
"""Create Index"""
|
"""Create Index"""
|
||||||
try:
|
try:
|
||||||
schema = (
|
schema = (
|
||||||
TextField("$.search_artist", as_name="artist"),
|
TextField("$.search_artist", as_name="artist"),
|
||||||
TextField("$.search_song", as_name="song"),
|
TextField("$.search_song", as_name="song"),
|
||||||
TextField("$.src", as_name="src"),
|
TextField("$.src", as_name="src"),
|
||||||
TextField("$.lyrics", as_name="lyrics")
|
TextField("$.lyrics", as_name="lyrics"),
|
||||||
)
|
)
|
||||||
result = await self.redis_client.ft().create_index(
|
result = await self.redis_client.ft().create_index(
|
||||||
schema, definition=IndexDefinition(prefix=["lyrics:"], index_type=IndexType.JSON))
|
schema,
|
||||||
|
definition=IndexDefinition(
|
||||||
|
prefix=["lyrics:"], index_type=IndexType.JSON
|
||||||
|
),
|
||||||
|
)
|
||||||
if str(result) != "OK":
|
if str(result) != "OK":
|
||||||
raise RedisException(f"Redis: Failed to create index: {result}")
|
raise RedisException(f"Redis: Failed to create index: {result}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug("Failed to create redis index: %s",
|
logging.debug("Failed to create redis index: %s", str(e))
|
||||||
str(e))
|
|
||||||
|
def sanitize_input(
|
||||||
def sanitize_input(self, artist: str, song: str,
|
self, artist: str, song: str, fuzzy: Optional[bool] = False
|
||||||
fuzzy: Optional[bool] = False) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
Sanitize artist/song input (convert to redis matchable fuzzy query)
|
Sanitize artist/song input (convert to redis matchable fuzzy query)
|
||||||
Args:
|
Args:
|
||||||
@ -77,10 +83,12 @@ class RedisCache:
|
|||||||
song = self.regexes[0].sub("", song)
|
song = self.regexes[0].sub("", song)
|
||||||
song = self.regexes[1].sub("", song).strip()
|
song = self.regexes[1].sub("", song).strip()
|
||||||
if fuzzy:
|
if fuzzy:
|
||||||
artist = " ".join([f"(%{artist_word}%)" for artist_word in artist.split(" ")])
|
artist = " ".join(
|
||||||
|
[f"(%{artist_word}%)" for artist_word in artist.split(" ")]
|
||||||
|
)
|
||||||
song = " ".join([f"(%{song_word}%)" for song_word in song.split(" ")])
|
song = " ".join([f"(%{song_word}%)" for song_word in song.split(" ")])
|
||||||
return (artist, song)
|
return (artist, song)
|
||||||
|
|
||||||
async def increment_found_count(self, src: str) -> None:
|
async def increment_found_count(self, src: str) -> None:
|
||||||
"""
|
"""
|
||||||
Increment the found count for a source
|
Increment the found count for a source
|
||||||
@ -94,13 +102,13 @@ class RedisCache:
|
|||||||
await self.redis_client.incr(f"returned:{src}")
|
await self.redis_client.incr(f"returned:{src}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
file: str = __file__.rsplit("/", maxsplit=1)[-1]
|
file: str = __file__.rsplit("/", maxsplit=1)[-1]
|
||||||
await self.notifier.send(f"ERROR @ {file}", str(e))
|
await self.notifier.send(f"ERROR @ {file}", str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def get_found_counts(self) -> Optional[dict]:
|
async def get_found_counts(self) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
Get found counts for all sources (and failed count)
|
Get found counts for all sources (and failed count)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: In the form {'source': count, 'source2': count, ...}
|
dict: In the form {'source': count, 'source2': count, ...}
|
||||||
"""
|
"""
|
||||||
@ -109,18 +117,20 @@ class RedisCache:
|
|||||||
counts: dict[str, int] = {}
|
counts: dict[str, int] = {}
|
||||||
for src in sources:
|
for src in sources:
|
||||||
src_found_count = await self.redis_client.get(f"returned:{src}")
|
src_found_count = await self.redis_client.get(f"returned:{src}")
|
||||||
counts[src] = int(src_found_count) # Redis returns bytes
|
counts[src] = int(src_found_count) # Redis returns bytes
|
||||||
return counts
|
return counts
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
file: str = __file__.rsplit("/", maxsplit=1)[-1]
|
file: str = __file__.rsplit("/", maxsplit=1)[-1]
|
||||||
await self.notifier.send(f"ERROR @ {file}", str(e))
|
await self.notifier.send(f"ERROR @ {file}", str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def search(
|
||||||
async def search(self, artist: Optional[str] = None,
|
self,
|
||||||
song: Optional[str] = None,
|
artist: Optional[str] = None,
|
||||||
lyrics: Optional[str] = None) -> Optional[list[tuple]]:
|
song: Optional[str] = None,
|
||||||
|
lyrics: Optional[str] = None,
|
||||||
|
) -> Optional[list[tuple]]:
|
||||||
"""
|
"""
|
||||||
Search Redis Cache
|
Search Redis Cache
|
||||||
Args:
|
Args:
|
||||||
@ -133,57 +143,72 @@ class RedisCache:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
fuzzy_artist = None
|
fuzzy_artist = None
|
||||||
fuzzy_song = None
|
fuzzy_song = None
|
||||||
is_random_search = artist == "!" and song == "!"
|
is_random_search = artist == "!" and song == "!"
|
||||||
|
|
||||||
if lyrics:
|
if lyrics:
|
||||||
# to code later
|
# to code later
|
||||||
raise RedisException("Lyric search not yet implemented")
|
raise RedisException("Lyric search not yet implemented")
|
||||||
|
|
||||||
if not is_random_search:
|
if not is_random_search:
|
||||||
logging.debug("Redis: Searching normally first")
|
logging.debug("Redis: Searching normally first")
|
||||||
if not artist or not song:
|
if not artist or not song:
|
||||||
logging.info("redis_cache:: search failed: No artist or song provided.")
|
logging.info(
|
||||||
|
"redis_cache:: search failed: No artist or song provided."
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
(artist, song) = self.sanitize_input(artist, song)
|
(artist, song) = self.sanitize_input(artist, song)
|
||||||
logging.debug("Seeking: %s - %s", artist, song)
|
logging.debug("Seeking: %s - %s", artist, song)
|
||||||
search_res: Union[dict, list] = await self.redis_client.ft().search(Query( # type: ignore
|
search_res: Union[dict, list] = await self.redis_client.ft().search(
|
||||||
f"@artist:{artist} @song:{song}"
|
Query(f"@artist:{artist} @song:{song}") # type: ignore
|
||||||
))
|
)
|
||||||
search_res_out: list[tuple] = [(result['id'].split(":",
|
search_res_out: list[tuple] = [
|
||||||
maxsplit=1)[1], dict(json.loads(result['json'])))
|
(
|
||||||
for result in search_res.docs] # type: ignore
|
result["id"].split(":", maxsplit=1)[1],
|
||||||
|
dict(json.loads(result["json"])),
|
||||||
|
)
|
||||||
|
for result in search_res.docs
|
||||||
|
] # type: ignore
|
||||||
if not search_res_out:
|
if not search_res_out:
|
||||||
logging.debug("Redis: Normal search failed, trying with fuzzy search")
|
logging.debug(
|
||||||
|
"Redis: Normal search failed, trying with fuzzy search"
|
||||||
|
)
|
||||||
|
|
||||||
short_artist = " ".join(artist.split(" ")[0:5])
|
short_artist = " ".join(artist.split(" ")[0:5])
|
||||||
short_song = " ".join(song.split(" ")[0:5])
|
short_song = " ".join(song.split(" ")[0:5])
|
||||||
(fuzzy_artist, fuzzy_song) = self.sanitize_input(artist=short_artist.strip(),
|
(fuzzy_artist, fuzzy_song) = self.sanitize_input(
|
||||||
song=short_song.strip(), fuzzy=True)
|
artist=short_artist.strip(), song=short_song.strip(), fuzzy=True
|
||||||
search_res = await self.redis_client.ft().search(Query( # type: ignore
|
)
|
||||||
f"@artist:{fuzzy_artist} @song:{fuzzy_song}"
|
search_res = await self.redis_client.ft().search(
|
||||||
))
|
Query( # type: ignore
|
||||||
search_res_out = [(result['id'].split(":",
|
f"@artist:{fuzzy_artist} @song:{fuzzy_song}"
|
||||||
maxsplit=1)[1], dict(json.loads(result['json'])))
|
)
|
||||||
for result in search_res.docs] # type: ignore
|
)
|
||||||
|
search_res_out = [
|
||||||
|
(
|
||||||
|
result["id"].split(":", maxsplit=1)[1],
|
||||||
|
dict(json.loads(result["json"])),
|
||||||
|
)
|
||||||
|
for result in search_res.docs
|
||||||
|
] # type: ignore
|
||||||
|
|
||||||
else:
|
else:
|
||||||
random_redis_key: str = await self.redis_client.randomkey()
|
random_redis_key: str = await self.redis_client.randomkey()
|
||||||
out_id: str = str(random_redis_key).split(":",
|
out_id: str = str(random_redis_key).split(":", maxsplit=1)[1][:-1]
|
||||||
maxsplit=1)[1][:-1]
|
|
||||||
search_res = await self.redis_client.json().get(random_redis_key)
|
search_res = await self.redis_client.json().get(random_redis_key)
|
||||||
search_res_out = [(out_id, search_res)]
|
search_res_out = [(out_id, search_res)]
|
||||||
|
|
||||||
if not search_res_out and self.notify_warnings:
|
if not search_res_out and self.notify_warnings:
|
||||||
await self.notifier.send("WARNING", f"Redis cache miss for: `{artist} - {song}`")
|
await self.notifier.send(
|
||||||
|
"WARNING", f"Redis cache miss for: `{artist} - {song}`"
|
||||||
|
)
|
||||||
return search_res_out
|
return search_res_out
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
# await self.notifier.send(f"ERROR @ {__file__.rsplit("/", maxsplit=1)[-1]}", f"{str(e)}\nSearch was: {artist} - {song}; fuzzy: {fuzzy_artist} - {fuzzy_song}")
|
# await self.notifier.send(f"ERROR @ {__file__.rsplit("/", maxsplit=1)[-1]}", f"{str(e)}\nSearch was: {artist} - {song}; fuzzy: {fuzzy_artist} - {fuzzy_song}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def redis_store(self, sqlite_id: int,
|
async def redis_store(self, sqlite_id: int, lyr_result: LyricsResult) -> None:
|
||||||
lyr_result: LyricsResult) -> None:
|
|
||||||
"""
|
"""
|
||||||
Store lyrics to redis cache
|
Store lyrics to redis cache
|
||||||
Args:
|
Args:
|
||||||
@ -193,34 +218,47 @@ class RedisCache:
|
|||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
(search_artist, search_song) = self.sanitize_input(lyr_result.artist,
|
(search_artist, search_song) = self.sanitize_input(
|
||||||
lyr_result.song)
|
lyr_result.artist, lyr_result.song
|
||||||
|
)
|
||||||
redis_mapping: dict = {
|
redis_mapping: dict = {
|
||||||
'id': sqlite_id,
|
"id": sqlite_id,
|
||||||
'src': lyr_result.src,
|
"src": lyr_result.src,
|
||||||
'date_retrieved': time.time(),
|
"date_retrieved": time.time(),
|
||||||
'artist': lyr_result.artist,
|
"artist": lyr_result.artist,
|
||||||
'search_artist': search_artist,
|
"search_artist": search_artist,
|
||||||
'search_song': search_song,
|
"search_song": search_song,
|
||||||
'search_artistsong': f'{search_artist}\n{search_song}',
|
"search_artistsong": f"{search_artist}\n{search_song}",
|
||||||
'song': lyr_result.song,
|
"song": lyr_result.song,
|
||||||
'artistsong': f"{lyr_result.artist}\n{lyr_result.song}",
|
"artistsong": f"{lyr_result.artist}\n{lyr_result.song}",
|
||||||
'confidence': lyr_result.confidence,
|
"confidence": lyr_result.confidence,
|
||||||
'lyrics': lyr_result.lyrics,
|
"lyrics": lyr_result.lyrics,
|
||||||
'tags': '(none)',
|
"tags": "(none)",
|
||||||
'liked': 0,
|
"liked": 0,
|
||||||
}
|
}
|
||||||
newkey: str = f"lyrics:000{sqlite_id}"
|
newkey: str = f"lyrics:000{sqlite_id}"
|
||||||
jsonset: bool = await self.redis_client.json().set(newkey, Path.root_path(),
|
jsonset: bool = await self.redis_client.json().set(
|
||||||
redis_mapping)
|
newkey, Path.root_path(), redis_mapping
|
||||||
|
)
|
||||||
if not jsonset:
|
if not jsonset:
|
||||||
raise RedisException(f"Failed to store {lyr_result.artist} - {lyr_result.song} (SQLite id: {sqlite_id}) to redis:\n{jsonset}")
|
raise RedisException(
|
||||||
logging.info("Stored %s - %s (related SQLite Row ID: %s) to %s",
|
f"Failed to store {lyr_result.artist} - {lyr_result.song} (SQLite id: {sqlite_id}) to redis:\n{jsonset}"
|
||||||
lyr_result.artist, lyr_result.song, sqlite_id, newkey)
|
)
|
||||||
await self.notifier.send("INFO",
|
logging.info(
|
||||||
f"Stored `{lyr_result.artist} - {lyr_result.song}` (related SQLite Row ID: `{sqlite_id}`) to redis: `{newkey}`")
|
"Stored %s - %s (related SQLite Row ID: %s) to %s",
|
||||||
|
lyr_result.artist,
|
||||||
|
lyr_result.song,
|
||||||
|
sqlite_id,
|
||||||
|
newkey,
|
||||||
|
)
|
||||||
|
await self.notifier.send(
|
||||||
|
"INFO",
|
||||||
|
f"Stored `{lyr_result.artist} - {lyr_result.song}` (related SQLite Row ID: `{sqlite_id}`) to redis: `{newkey}`",
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
file: str = __file__.rsplit("/", maxsplit=1)[-1]
|
file: str = __file__.rsplit("/", maxsplit=1)[-1]
|
||||||
await self.notifier.send(f"ERROR @ {file}",
|
await self.notifier.send(
|
||||||
f"Failed to store `{lyr_result.artist} - {lyr_result.song}`\
|
f"ERROR @ {file}",
|
||||||
(SQLite id: `{sqlite_id}`) to Redis:\n`{str(e)}`")
|
f"Failed to store `{lyr_result.artist} - {lyr_result.song}`\
|
||||||
|
(SQLite id: `{sqlite_id}`) to Redis:\n`{str(e)}`",
|
||||||
|
)
|
||||||
|
@ -4,38 +4,41 @@ import logging
|
|||||||
import regex
|
import regex
|
||||||
from regex import Pattern
|
from regex import Pattern
|
||||||
|
|
||||||
|
|
||||||
class TrackMatcher:
|
class TrackMatcher:
|
||||||
"""Track Matcher"""
|
"""Track Matcher"""
|
||||||
|
|
||||||
def __init__(self, threshold: float = 0.85):
|
def __init__(self, threshold: float = 0.85):
|
||||||
"""
|
"""
|
||||||
Initialize the TrackMatcher with a similarity threshold.
|
Initialize the TrackMatcher with a similarity threshold.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
threshold (float): Minimum similarity score to consider a match valid
|
threshold (float): Minimum similarity score to consider a match valid
|
||||||
(between 0 and 1, default 0.85)
|
(between 0 and 1, default 0.85)
|
||||||
"""
|
"""
|
||||||
self.threshold = threshold
|
self.threshold = threshold
|
||||||
|
|
||||||
def find_best_match(self, input_track: str, candidate_tracks: List[tuple[int|str, str]]) -> Optional[tuple]:
|
def find_best_match(
|
||||||
|
self, input_track: str, candidate_tracks: List[tuple[int | str, str]]
|
||||||
|
) -> Optional[tuple]:
|
||||||
"""
|
"""
|
||||||
Find the best matching track from the candidate list.
|
Find the best matching track from the candidate list.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input_track (str): Input track in "ARTIST - SONG" format
|
input_track (str): Input track in "ARTIST - SONG" format
|
||||||
candidate_tracks (List[tuple[int|str, str]]): List of candidate tracks
|
candidate_tracks (List[tuple[int|str, str]]): List of candidate tracks
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[tuple[int, str, float]]: Tuple of (best matching track, similarity score)
|
Optional[tuple[int, str, float]]: Tuple of (best matching track, similarity score)
|
||||||
or None if no good match found
|
or None if no good match found
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
if not input_track or not candidate_tracks:
|
if not input_track or not candidate_tracks:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Normalize input track
|
# Normalize input track
|
||||||
input_track = self._normalize_string(input_track)
|
input_track = self._normalize_string(input_track)
|
||||||
|
|
||||||
best_match = None
|
best_match = None
|
||||||
best_score: float = 0.0
|
best_score: float = 0.0
|
||||||
|
|
||||||
@ -43,12 +46,16 @@ class TrackMatcher:
|
|||||||
normalized_candidate = self._normalize_string(candidate[1])
|
normalized_candidate = self._normalize_string(candidate[1])
|
||||||
if normalized_candidate.strip().lower() == input_track.strip().lower():
|
if normalized_candidate.strip().lower() == input_track.strip().lower():
|
||||||
return (candidate, 100.0)
|
return (candidate, 100.0)
|
||||||
|
|
||||||
# Calculate various similarity scores
|
# Calculate various similarity scores
|
||||||
exact_score = 1.0 if input_track == normalized_candidate else 0.0
|
exact_score = 1.0 if input_track == normalized_candidate else 0.0
|
||||||
sequence_score = SequenceMatcher(None, input_track, normalized_candidate).ratio()
|
sequence_score = SequenceMatcher(
|
||||||
token_score = self._calculate_token_similarity(input_track, normalized_candidate)
|
None, input_track, normalized_candidate
|
||||||
|
).ratio()
|
||||||
|
token_score = self._calculate_token_similarity(
|
||||||
|
input_track, normalized_candidate
|
||||||
|
)
|
||||||
|
|
||||||
# Take the maximum of the different scoring methods
|
# Take the maximum of the different scoring methods
|
||||||
final_score = max(exact_score, sequence_score, token_score)
|
final_score = max(exact_score, sequence_score, token_score)
|
||||||
|
|
||||||
@ -59,7 +66,7 @@ class TrackMatcher:
|
|||||||
# Return the match only if it meets the threshold
|
# Return the match only if it meets the threshold
|
||||||
if best_score < self.threshold:
|
if best_score < self.threshold:
|
||||||
return None
|
return None
|
||||||
match: tuple = (best_match, round(best_score * 100))
|
match: tuple = (best_match, round(best_score * 100))
|
||||||
return match
|
return match
|
||||||
|
|
||||||
def _normalize_string(self, text: str) -> str:
|
def _normalize_string(self, text: str) -> str:
|
||||||
@ -72,9 +79,9 @@ class TrackMatcher:
|
|||||||
str: Normalized text
|
str: Normalized text
|
||||||
"""
|
"""
|
||||||
# Remove special characters and convert to lowercase
|
# Remove special characters and convert to lowercase
|
||||||
text = regex.sub(r'[^\w\s-]', '', text).lower()
|
text = regex.sub(r"[^\w\s-]", "", text).lower()
|
||||||
# Normalize spaces
|
# Normalize spaces
|
||||||
text = ' '.join(text.split())
|
text = " ".join(text.split())
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def _calculate_token_similarity(self, str1: str, str2: str) -> float:
|
def _calculate_token_similarity(self, str1: str, str2: str) -> float:
|
||||||
@ -88,28 +95,32 @@ class TrackMatcher:
|
|||||||
"""
|
"""
|
||||||
tokens1 = set(str1.split())
|
tokens1 = set(str1.split())
|
||||||
tokens2 = set(str2.split())
|
tokens2 = set(str2.split())
|
||||||
|
|
||||||
if not tokens1 or not tokens2:
|
if not tokens1 or not tokens2:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
intersection = tokens1.intersection(tokens2)
|
intersection = tokens1.intersection(tokens2)
|
||||||
union = tokens1.union(tokens2)
|
union = tokens1.union(tokens2)
|
||||||
|
|
||||||
return len(intersection) / len(union)
|
return len(intersection) / len(union)
|
||||||
|
|
||||||
|
|
||||||
class DataUtils:
|
class DataUtils:
|
||||||
"""
|
"""
|
||||||
Data Utils
|
Data Utils
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.lrc_regex = regex.compile(r'\[([0-9]{2}:[0-9]{2})\.[0-9]{1,3}\](\s(.*)){0,}')
|
self.lrc_regex = regex.compile(
|
||||||
self.scrub_regex_1: Pattern = regex.compile(r'(\[.*?\])(\s){0,}(\:){0,1}')
|
r"\[([0-9]{2}:[0-9]{2})\.[0-9]{1,3}\](\s(.*)){0,}"
|
||||||
self.scrub_regex_2: Pattern = regex.compile(r'(\d?)(Embed\b)',
|
)
|
||||||
flags=regex.IGNORECASE)
|
self.scrub_regex_1: Pattern = regex.compile(r"(\[.*?\])(\s){0,}(\:){0,1}")
|
||||||
self.scrub_regex_3: Pattern = regex.compile(r'\n{2}')
|
self.scrub_regex_2: Pattern = regex.compile(
|
||||||
self.scrub_regex_4: Pattern = regex.compile(r'[0-9]\b$')
|
r"(\d?)(Embed\b)", flags=regex.IGNORECASE
|
||||||
|
)
|
||||||
|
self.scrub_regex_3: Pattern = regex.compile(r"\n{2}")
|
||||||
|
self.scrub_regex_4: Pattern = regex.compile(r"[0-9]\b$")
|
||||||
|
|
||||||
def scrub_lyrics(self, lyrics: str) -> str:
|
def scrub_lyrics(self, lyrics: str) -> str:
|
||||||
"""
|
"""
|
||||||
Lyric Scrub Regex Chain
|
Lyric Scrub Regex Chain
|
||||||
@ -118,11 +129,11 @@ class DataUtils:
|
|||||||
Returns:
|
Returns:
|
||||||
str: Regex scrubbed lyrics
|
str: Regex scrubbed lyrics
|
||||||
"""
|
"""
|
||||||
lyrics = self.scrub_regex_1.sub('', lyrics)
|
lyrics = self.scrub_regex_1.sub("", lyrics)
|
||||||
lyrics = self.scrub_regex_2.sub('', lyrics)
|
lyrics = self.scrub_regex_2.sub("", lyrics)
|
||||||
lyrics = self.scrub_regex_3.sub('\n', lyrics) # Gaps between verses
|
lyrics = self.scrub_regex_3.sub("\n", lyrics) # Gaps between verses
|
||||||
lyrics = self.scrub_regex_3.sub('', lyrics)
|
lyrics = self.scrub_regex_3.sub("", lyrics)
|
||||||
return lyrics
|
return lyrics
|
||||||
|
|
||||||
def create_lrc_object(self, lrc_str: str) -> list[dict]:
|
def create_lrc_object(self, lrc_str: str) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
@ -142,15 +153,21 @@ class DataUtils:
|
|||||||
if not reg_helper:
|
if not reg_helper:
|
||||||
continue
|
continue
|
||||||
reg_helper = reg_helper[0]
|
reg_helper = reg_helper[0]
|
||||||
logging.debug("Reg helper: %s for line: %s; len: %s",
|
logging.debug(
|
||||||
reg_helper, line, len(reg_helper))
|
"Reg helper: %s for line: %s; len: %s",
|
||||||
|
reg_helper,
|
||||||
|
line,
|
||||||
|
len(reg_helper),
|
||||||
|
)
|
||||||
_timetag = reg_helper[0]
|
_timetag = reg_helper[0]
|
||||||
if not reg_helper[1].strip():
|
if not reg_helper[1].strip():
|
||||||
_words = "♪"
|
_words = "♪"
|
||||||
else:
|
else:
|
||||||
_words = reg_helper[1].strip()
|
_words = reg_helper[1].strip()
|
||||||
lrc_out.append({
|
lrc_out.append(
|
||||||
"timeTag": _timetag,
|
{
|
||||||
"words": _words,
|
"timeTag": _timetag,
|
||||||
})
|
"words": _words,
|
||||||
return lrc_out
|
}
|
||||||
|
)
|
||||||
|
return lrc_out
|
||||||
|
21
util.py
21
util.py
@ -10,7 +10,8 @@ class Utilities:
|
|||||||
"""
|
"""
|
||||||
API Utilities
|
API Utilities
|
||||||
"""
|
"""
|
||||||
def __init__(self, app: FastAPI, constants):
|
|
||||||
|
def __init__(self, app: FastAPI, constants):
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
self.blocked_redirect_uri = "https://codey.lol"
|
self.blocked_redirect_uri = "https://codey.lol"
|
||||||
self.app = app
|
self.app = app
|
||||||
@ -22,35 +23,31 @@ class Utilities:
|
|||||||
logging.error("Rejected request: Blocked")
|
logging.error("Rejected request: Blocked")
|
||||||
return RedirectResponse(url=self.blocked_redirect_uri)
|
return RedirectResponse(url=self.blocked_redirect_uri)
|
||||||
|
|
||||||
def get_no_endpoint_found(self, path: Optional[str] = None):
|
def get_no_endpoint_found(self, path: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
Get 404 Response
|
Get 404 Response
|
||||||
"""
|
"""
|
||||||
logging.error("Rejected request: No such endpoint")
|
logging.error("Rejected request: No such endpoint")
|
||||||
raise HTTPException(detail="Unknown endpoint", status_code=404)
|
raise HTTPException(detail="Unknown endpoint", status_code=404)
|
||||||
|
|
||||||
def check_key(self, path: str, key: str,
|
def check_key(self, path: str, key: str, req_type: int = 0) -> bool:
|
||||||
req_type: int = 0) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Accepts path as an argument to allow fine tuning access for each API key, not currently in use.
|
Accepts path as an argument to allow fine tuning access for each API key, not currently in use.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not key or not key.startswith("Bearer "):
|
if not key or not key.startswith("Bearer "):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
_key: str = key.split("Bearer ", maxsplit=1)[1].strip()
|
_key: str = key.split("Bearer ", maxsplit=1)[1].strip()
|
||||||
|
|
||||||
if not _key in self.constants.API_KEYS:
|
if not _key in self.constants.API_KEYS:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if req_type == 2:
|
if req_type == 2:
|
||||||
return _key.startswith("PRV-")
|
return _key.startswith("PRV-")
|
||||||
elif req_type == 4:
|
elif req_type == 4:
|
||||||
return _key.startswith("RAD-")
|
return _key.startswith("RAD-")
|
||||||
|
|
||||||
if path.lower().startswith("/xc/")\
|
if path.lower().startswith("/xc/") and not key.startswith("XC-"):
|
||||||
and not key.startswith("XC-"):
|
return False
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -2,5 +2,6 @@
|
|||||||
LastFM
|
LastFM
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class InvalidLastFMResponseException(Exception):
|
class InvalidLastFMResponseException(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -6,54 +6,60 @@ from aiohttp import ClientSession, ClientTimeout
|
|||||||
from constants import Constants
|
from constants import Constants
|
||||||
from .constructors import InvalidLastFMResponseException
|
from .constructors import InvalidLastFMResponseException
|
||||||
|
|
||||||
|
|
||||||
class LastFM:
|
class LastFM:
|
||||||
"""LastFM Endpoints"""
|
"""LastFM Endpoints"""
|
||||||
def __init__(self,
|
|
||||||
noInit: Optional[bool] = False) -> None:
|
def __init__(self, noInit: Optional[bool] = False) -> None:
|
||||||
self.creds = Constants().LFM_CREDS
|
self.creds = Constants().LFM_CREDS
|
||||||
self.api_base_url: str = "https://ws.audioscrobbler.com/2.0"
|
self.api_base_url: str = "https://ws.audioscrobbler.com/2.0"
|
||||||
|
|
||||||
async def search_artist(self, artist: Optional[str] = None) -> dict:
|
async def search_artist(self, artist: Optional[str] = None) -> dict:
|
||||||
"""Search LastFM for an artist"""
|
"""Search LastFM for an artist"""
|
||||||
try:
|
try:
|
||||||
if not artist:
|
if not artist:
|
||||||
return {
|
return {
|
||||||
'err': 'No artist specified.',
|
"err": "No artist specified.",
|
||||||
}
|
}
|
||||||
|
|
||||||
request_params: list[tuple] = [
|
request_params: list[tuple] = [
|
||||||
("method", "artist.getInfo"),
|
("method", "artist.getInfo"),
|
||||||
("artist", artist),
|
("artist", artist),
|
||||||
("api_key", self.creds.get('key')),
|
("api_key", self.creds.get("key")),
|
||||||
("autocorrect", "1"),
|
("autocorrect", "1"),
|
||||||
("format", "json"),
|
("format", "json"),
|
||||||
]
|
]
|
||||||
|
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
|
|
||||||
async with await session.get(self.api_base_url,
|
async with await session.get(
|
||||||
params=request_params,
|
self.api_base_url,
|
||||||
timeout=ClientTimeout(connect=3, sock_read=8)) as request:
|
params=request_params,
|
||||||
|
timeout=ClientTimeout(connect=3, sock_read=8),
|
||||||
|
) as request:
|
||||||
request.raise_for_status()
|
request.raise_for_status()
|
||||||
data: dict = await request.json()
|
data: dict = await request.json()
|
||||||
data = data.get('artist', 'N/A')
|
data = data.get("artist", "N/A")
|
||||||
|
|
||||||
ret_obj: dict = {
|
ret_obj: dict = {
|
||||||
'id': data.get('mbid'),
|
"id": data.get("mbid"),
|
||||||
'touring': data.get('ontour'),
|
"touring": data.get("ontour"),
|
||||||
'name': data.get('name'),
|
"name": data.get("name"),
|
||||||
'bio': data.get('bio', None).get('summary').strip()\
|
"bio": data.get("bio", None)
|
||||||
.split("<a href")[0],
|
.get("summary")
|
||||||
}
|
.strip()
|
||||||
|
.split("<a href")[0],
|
||||||
|
}
|
||||||
return ret_obj
|
return ret_obj
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {
|
return {
|
||||||
'err': 'Failed',
|
"err": "Failed",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get_track_info(self, artist: Optional[str] = None,
|
async def get_track_info(
|
||||||
track: Optional[str] = None) -> Optional[dict]:
|
self, artist: Optional[str] = None, track: Optional[str] = None
|
||||||
|
) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
Get Track Info from LastFM
|
Get Track Info from LastFM
|
||||||
Args:
|
Args:
|
||||||
@ -66,12 +72,12 @@ class LastFM:
|
|||||||
if not artist or not track:
|
if not artist or not track:
|
||||||
logging.info("inv request")
|
logging.info("inv request")
|
||||||
return {
|
return {
|
||||||
'err': 'Invalid/No artist or track specified',
|
"err": "Invalid/No artist or track specified",
|
||||||
}
|
}
|
||||||
|
|
||||||
request_params: list[tuple] = [
|
request_params: list[tuple] = [
|
||||||
("method", "track.getInfo"),
|
("method", "track.getInfo"),
|
||||||
("api_key", self.creds.get('key')),
|
("api_key", self.creds.get("key")),
|
||||||
("autocorrect", "1"),
|
("autocorrect", "1"),
|
||||||
("artist", artist),
|
("artist", artist),
|
||||||
("track", track),
|
("track", track),
|
||||||
@ -79,29 +85,32 @@ class LastFM:
|
|||||||
]
|
]
|
||||||
|
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with await session.get(self.api_base_url,
|
async with await session.get(
|
||||||
params=request_params,
|
self.api_base_url,
|
||||||
timeout=ClientTimeout(connect=3, sock_read=8)) as request:
|
params=request_params,
|
||||||
|
timeout=ClientTimeout(connect=3, sock_read=8),
|
||||||
|
) as request:
|
||||||
request.raise_for_status()
|
request.raise_for_status()
|
||||||
data: dict = await request.json()
|
data: dict = await request.json()
|
||||||
data = data.get('track', None)
|
data = data.get("track", None)
|
||||||
if not isinstance(data.get('artist'), dict):
|
if not isinstance(data.get("artist"), dict):
|
||||||
return None
|
return None
|
||||||
artist_mbid: int = data.get('artist', None).get('mbid')
|
artist_mbid: int = data.get("artist", None).get("mbid")
|
||||||
album: str = data.get('album', None).get('title')
|
album: str = data.get("album", None).get("title")
|
||||||
ret_obj: dict = {
|
ret_obj: dict = {
|
||||||
'artist_mbid': artist_mbid,
|
"artist_mbid": artist_mbid,
|
||||||
'album': album,
|
"album": album,
|
||||||
}
|
}
|
||||||
return ret_obj
|
return ret_obj
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {
|
return {
|
||||||
'err': 'General Failure',
|
"err": "General Failure",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get_album_tracklist(self, artist: Optional[str] = None,
|
async def get_album_tracklist(
|
||||||
album: Optional[str] = None) -> dict:
|
self, artist: Optional[str] = None, album: Optional[str] = None
|
||||||
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Get Album Tracklist
|
Get Album Tracklist
|
||||||
Args:
|
Args:
|
||||||
@ -113,24 +122,26 @@ class LastFM:
|
|||||||
try:
|
try:
|
||||||
if not artist or not album:
|
if not artist or not album:
|
||||||
return {
|
return {
|
||||||
'err': 'No artist or album specified',
|
"err": "No artist or album specified",
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks: dict = await self.get_release(artist=artist, album=album)
|
tracks: dict = await self.get_release(artist=artist, album=album)
|
||||||
tracks = tracks.get('tracks', None)
|
tracks = tracks.get("tracks", None)
|
||||||
ret_obj: dict = {
|
ret_obj: dict = {
|
||||||
'tracks': tracks,
|
"tracks": tracks,
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret_obj
|
return ret_obj
|
||||||
|
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {
|
return {
|
||||||
'err': 'General Failure',
|
"err": "General Failure",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get_artist_albums(self, artist: Optional[str] = None) -> Union[dict, list[dict]]:
|
async def get_artist_albums(
|
||||||
|
self, artist: Optional[str] = None
|
||||||
|
) -> Union[dict, list[dict]]:
|
||||||
"""
|
"""
|
||||||
Get Artists Albums from LastFM
|
Get Artists Albums from LastFM
|
||||||
Args:
|
Args:
|
||||||
@ -141,37 +152,39 @@ class LastFM:
|
|||||||
try:
|
try:
|
||||||
if not artist:
|
if not artist:
|
||||||
return {
|
return {
|
||||||
'err': 'No artist specified.',
|
"err": "No artist specified.",
|
||||||
}
|
}
|
||||||
|
|
||||||
request_params: list[tuple] = [
|
request_params: list[tuple] = [
|
||||||
("method", "artist.gettopalbums"),
|
("method", "artist.gettopalbums"),
|
||||||
("artist", artist),
|
("artist", artist),
|
||||||
("api_key", self.creds.get('key')),
|
("api_key", self.creds.get("key")),
|
||||||
("autocorrect", "1"),
|
("autocorrect", "1"),
|
||||||
("format", "json"),
|
("format", "json"),
|
||||||
]
|
]
|
||||||
|
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with await session.get(self.api_base_url,
|
async with await session.get(
|
||||||
params=request_params,
|
self.api_base_url,
|
||||||
timeout=ClientTimeout(connect=3, sock_read=8)) as request:
|
params=request_params,
|
||||||
|
timeout=ClientTimeout(connect=3, sock_read=8),
|
||||||
|
) as request:
|
||||||
request.raise_for_status()
|
request.raise_for_status()
|
||||||
json_data: dict = await request.json()
|
json_data: dict = await request.json()
|
||||||
data: dict = json_data.get('topalbums', None).get('album')
|
data: dict = json_data.get("topalbums", None).get("album")
|
||||||
ret_obj: list = [
|
ret_obj: list = [
|
||||||
{
|
{"title": item.get("name")}
|
||||||
'title': item.get('name')
|
for item in data
|
||||||
} for item in data if not(item.get('name').lower() == "(null)")\
|
if not (item.get("name").lower() == "(null)")
|
||||||
and int(item.get('playcount')) >= 50
|
and int(item.get("playcount")) >= 50
|
||||||
]
|
]
|
||||||
return ret_obj
|
return ret_obj
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {
|
return {
|
||||||
'err': 'Failed',
|
"err": "Failed",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get_artist_id(self, artist: Optional[str] = None) -> int:
|
async def get_artist_id(self, artist: Optional[str] = None) -> int:
|
||||||
"""
|
"""
|
||||||
Get Artist ID from LastFM
|
Get Artist ID from LastFM
|
||||||
@ -183,16 +196,16 @@ class LastFM:
|
|||||||
try:
|
try:
|
||||||
if not artist:
|
if not artist:
|
||||||
return -1
|
return -1
|
||||||
artist_search: dict = await self.search_artist(artist=artist)
|
artist_search: dict = await self.search_artist(artist=artist)
|
||||||
if not artist_search:
|
if not artist_search:
|
||||||
logging.debug("[get_artist_id] Throwing no result error")
|
logging.debug("[get_artist_id] Throwing no result error")
|
||||||
return -1
|
return -1
|
||||||
artist_id: int = int(artist_search[0].get('id', 0))
|
artist_id: int = int(artist_search[0].get("id", 0))
|
||||||
return artist_id
|
return artist_id
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
async def get_artist_info_by_id(self, artist_id: Optional[int] = None) -> dict:
|
async def get_artist_info_by_id(self, artist_id: Optional[int] = None) -> dict:
|
||||||
"""
|
"""
|
||||||
Get Artist info by ID from LastFM
|
Get Artist info by ID from LastFM
|
||||||
@ -204,44 +217,48 @@ class LastFM:
|
|||||||
try:
|
try:
|
||||||
if not artist_id or not str(artist_id).isnumeric():
|
if not artist_id or not str(artist_id).isnumeric():
|
||||||
return {
|
return {
|
||||||
'err': 'Invalid/no artist_id specified.',
|
"err": "Invalid/no artist_id specified.",
|
||||||
}
|
}
|
||||||
|
|
||||||
req_url: str = f"{self.api_base_url}/artists/{artist_id}"
|
req_url: str = f"{self.api_base_url}/artists/{artist_id}"
|
||||||
|
|
||||||
request_params: list[tuple] = [
|
request_params: list[tuple] = [
|
||||||
("key", self.creds.get('key')),
|
("key", self.creds.get("key")),
|
||||||
("secret", self.creds.get('secret')),
|
("secret", self.creds.get("secret")),
|
||||||
]
|
]
|
||||||
|
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with await session.get(req_url,
|
async with await session.get(
|
||||||
params=request_params,
|
req_url,
|
||||||
timeout=ClientTimeout(connect=3, sock_read=8)) as request:
|
params=request_params,
|
||||||
|
timeout=ClientTimeout(connect=3, sock_read=8),
|
||||||
|
) as request:
|
||||||
request.raise_for_status()
|
request.raise_for_status()
|
||||||
data: dict = await request.json()
|
data: dict = await request.json()
|
||||||
if not data.get('profile'):
|
if not data.get("profile"):
|
||||||
raise InvalidLastFMResponseException("Data did not contain 'profile' key.")
|
raise InvalidLastFMResponseException(
|
||||||
|
"Data did not contain 'profile' key."
|
||||||
_id: int = data.get('id', None)
|
)
|
||||||
name: str = data.get('name', None)
|
|
||||||
profile: str = data.get('profile', '')
|
_id: int = data.get("id", None)
|
||||||
|
name: str = data.get("name", None)
|
||||||
|
profile: str = data.get("profile", "")
|
||||||
profile = regex.sub(r"(\[(\/{0,})(u|b|i)])", "", profile)
|
profile = regex.sub(r"(\[(\/{0,})(u|b|i)])", "", profile)
|
||||||
members: list = data.get('members', None)
|
members: list = data.get("members", None)
|
||||||
|
|
||||||
ret_obj: dict = {
|
ret_obj: dict = {
|
||||||
'id': _id,
|
"id": _id,
|
||||||
'name': name,
|
"name": name,
|
||||||
'profile': profile,
|
"profile": profile,
|
||||||
'members': members,
|
"members": members,
|
||||||
}
|
}
|
||||||
return ret_obj
|
return ret_obj
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {
|
return {
|
||||||
'err': 'Failed',
|
"err": "Failed",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get_artist_info(self, artist: Optional[str] = None) -> dict:
|
async def get_artist_info(self, artist: Optional[str] = None) -> dict:
|
||||||
"""
|
"""
|
||||||
Get Artist Info from LastFM
|
Get Artist Info from LastFM
|
||||||
@ -253,27 +270,30 @@ class LastFM:
|
|||||||
try:
|
try:
|
||||||
if not artist:
|
if not artist:
|
||||||
return {
|
return {
|
||||||
'err': 'No artist specified.',
|
"err": "No artist specified.",
|
||||||
}
|
}
|
||||||
artist_id: Optional[int] = await self.get_artist_id(artist=artist)
|
artist_id: Optional[int] = await self.get_artist_id(artist=artist)
|
||||||
if not artist_id:
|
if not artist_id:
|
||||||
return {
|
return {
|
||||||
'err': 'Failed',
|
"err": "Failed",
|
||||||
}
|
}
|
||||||
artist_info: Optional[dict] = await self.get_artist_info_by_id(artist_id=artist_id)
|
artist_info: Optional[dict] = await self.get_artist_info_by_id(
|
||||||
|
artist_id=artist_id
|
||||||
|
)
|
||||||
if not artist_info:
|
if not artist_info:
|
||||||
return {
|
return {
|
||||||
'err': 'Failed',
|
"err": "Failed",
|
||||||
}
|
}
|
||||||
return artist_info
|
return artist_info
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {
|
return {
|
||||||
'err': 'Failed',
|
"err": "Failed",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get_release(self, artist: Optional[str] = None,
|
async def get_release(
|
||||||
album: Optional[str] = None) -> dict:
|
self, artist: Optional[str] = None, album: Optional[str] = None
|
||||||
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Get Release info from LastFM
|
Get Release info from LastFM
|
||||||
Args:
|
Args:
|
||||||
@ -285,55 +305,61 @@ class LastFM:
|
|||||||
try:
|
try:
|
||||||
if not artist or not album:
|
if not artist or not album:
|
||||||
return {
|
return {
|
||||||
'err': 'Invalid artist/album pair',
|
"err": "Invalid artist/album pair",
|
||||||
}
|
}
|
||||||
|
|
||||||
request_params: list[tuple] = [
|
request_params: list[tuple] = [
|
||||||
("method", "album.getinfo"),
|
("method", "album.getinfo"),
|
||||||
("artist", artist),
|
("artist", artist),
|
||||||
("album", album),
|
("album", album),
|
||||||
("api_key", self.creds.get('key')),
|
("api_key", self.creds.get("key")),
|
||||||
("autocorrect", "1"),
|
("autocorrect", "1"),
|
||||||
("format", "json"),
|
("format", "json"),
|
||||||
]
|
]
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with await session.get(self.api_base_url,
|
async with await session.get(
|
||||||
params=request_params,
|
self.api_base_url,
|
||||||
timeout=ClientTimeout(connect=3, sock_read=8)) as request:
|
params=request_params,
|
||||||
|
timeout=ClientTimeout(connect=3, sock_read=8),
|
||||||
|
) as request:
|
||||||
request.raise_for_status()
|
request.raise_for_status()
|
||||||
json_data: dict = await request.json()
|
json_data: dict = await request.json()
|
||||||
data: dict = json_data.get('album', None)
|
data: dict = json_data.get("album", None)
|
||||||
ret_obj: dict = {
|
ret_obj: dict = {
|
||||||
'id': data.get('mbid'),
|
"id": data.get("mbid"),
|
||||||
'artists': data.get('artist'),
|
"artists": data.get("artist"),
|
||||||
'tags': data.get('tags'),
|
"tags": data.get("tags"),
|
||||||
'title': data.get('name'),
|
"title": data.get("name"),
|
||||||
'summary': data.get('wiki', None).get('summary').split("<a href")[0]\
|
"summary": (
|
||||||
if "wiki" in data.keys()\
|
data.get("wiki", None).get("summary").split("<a href")[0]
|
||||||
else "No summary available for this release.",
|
if "wiki" in data.keys()
|
||||||
|
else "No summary available for this release."
|
||||||
|
),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
track_key: list = data.get('tracks', None).get('track')
|
track_key: list = data.get("tracks", None).get("track")
|
||||||
except:
|
except:
|
||||||
track_key = []
|
track_key = []
|
||||||
if isinstance(track_key, list):
|
if isinstance(track_key, list):
|
||||||
ret_obj['tracks'] = [
|
ret_obj["tracks"] = [
|
||||||
{
|
{
|
||||||
'duration': item.get('duration', 'N/A'),
|
"duration": item.get("duration", "N/A"),
|
||||||
'title': item.get('name'),
|
"title": item.get("name"),
|
||||||
} for item in track_key]
|
}
|
||||||
|
for item in track_key
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
ret_obj['tracks'] = [
|
ret_obj["tracks"] = [
|
||||||
{
|
{
|
||||||
'duration': data.get('tracks').get('track')\
|
"duration": data.get("tracks")
|
||||||
.get('duration'),
|
.get("track")
|
||||||
'title': data.get('tracks').get('track')\
|
.get("duration"),
|
||||||
.get('name'),
|
"title": data.get("tracks").get("track").get("name"),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
return ret_obj
|
return ret_obj
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {
|
return {
|
||||||
'err': 'Failed',
|
"err": "Failed",
|
||||||
}
|
}
|
||||||
|
@ -12,45 +12,48 @@ from typing import Union, Optional, LiteralString, Iterable
|
|||||||
from uuid import uuid4 as uuid
|
from uuid import uuid4 as uuid
|
||||||
from endpoints.constructors import RadioException
|
from endpoints.constructors import RadioException
|
||||||
|
|
||||||
double_space: Pattern = regex.compile(r'\s{2,}')
|
double_space: Pattern = regex.compile(r"\s{2,}")
|
||||||
|
|
||||||
|
|
||||||
class RadioUtil:
|
class RadioUtil:
|
||||||
"""
|
"""
|
||||||
Radio Utils
|
Radio Utils
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, constants) -> None:
|
def __init__(self, constants) -> None:
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
self.gpt = gpt.GPT(self.constants)
|
self.gpt = gpt.GPT(self.constants)
|
||||||
self.ls_uri: str = self.constants.LS_URI
|
self.ls_uri: str = self.constants.LS_URI
|
||||||
self.sqlite_exts: list[str] = ['/home/api/api/solibs/spellfix1.cpython-311-x86_64-linux-gnu.so']
|
self.sqlite_exts: list[str] = [
|
||||||
self.active_playlist_path: Union[str, LiteralString] = os.path\
|
"/home/api/api/solibs/spellfix1.cpython-311-x86_64-linux-gnu.so"
|
||||||
.join("/usr/local/share",
|
]
|
||||||
"sqlite_dbs", "track_file_map.db")
|
self.active_playlist_path: Union[str, LiteralString] = os.path.join(
|
||||||
self.active_playlist_name = "default" # not used
|
"/usr/local/share", "sqlite_dbs", "track_file_map.db"
|
||||||
|
)
|
||||||
|
self.active_playlist_name = "default" # not used
|
||||||
self.active_playlist: list[dict] = []
|
self.active_playlist: list[dict] = []
|
||||||
self.now_playing: dict = {
|
self.now_playing: dict = {
|
||||||
'artist': 'N/A',
|
"artist": "N/A",
|
||||||
'song': 'N/A',
|
"song": "N/A",
|
||||||
'album': 'N/A',
|
"album": "N/A",
|
||||||
'genre': 'N/A',
|
"genre": "N/A",
|
||||||
'artistsong': 'N/A - N/A',
|
"artistsong": "N/A - N/A",
|
||||||
'duration': 0,
|
"duration": 0,
|
||||||
'start': 0,
|
"start": 0,
|
||||||
'end': 0,
|
"end": 0,
|
||||||
'file_path': None,
|
"file_path": None,
|
||||||
'id': None,
|
"id": None,
|
||||||
}
|
}
|
||||||
self.webhooks: dict = {
|
self.webhooks: dict = {
|
||||||
'gpt': {
|
"gpt": {
|
||||||
'hook': self.constants.GPT_WEBHOOK,
|
"hook": self.constants.GPT_WEBHOOK,
|
||||||
},
|
},
|
||||||
'sfm': {
|
"sfm": {
|
||||||
'hook': self.constants.SFM_WEBHOOK,
|
"hook": self.constants.SFM_WEBHOOK,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def duration_conv(self,
|
def duration_conv(self, s: Union[int, float]) -> str:
|
||||||
s: Union[int, float]) -> str:
|
|
||||||
"""
|
"""
|
||||||
Convert duration given in seconds to hours, minutes, and seconds (h:m:s)
|
Convert duration given in seconds to hours, minutes, and seconds (h:m:s)
|
||||||
Args:
|
Args:
|
||||||
@ -58,14 +61,12 @@ class RadioUtil:
|
|||||||
Returns:
|
Returns:
|
||||||
str
|
str
|
||||||
"""
|
"""
|
||||||
return str(datetime.timedelta(seconds=s))\
|
return str(datetime.timedelta(seconds=s)).split(".", maxsplit=1)[0]
|
||||||
.split(".", maxsplit=1)[0]
|
|
||||||
|
|
||||||
async def trackdb_typeahead(self, query: str) -> Optional[list[str]]:
|
async def trackdb_typeahead(self, query: str) -> Optional[list[str]]:
|
||||||
if not query:
|
if not query:
|
||||||
return None
|
return None
|
||||||
async with sqlite3.connect(self.active_playlist_path,
|
async with sqlite3.connect(self.active_playlist_path, timeout=1) as _db:
|
||||||
timeout=1) as _db:
|
|
||||||
_db.row_factory = sqlite3.Row
|
_db.row_factory = sqlite3.Row
|
||||||
db_query: str = """SELECT DISTINCT(LOWER(TRIM(artist) || " - " || TRIM(song))),\
|
db_query: str = """SELECT DISTINCT(LOWER(TRIM(artist) || " - " || TRIM(song))),\
|
||||||
(TRIM(artist) || " - " || TRIM(song)) as artistsong FROM tracks WHERE\
|
(TRIM(artist) || " - " || TRIM(song)) as artistsong FROM tracks WHERE\
|
||||||
@ -73,14 +74,15 @@ class RadioUtil:
|
|||||||
db_params: tuple[str] = (f"%{query}%",)
|
db_params: tuple[str] = (f"%{query}%",)
|
||||||
async with _db.execute(db_query, db_params) as _cursor:
|
async with _db.execute(db_query, db_params) as _cursor:
|
||||||
result: Iterable[sqlite3.Row] = await _cursor.fetchall()
|
result: Iterable[sqlite3.Row] = await _cursor.fetchall()
|
||||||
out_result = [
|
out_result = [str(r["artistsong"]) for r in result]
|
||||||
str(r['artistsong']) for r in result
|
|
||||||
]
|
|
||||||
return out_result
|
return out_result
|
||||||
|
|
||||||
async def search_playlist(self, artistsong: Optional[str] = None,
|
async def search_playlist(
|
||||||
artist: Optional[str] = None,
|
self,
|
||||||
song: Optional[str] = None) -> bool:
|
artistsong: Optional[str] = None,
|
||||||
|
artist: Optional[str] = None,
|
||||||
|
song: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Search for track, add it up next in play queue if found
|
Search for track, add it up next in play queue if found
|
||||||
Args:
|
Args:
|
||||||
@ -95,9 +97,11 @@ class RadioUtil:
|
|||||||
if not artistsong and (not artist or not song):
|
if not artistsong and (not artist or not song):
|
||||||
raise RadioException("No query provided")
|
raise RadioException("No query provided")
|
||||||
try:
|
try:
|
||||||
search_query: str = 'SELECT id, artist, song, (artist || " - " || song) AS artistsong, album, genre, file_path, duration FROM tracks\
|
search_query: str = (
|
||||||
|
'SELECT id, artist, song, (artist || " - " || song) AS artistsong, album, genre, file_path, duration FROM tracks\
|
||||||
WHERE editdist3((lower(artist) || " " || lower(song)), (? || " " || ?))\
|
WHERE editdist3((lower(artist) || " " || lower(song)), (? || " " || ?))\
|
||||||
<= 410 ORDER BY editdist3((lower(artist) || " " || lower(song)), ?) ASC LIMIT 1'
|
<= 410 ORDER BY editdist3((lower(artist) || " " || lower(song)), ?) ASC LIMIT 1'
|
||||||
|
)
|
||||||
if artistsong:
|
if artistsong:
|
||||||
artistsong_split: list = artistsong.split(" - ", maxsplit=1)
|
artistsong_split: list = artistsong.split(" - ", maxsplit=1)
|
||||||
(search_artist, search_song) = tuple(artistsong_split)
|
(search_artist, search_song) = tuple(artistsong_split)
|
||||||
@ -106,26 +110,31 @@ class RadioUtil:
|
|||||||
search_song = song
|
search_song = song
|
||||||
if not artistsong:
|
if not artistsong:
|
||||||
artistsong = f"{search_artist} - {search_song}"
|
artistsong = f"{search_artist} - {search_song}"
|
||||||
search_params = (search_artist.lower(), search_song.lower(), artistsong.lower(),)
|
search_params = (
|
||||||
async with sqlite3.connect(self.active_playlist_path,
|
search_artist.lower(),
|
||||||
timeout=2) as db_conn:
|
search_song.lower(),
|
||||||
|
artistsong.lower(),
|
||||||
|
)
|
||||||
|
async with sqlite3.connect(self.active_playlist_path, timeout=2) as db_conn:
|
||||||
await db_conn.enable_load_extension(True)
|
await db_conn.enable_load_extension(True)
|
||||||
for ext in self.sqlite_exts:
|
for ext in self.sqlite_exts:
|
||||||
await db_conn.load_extension(ext)
|
await db_conn.load_extension(ext)
|
||||||
db_conn.row_factory = sqlite3.Row
|
db_conn.row_factory = sqlite3.Row
|
||||||
async with await db_conn.execute(search_query, search_params) as db_cursor:
|
async with await db_conn.execute(
|
||||||
result: Optional[sqlite3.Row|bool] = await db_cursor.fetchone()
|
search_query, search_params
|
||||||
|
) as db_cursor:
|
||||||
|
result: Optional[sqlite3.Row | bool] = await db_cursor.fetchone()
|
||||||
if not result or not isinstance(result, sqlite3.Row):
|
if not result or not isinstance(result, sqlite3.Row):
|
||||||
return False
|
return False
|
||||||
pushObj: dict = {
|
pushObj: dict = {
|
||||||
'id': result['id'],
|
"id": result["id"],
|
||||||
'uuid': str(uuid().hex),
|
"uuid": str(uuid().hex),
|
||||||
'artist': result['artist'].strip(),
|
"artist": result["artist"].strip(),
|
||||||
'song': result['song'].strip(),
|
"song": result["song"].strip(),
|
||||||
'artistsong': result['artistsong'].strip(),
|
"artistsong": result["artistsong"].strip(),
|
||||||
'genre': result['genre'],
|
"genre": result["genre"],
|
||||||
'file_path': result['file_path'],
|
"file_path": result["file_path"],
|
||||||
'duration': result['duration'],
|
"duration": result["duration"],
|
||||||
}
|
}
|
||||||
self.active_playlist.insert(0, pushObj)
|
self.active_playlist.insert(0, pushObj)
|
||||||
return True
|
return True
|
||||||
@ -133,7 +142,7 @@ class RadioUtil:
|
|||||||
logging.critical("search_playlist:: Search error occurred: %s", str(e))
|
logging.critical("search_playlist:: Search error occurred: %s", str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def load_playlist(self) -> None:
|
async def load_playlist(self) -> None:
|
||||||
"""Load Playlist"""
|
"""Load Playlist"""
|
||||||
try:
|
try:
|
||||||
@ -141,11 +150,11 @@ class RadioUtil:
|
|||||||
self.active_playlist.clear()
|
self.active_playlist.clear()
|
||||||
# db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
|
# db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
|
||||||
# GROUP BY artistdashsong ORDER BY RANDOM()'
|
# GROUP BY artistdashsong ORDER BY RANDOM()'
|
||||||
|
|
||||||
"""
|
"""
|
||||||
LIMITED GENRES
|
LIMITED GENRES
|
||||||
"""
|
"""
|
||||||
|
|
||||||
db_query: str = """SELECT distinct(LOWER(TRIM(artist)) || " - " || LOWER(TRIM(song))), (TRIM(artist) || " - " || TRIM(song)) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
|
db_query: str = """SELECT distinct(LOWER(TRIM(artist)) || " - " || LOWER(TRIM(song))), (TRIM(artist) || " - " || TRIM(song)) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
|
||||||
WHERE (genre LIKE "%metalcore%"\
|
WHERE (genre LIKE "%metalcore%"\
|
||||||
OR genre LIKE "%math rock%"\
|
OR genre LIKE "%math rock%"\
|
||||||
@ -176,44 +185,57 @@ class RadioUtil:
|
|||||||
OR genre LIKE "%indie pop%"\
|
OR genre LIKE "%indie pop%"\
|
||||||
OR genre LIKE "%dnb%")\
|
OR genre LIKE "%dnb%")\
|
||||||
GROUP BY artistdashsong ORDER BY RANDOM()"""
|
GROUP BY artistdashsong ORDER BY RANDOM()"""
|
||||||
|
|
||||||
"""
|
"""
|
||||||
LIMITED TO ONE/SMALL SUBSET OF GENRES
|
LIMITED TO ONE/SMALL SUBSET OF GENRES
|
||||||
"""
|
"""
|
||||||
|
|
||||||
db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
|
# db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
|
||||||
WHERE (artist LIKE "%winds of plague%" OR artist LIKE "%acacia st%" OR artist LIKE "%suicide si%" OR artist LIKE "%in dying%") AND (NOT song LIKE "%(live%") ORDER BY RANDOM()' #ORDER BY artist DESC, album ASC, song ASC'
|
# WHERE (artist LIKE "%winds of plague%" OR artist LIKE "%acacia st%" OR artist LIKE "%suicide si%" OR artist LIKE "%in dying%") AND (NOT song LIKE "%(live%") ORDER BY RANDOM()' #ORDER BY artist DESC, album ASC, song ASC'
|
||||||
|
|
||||||
"""
|
"""
|
||||||
LIMITED TO ONE/SOME ARTISTS...
|
LIMITED TO ONE/SOME ARTISTS...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
|
# db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
|
||||||
# WHERE (artist LIKE "%we butter%" OR artist LIKE "%eisbrecher%" OR artist LIKE "%black ang%" OR artist LIKE "%madison affair%") AND (NOT song LIKE "%%stripped%%" AND NOT song LIKE "%(2022)%" AND NOT song LIKE "%(live%%" AND NOT song LIKE "%%acoustic%%" AND NOT song LIKE "%%instrumental%%" AND NOT song LIKE "%%remix%%" AND NOT song LIKE "%%reimagined%%" AND NOT song LIKE "%%alternative%%" AND NOT song LIKE "%%unzipped%%") GROUP BY artistdashsong ORDER BY RANDOM()'# ORDER BY album ASC, id ASC'
|
# WHERE (artist LIKE "%rise against%" OR artist LIKE "%i prevail%" OR artist LIKE "%volumes%" OR artist LIKE "%movements%" OR artist LIKE "%woe%" OR artist LIKE "%smittyztop%" OR artist LIKE "%chunk! no,%" OR artist LIKE "%fame on fire%" OR artist LIKE "%our last night%" OR artist LIKE "%animal in me%") AND (NOT song LIKE "%%stripped%%" AND NOT song LIKE "%(2022)%" AND NOT song LIKE "%(live%%" AND NOT song LIKE "%%acoustic%%" AND NOT song LIKE "%%instrumental%%" AND NOT song LIKE "%%remix%%" AND NOT song LIKE "%%reimagined%%" AND NOT song LIKE "%%alternative%%" AND NOT song LIKE "%%unzipped%%") GROUP BY artistdashsong ORDER BY RANDOM()'# ORDER BY album ASC, id ASC'
|
||||||
|
|
||||||
async with sqlite3.connect(self.active_playlist_path,
|
# db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
|
||||||
timeout=2) as db_conn:
|
# WHERE (artist LIKE "%%" OR artist LIKE "%belmont%" OR artist LIKE "%in dying arms%" OR artist LIKE "%iwrestleda%" OR artist LIKE "%winds of p%") AND (NOT song LIKE "%%stripped%%" AND NOT song LIKE "%(2022)%" AND NOT song LIKE "%(live%%" AND NOT song LIKE "%%acoustic%%" AND NOT song LIKE "%%instrumental%%" AND NOT song LIKE "%%remix%%" AND NOT song LIKE "%%reimagined%%" AND NOT song LIKE "%%alternative%%" AND NOT song LIKE "%%unzipped%%") GROUP BY artistdashsong ORDER BY RANDOM()'# ORDER BY album ASC, id ASC'
|
||||||
|
|
||||||
|
# db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
|
||||||
|
# WHERE (artist LIKE "%akira the don%") AND (NOT song LIKE "%%stripped%%" AND NOT song LIKE "%(2022)%" AND NOT song LIKE "%(live%%" AND NOT song LIKE "%%acoustic%%" AND NOT song LIKE "%%instrumental%%" AND NOT song LIKE "%%remix%%" AND NOT song LIKE "%%reimagined%%" AND NOT song LIKE "%%alternative%%" AND NOT song LIKE "%%unzipped%%") GROUP BY artistdashsong ORDER BY RANDOM()'# ORDER BY album ASC, id ASC'
|
||||||
|
|
||||||
|
async with sqlite3.connect(
|
||||||
|
f"file:{self.active_playlist_path}?mode=readonly", uri=True, timeout=2
|
||||||
|
) as db_conn:
|
||||||
db_conn.row_factory = sqlite3.Row
|
db_conn.row_factory = sqlite3.Row
|
||||||
async with await db_conn.execute(db_query) as db_cursor:
|
async with await db_conn.execute(db_query) as db_cursor:
|
||||||
results: list[sqlite3.Row] = await db_cursor.fetchall()
|
results: list[sqlite3.Row] = await db_cursor.fetchall()
|
||||||
self.active_playlist = [{
|
self.active_playlist = [
|
||||||
'uuid': str(uuid().hex),
|
{
|
||||||
'id': r['id'],
|
"uuid": str(uuid().hex),
|
||||||
'artist': double_space.sub(' ', r['artist']).strip(),
|
"id": r["id"],
|
||||||
'song': double_space.sub(' ', r['song']).strip(),
|
"artist": double_space.sub(" ", r["artist"]).strip(),
|
||||||
'album': double_space.sub(' ', r['album']).strip(),
|
"song": double_space.sub(" ", r["song"]).strip(),
|
||||||
'genre': r['genre'] if r['genre'] else 'Unknown',
|
"album": double_space.sub(" ", r["album"]).strip(),
|
||||||
'artistsong': double_space.sub(' ', r['artistdashsong']).strip(),
|
"genre": r["genre"] if r["genre"] else "Unknown",
|
||||||
'file_path': r['file_path'],
|
"artistsong": double_space.sub(
|
||||||
'duration': r['duration'],
|
" ", r["artistdashsong"]
|
||||||
} for r in results]
|
).strip(),
|
||||||
logging.info("Populated active playlists with %s items",
|
"file_path": r["file_path"],
|
||||||
len(self.active_playlist))
|
"duration": r["duration"],
|
||||||
|
}
|
||||||
|
for r in results
|
||||||
|
]
|
||||||
|
logging.info(
|
||||||
|
"Populated active playlists with %s items",
|
||||||
|
len(self.active_playlist),
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def cache_album_art(self, track_id: int,
|
async def cache_album_art(self, track_id: int, album_art: bytes) -> None:
|
||||||
album_art: bytes) -> None:
|
|
||||||
"""
|
"""
|
||||||
Cache Album Art to SQLite DB
|
Cache Album Art to SQLite DB
|
||||||
Args:
|
Args:
|
||||||
@ -223,16 +245,21 @@ class RadioUtil:
|
|||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
async with sqlite3.connect(self.active_playlist_path,
|
async with sqlite3.connect(self.active_playlist_path, timeout=2) as db_conn:
|
||||||
timeout=2) as db_conn:
|
async with await db_conn.execute(
|
||||||
async with await db_conn.execute("UPDATE tracks SET album_art = ? WHERE id = ?",
|
"UPDATE tracks SET album_art = ? WHERE id = ?",
|
||||||
(album_art, track_id,)) as db_cursor:
|
(
|
||||||
|
album_art,
|
||||||
|
track_id,
|
||||||
|
),
|
||||||
|
) as db_cursor:
|
||||||
await db_conn.commit()
|
await db_conn.commit()
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def get_album_art(self, track_id: Optional[int] = None,
|
async def get_album_art(
|
||||||
file_path: Optional[str] = None) -> Optional[bytes]:
|
self, track_id: Optional[int] = None, file_path: Optional[str] = None
|
||||||
|
) -> Optional[bytes]:
|
||||||
"""
|
"""
|
||||||
Get Album Art
|
Get Album Art
|
||||||
Args:
|
Args:
|
||||||
@ -242,28 +269,27 @@ class RadioUtil:
|
|||||||
bytes
|
bytes
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
async with sqlite3.connect(self.active_playlist_path,
|
async with sqlite3.connect(self.active_playlist_path, timeout=2) as db_conn:
|
||||||
timeout=2) as db_conn:
|
|
||||||
db_conn.row_factory = sqlite3.Row
|
db_conn.row_factory = sqlite3.Row
|
||||||
query: str = "SELECT album_art FROM tracks WHERE id = ?"
|
query: str = "SELECT album_art FROM tracks WHERE id = ?"
|
||||||
query_params: tuple = (track_id,)
|
query_params: tuple = (track_id,)
|
||||||
|
|
||||||
if file_path and not track_id:
|
if file_path and not track_id:
|
||||||
query = "SELECT album_art FROM tracks WHERE file_path = ?"
|
query = "SELECT album_art FROM tracks WHERE file_path = ?"
|
||||||
query_params = (file_path,)
|
query_params = (file_path,)
|
||||||
|
|
||||||
async with await db_conn.execute(query,
|
async with await db_conn.execute(query, query_params) as db_cursor:
|
||||||
query_params) as db_cursor:
|
result: Optional[Union[sqlite3.Row, bool]] = (
|
||||||
result: Optional[Union[sqlite3.Row, bool]] = await db_cursor.fetchone()
|
await db_cursor.fetchone()
|
||||||
|
)
|
||||||
if not result or not isinstance(result, sqlite3.Row):
|
if not result or not isinstance(result, sqlite3.Row):
|
||||||
return None
|
return None
|
||||||
return result['album_art']
|
return result["album_art"]
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_queue_item_by_uuid(self,
|
def get_queue_item_by_uuid(self, uuid: str) -> Optional[tuple[int, dict]]:
|
||||||
uuid: str) -> Optional[tuple[int, dict]]:
|
|
||||||
"""
|
"""
|
||||||
Get queue item by UUID
|
Get queue item by UUID
|
||||||
Args:
|
Args:
|
||||||
@ -272,10 +298,10 @@ class RadioUtil:
|
|||||||
Optional[tuple[int, dict]]
|
Optional[tuple[int, dict]]
|
||||||
"""
|
"""
|
||||||
for x, item in enumerate(self.active_playlist):
|
for x, item in enumerate(self.active_playlist):
|
||||||
if item.get('uuid') == uuid:
|
if item.get("uuid") == uuid:
|
||||||
return (x, item)
|
return (x, item)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _ls_skip(self) -> bool:
|
async def _ls_skip(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Ask LiquidSoap server to skip to the next track
|
Ask LiquidSoap server to skip to the next track
|
||||||
@ -286,18 +312,18 @@ class RadioUtil:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with session.get(f"{self.ls_uri}/next",
|
async with session.get(
|
||||||
timeout=ClientTimeout(connect=2, sock_read=2)) as request:
|
f"{self.ls_uri}/next", timeout=ClientTimeout(connect=2, sock_read=2)
|
||||||
request.raise_for_status()
|
) as request:
|
||||||
text: Optional[str] = await request.text()
|
request.raise_for_status()
|
||||||
return text == "OK"
|
text: Optional[str] = await request.text()
|
||||||
|
return text == "OK"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug("Skip failed: %s", str(e))
|
logging.debug("Skip failed: %s", str(e))
|
||||||
|
|
||||||
return False # failsafe
|
return False # failsafe
|
||||||
|
|
||||||
async def get_ai_song_info(self, artist: str,
|
async def get_ai_song_info(self, artist: str, song: str) -> Optional[str]:
|
||||||
song: str) -> Optional[str]:
|
|
||||||
"""
|
"""
|
||||||
Get AI Song Info
|
Get AI Song Info
|
||||||
Args:
|
Args:
|
||||||
@ -312,44 +338,50 @@ class RadioUtil:
|
|||||||
logging.critical("No response received from GPT?")
|
logging.critical("No response received from GPT?")
|
||||||
return None
|
return None
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def webhook_song_change(self, track: dict) -> None:
|
async def webhook_song_change(self, track: dict) -> None:
|
||||||
try:
|
try:
|
||||||
"""
|
"""
|
||||||
Handles Song Change Outbounds (Webhooks)
|
Handles Song Change Outbounds (Webhooks)
|
||||||
Args:
|
Args:
|
||||||
track (dict)
|
track (dict)
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# First, send track info
|
# First, send track info
|
||||||
friendly_track_start: str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(track['start']))
|
friendly_track_start: str = time.strftime(
|
||||||
friendly_track_end: str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(track['end']))
|
"%Y-%m-%d %H:%M:%S", time.localtime(track["start"])
|
||||||
|
)
|
||||||
|
friendly_track_end: str = time.strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S", time.localtime(track["end"])
|
||||||
|
)
|
||||||
hook_data: dict = {
|
hook_data: dict = {
|
||||||
'username': 'serious.FM',
|
"username": "serious.FM",
|
||||||
"embeds": [{
|
"embeds": [
|
||||||
|
{
|
||||||
"title": "Now Playing",
|
"title": "Now Playing",
|
||||||
"description": f'## {track['song']}\nby\n## {track['artist']}',
|
"description": f"## {track['song']}\nby\n## {track['artist']}",
|
||||||
"color": 0x30c56f,
|
"color": 0x30C56F,
|
||||||
"thumbnail": {
|
"thumbnail": {
|
||||||
"url": f"https://api.codey.lol/radio/album_art?track_id={track['id']}&{int(time.time())}",
|
"url": f"https://api.codey.lol/radio/album_art?track_id={track['id']}&{int(time.time())}",
|
||||||
},
|
},
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"name": "Duration",
|
"name": "Duration",
|
||||||
"value": self.duration_conv(track['duration']),
|
"value": self.duration_conv(track["duration"]),
|
||||||
"inline": True,
|
"inline": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Genre",
|
"name": "Genre",
|
||||||
"value": track['genre'] if track['genre'] else 'Unknown',
|
"value": (
|
||||||
|
track["genre"] if track["genre"] else "Unknown"
|
||||||
|
),
|
||||||
"inline": True,
|
"inline": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Filetype",
|
"name": "Filetype",
|
||||||
"value": track['file_path'].rsplit(".", maxsplit=1)[1],
|
"value": track["file_path"].rsplit(".", maxsplit=1)[1],
|
||||||
"inline": True,
|
"inline": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -359,42 +391,56 @@ class RadioUtil:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Album",
|
"name": "Album",
|
||||||
"value": track['album'] if track['album'] else "Unknown",
|
"value": (
|
||||||
|
track["album"] if track["album"] else "Unknown"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
}]
|
}
|
||||||
}
|
],
|
||||||
|
}
|
||||||
sfm_hook: str = self.webhooks['sfm'].get('hook')
|
|
||||||
|
sfm_hook: str = self.webhooks["sfm"].get("hook")
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with await session.post(sfm_hook, json=hook_data,
|
async with await session.post(
|
||||||
timeout=ClientTimeout(connect=5, sock_read=5), headers={
|
sfm_hook,
|
||||||
'content-type': 'application/json; charset=utf-8',}) as request:
|
json=hook_data,
|
||||||
request.raise_for_status()
|
timeout=ClientTimeout(connect=5, sock_read=5),
|
||||||
|
headers={
|
||||||
# Next, AI feedback
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
ai_response: Optional[str] = await self.get_ai_song_info(track['artist'],
|
) as request:
|
||||||
track['song'])
|
request.raise_for_status()
|
||||||
|
|
||||||
|
# Next, AI feedback
|
||||||
|
|
||||||
|
ai_response: Optional[str] = await self.get_ai_song_info(
|
||||||
|
track["artist"], track["song"]
|
||||||
|
)
|
||||||
if not ai_response:
|
if not ai_response:
|
||||||
return
|
return
|
||||||
|
|
||||||
hook_data = {
|
hook_data = {
|
||||||
'username': 'GPT',
|
"username": "GPT",
|
||||||
"embeds": [{
|
"embeds": [
|
||||||
|
{
|
||||||
"title": "AI Feedback",
|
"title": "AI Feedback",
|
||||||
"color": 0x35d0ff,
|
"color": 0x35D0FF,
|
||||||
"description": ai_response.strip(),
|
"description": ai_response.strip(),
|
||||||
}]
|
}
|
||||||
}
|
],
|
||||||
|
}
|
||||||
ai_hook: str = self.webhooks['gpt'].get('hook')
|
|
||||||
|
ai_hook: str = self.webhooks["gpt"].get("hook")
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with await session.post(ai_hook, json=hook_data,
|
async with await session.post(
|
||||||
timeout=ClientTimeout(connect=5, sock_read=5), headers={
|
ai_hook,
|
||||||
'content-type': 'application/json; charset=utf-8',}) as request:
|
json=hook_data,
|
||||||
request.raise_for_status()
|
timeout=ClientTimeout(connect=5, sock_read=5),
|
||||||
|
headers={
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
) as request:
|
||||||
|
request.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user