swap threading for multiprocessing's ThreadPool (radio playlist load), aiosqlite -> sqlite3 standard lib as disk i/o is blocking regardless; changes related to #32 for radio queue pagination, more work needed
This commit is contained in:
parent
6502199b5d
commit
4c5d2b6943
@ -281,6 +281,17 @@ class ValidRadioReshuffleRequest(ValidRadioNextRequest):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ValidRadioQueueRequest(BaseModel):
|
||||||
|
"""
|
||||||
|
- **draw**: DataTables draw count, default 1
|
||||||
|
- **start**: paging start position, default 0
|
||||||
|
- **search**: Optional search query
|
||||||
|
"""
|
||||||
|
|
||||||
|
draw: Optional[int] = 1
|
||||||
|
start: Optional[int] = 0
|
||||||
|
search: Optional[str] = None
|
||||||
|
|
||||||
class ValidRadioQueueShiftRequest(BaseModel):
|
class ValidRadioQueueShiftRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **key**: API Key
|
- **key**: API Key
|
||||||
|
@ -2,8 +2,7 @@ import logging
|
|||||||
import traceback
|
import traceback
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
import asyncio
|
from multiprocessing.pool import ThreadPool as Pool
|
||||||
import threading
|
|
||||||
from .constructors import (
|
from .constructors import (
|
||||||
ValidRadioNextRequest,
|
ValidRadioNextRequest,
|
||||||
ValidRadioReshuffleRequest,
|
ValidRadioReshuffleRequest,
|
||||||
@ -11,10 +10,9 @@ from .constructors import (
|
|||||||
ValidRadioQueueRemovalRequest,
|
ValidRadioQueueRemovalRequest,
|
||||||
ValidRadioSongRequest,
|
ValidRadioSongRequest,
|
||||||
ValidRadioTypeaheadRequest,
|
ValidRadioTypeaheadRequest,
|
||||||
|
ValidRadioQueueRequest,
|
||||||
)
|
)
|
||||||
from utils import radio_util
|
from utils import radio_util
|
||||||
|
|
||||||
from uuid import uuid4 as uuid
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi import FastAPI, BackgroundTasks, Request, Response, HTTPException
|
from fastapi import FastAPI, BackgroundTasks, Request, Response, HTTPException
|
||||||
from fastapi.responses import RedirectResponse, JSONResponse
|
from fastapi.responses import RedirectResponse, JSONResponse
|
||||||
@ -59,11 +57,9 @@ class Radio(FastAPI):
|
|||||||
|
|
||||||
async def on_start(self) -> None:
|
async def on_start(self) -> None:
|
||||||
logging.info("radio: Initializing")
|
logging.info("radio: Initializing")
|
||||||
thread = threading.Thread(
|
with Pool() as pool:
|
||||||
target=asyncio.run, args=(self.radio_util.load_playlist(),)
|
res = pool.apply_async(self.radio_util.load_playlist)
|
||||||
)
|
if res:
|
||||||
thread.start()
|
|
||||||
# await self.radio_util.load_playlist()
|
|
||||||
await self.radio_util._ls_skip()
|
await self.radio_util._ls_skip()
|
||||||
|
|
||||||
async def radio_skip(
|
async def radio_skip(
|
||||||
@ -101,6 +97,8 @@ class Radio(FastAPI):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logging.debug("radio_skip Exception: %s",
|
||||||
|
str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
@ -124,18 +122,23 @@ class Radio(FastAPI):
|
|||||||
return JSONResponse(content={"ok": True})
|
return JSONResponse(content={"ok": True})
|
||||||
|
|
||||||
async def radio_get_queue(
|
async def radio_get_queue(
|
||||||
self, request: Request, limit: Optional[int] = 15_000
|
self, request: Request, data: ValidRadioQueueRequest,
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get current play queue, up to limit [default: 15k]
|
Get current play queue (paged, 20 results per page)
|
||||||
- **limit**: Number of queue items to return, default 15k
|
|
||||||
"""
|
"""
|
||||||
queue: list = self.radio_util.active_playlist[0:limit]
|
start: int = int(data.start)
|
||||||
|
end: int = start+20
|
||||||
|
logging.info("queue request with start pos: %s & end pos: %s",
|
||||||
|
start, end)
|
||||||
|
queue_full: list = self.radio_util.active_playlist
|
||||||
|
queue: list = queue_full[start:end]
|
||||||
|
logging.info("queue length: %s", len(queue))
|
||||||
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,
|
"pos": queue_full.index(item),
|
||||||
"id": item.get("id"),
|
"id": item.get("id"),
|
||||||
"uuid": item.get("uuid"),
|
"uuid": item.get("uuid"),
|
||||||
"artist": item.get("artist"),
|
"artist": item.get("artist"),
|
||||||
@ -146,7 +149,13 @@ class Radio(FastAPI):
|
|||||||
"duration": item.get("duration"),
|
"duration": item.get("duration"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return JSONResponse(content={"items": queue_out})
|
out_json = {
|
||||||
|
"draw": data.draw,
|
||||||
|
"recordsTotal": len(queue_full),
|
||||||
|
"recordsFiltered": len(queue_full) if not data.search else len(queue_full), # todo: implement search
|
||||||
|
"items": queue_out,
|
||||||
|
}
|
||||||
|
return JSONResponse(content=out_json)
|
||||||
|
|
||||||
async def radio_queue_shift(
|
async def radio_queue_shift(
|
||||||
self, data: ValidRadioQueueShiftRequest, request: Request
|
self, data: ValidRadioQueueShiftRequest, request: Request
|
||||||
@ -230,6 +239,8 @@ class Radio(FastAPI):
|
|||||||
)
|
)
|
||||||
return Response(content=album_art, media_type="image/png")
|
return Response(content=album_art, media_type="image/png")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logging.debug("album_art_handler Exception: %s",
|
||||||
|
str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
url="https://codey.lol/images/radio_art_default.jpg", status_code=302
|
url="https://codey.lol/images/radio_art_default.jpg", status_code=302
|
||||||
@ -256,7 +267,7 @@ class Radio(FastAPI):
|
|||||||
) -> JSONResponse:
|
) -> 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.)
|
||||||
- **key**: API key
|
- **key**: API key
|
||||||
- **skipTo**: Optional UUID to skip to
|
- **skipTo**: Optional UUID to skip to
|
||||||
"""
|
"""
|
||||||
|
@ -8,7 +8,7 @@ from typing import Union, Optional, Iterable
|
|||||||
from aiohttp import ClientSession, ClientTimeout
|
from aiohttp import ClientSession, ClientTimeout
|
||||||
import regex
|
import regex
|
||||||
from regex import Pattern
|
from regex import Pattern
|
||||||
import aiosqlite as sqlite3
|
import sqlite3
|
||||||
import gpt
|
import gpt
|
||||||
import music_tag # type: ignore
|
import music_tag # type: ignore
|
||||||
from endpoints.constructors import RadioException
|
from endpoints.constructors import RadioException
|
||||||
@ -49,12 +49,12 @@ class RadioUtil:
|
|||||||
"/usr/local/share", "sqlite_dbs", "track_album_art.db"
|
"/usr/local/share", "sqlite_dbs", "track_album_art.db"
|
||||||
)
|
)
|
||||||
self.playback_genres: list[str] = [
|
self.playback_genres: list[str] = [
|
||||||
"post-hardcore",
|
# "post-hardcore",
|
||||||
"post hardcore",
|
# "post hardcore",
|
||||||
"metalcore",
|
# "metalcore",
|
||||||
"deathcore",
|
# "deathcore",
|
||||||
"edm",
|
# "edm",
|
||||||
"electronic",
|
# "electronic",
|
||||||
]
|
]
|
||||||
self.active_playlist: list[dict] = []
|
self.active_playlist: list[dict] = []
|
||||||
self.playlist_loaded: bool = False
|
self.playlist_loaded: bool = False
|
||||||
@ -99,14 +99,14 @@ class RadioUtil:
|
|||||||
"""
|
"""
|
||||||
if not query:
|
if not query:
|
||||||
return None
|
return None
|
||||||
async with sqlite3.connect(self.active_playlist_path, timeout=1) as _db:
|
with sqlite3.connect(self.active_playlist_path, 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\
|
||||||
artistsong LIKE ? LIMIT 30"""
|
artistsong LIKE ? LIMIT 30"""
|
||||||
db_params: tuple[str] = (f"%{query}%",)
|
db_params: tuple[str] = (f"%{query}%",)
|
||||||
async with _db.execute(db_query, db_params) as _cursor:
|
_cursor = _db.execute(db_query, db_params)
|
||||||
result: Iterable[sqlite3.Row] = await _cursor.fetchall()
|
result: Iterable[sqlite3.Row] = _cursor.fetchall()
|
||||||
out_result = [str(r["artistsong"]) for r in result]
|
out_result = [str(r["artistsong"]) for r in result]
|
||||||
return out_result
|
return out_result
|
||||||
|
|
||||||
@ -148,15 +148,15 @@ class RadioUtil:
|
|||||||
search_song.lower(),
|
search_song.lower(),
|
||||||
artistsong.lower(),
|
artistsong.lower(),
|
||||||
)
|
)
|
||||||
async with sqlite3.connect(self.active_playlist_path, timeout=2) as db_conn:
|
with sqlite3.connect(self.active_playlist_path, timeout=2) as db_conn:
|
||||||
await db_conn.enable_load_extension(True)
|
db_conn.enable_load_extension(True)
|
||||||
for ext in self.sqlite_exts:
|
for ext in self.sqlite_exts:
|
||||||
await db_conn.load_extension(ext)
|
db_conn.load_extension(ext)
|
||||||
db_conn.row_factory = sqlite3.Row
|
db_conn.row_factory = sqlite3.Row
|
||||||
async with await db_conn.execute(
|
db_cursor = db_conn.execute(
|
||||||
search_query, search_params
|
search_query, search_params
|
||||||
) as db_cursor:
|
)
|
||||||
result: Optional[sqlite3.Row | bool] = await db_cursor.fetchone()
|
result: Optional[sqlite3.Row | bool] = 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
|
||||||
push_obj: dict = {
|
push_obj: dict = {
|
||||||
@ -165,7 +165,7 @@ class RadioUtil:
|
|||||||
"artist": double_space.sub(" ", result["artist"].strip()),
|
"artist": double_space.sub(" ", result["artist"].strip()),
|
||||||
"song": double_space.sub(" ", result["song"].strip()),
|
"song": double_space.sub(" ", result["song"].strip()),
|
||||||
"artistsong": result["artistsong"].strip(),
|
"artistsong": result["artistsong"].strip(),
|
||||||
"genre": await self.get_genre(
|
"genre": self.get_genre(
|
||||||
double_space.sub(" ", result["artist"].strip())
|
double_space.sub(" ", result["artist"].strip())
|
||||||
),
|
),
|
||||||
"file_path": result["file_path"],
|
"file_path": result["file_path"],
|
||||||
@ -188,19 +188,19 @@ class RadioUtil:
|
|||||||
bool
|
bool
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
async with sqlite3.connect(self.artist_genre_db_path, timeout=2) as _db:
|
with sqlite3.connect(self.artist_genre_db_path, timeout=2) as _db:
|
||||||
query: str = (
|
query: str = (
|
||||||
"INSERT OR IGNORE INTO artist_genre (artist, genre) VALUES(?, ?)"
|
"INSERT OR IGNORE INTO artist_genre (artist, genre) VALUES(?, ?)"
|
||||||
)
|
)
|
||||||
params: tuple[str, str] = (artist, genre)
|
params: tuple[str, str] = (artist, genre)
|
||||||
res = await _db.execute_insert(query, params)
|
res = _db.execute(query, params)
|
||||||
if res:
|
if isinstance(res.lastrowid, int):
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"Query executed successfully for %s/%s, committing",
|
"Query executed successfully for %s/%s, committing",
|
||||||
artist,
|
artist,
|
||||||
genre,
|
genre,
|
||||||
)
|
)
|
||||||
await _db.commit()
|
_db.commit()
|
||||||
return True
|
return True
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"Failed to store artist/genre pair: %s/%s (res: %s)", artist, genre, res
|
"Failed to store artist/genre pair: %s/%s (res: %s)", artist, genre, res
|
||||||
@ -224,7 +224,7 @@ class RadioUtil:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
added_rows: int = 0
|
added_rows: int = 0
|
||||||
async with sqlite3.connect(self.artist_genre_db_path, timeout=2) as _db:
|
with sqlite3.connect(self.artist_genre_db_path, timeout=2) as _db:
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
try:
|
try:
|
||||||
artist, genre = pair
|
artist, genre = pair
|
||||||
@ -232,8 +232,8 @@ class RadioUtil:
|
|||||||
"INSERT OR IGNORE INTO artist_genre (artist, genre) VALUES(?, ?)"
|
"INSERT OR IGNORE INTO artist_genre (artist, genre) VALUES(?, ?)"
|
||||||
)
|
)
|
||||||
params: tuple[str, str] = (artist, genre)
|
params: tuple[str, str] = (artist, genre)
|
||||||
res = await _db.execute_insert(query, params)
|
res = _db.execute(query, params)
|
||||||
if res:
|
if isinstance(res.lastrowid, int):
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"add_genres: Query executed successfully for %s/%s",
|
"add_genres: Query executed successfully for %s/%s",
|
||||||
artist,
|
artist,
|
||||||
@ -257,7 +257,7 @@ class RadioUtil:
|
|||||||
continue
|
continue
|
||||||
if added_rows:
|
if added_rows:
|
||||||
logging.info("add_genres: Committing %s rows", added_rows)
|
logging.info("add_genres: Committing %s rows", added_rows)
|
||||||
await _db.commit()
|
_db.commit()
|
||||||
return True
|
return True
|
||||||
logging.info("add_genres: Failed (No rows added)")
|
logging.info("add_genres: Failed (No rows added)")
|
||||||
return False
|
return False
|
||||||
@ -266,7 +266,7 @@ class RadioUtil:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_genre(self, artist: str) -> str:
|
def get_genre(self, artist: str) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve Genre for given Artist
|
Retrieve Genre for given Artist
|
||||||
Args:
|
Args:
|
||||||
@ -280,10 +280,10 @@ class RadioUtil:
|
|||||||
"SELECT genre FROM artist_genre WHERE artist LIKE ? COLLATE NOCASE"
|
"SELECT genre FROM artist_genre WHERE artist LIKE ? COLLATE NOCASE"
|
||||||
)
|
)
|
||||||
params: tuple[str] = (f"%%{artist}%%",)
|
params: tuple[str] = (f"%%{artist}%%",)
|
||||||
async with sqlite3.connect(self.artist_genre_db_path, timeout=2) as _db:
|
with sqlite3.connect(self.artist_genre_db_path, timeout=2) as _db:
|
||||||
_db.row_factory = sqlite3.Row
|
_db.row_factory = sqlite3.Row
|
||||||
async with await _db.execute(query, params) as _cursor:
|
_cursor = _db.execute(query, params)
|
||||||
res = await _cursor.fetchone()
|
res = _cursor.fetchone()
|
||||||
if not res:
|
if not res:
|
||||||
return "Not Found" # Exception suppressed
|
return "Not Found" # Exception suppressed
|
||||||
# raise RadioException(
|
# raise RadioException(
|
||||||
@ -295,7 +295,7 @@ class RadioUtil:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return "Not Found"
|
return "Not Found"
|
||||||
|
|
||||||
async def load_playlist(self) -> None:
|
def load_playlist(self) -> None:
|
||||||
"""Load Playlist"""
|
"""Load Playlist"""
|
||||||
try:
|
try:
|
||||||
logging.info("Loading playlist...")
|
logging.info("Loading playlist...")
|
||||||
@ -332,12 +332,12 @@ class RadioUtil:
|
|||||||
# 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 "%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'
|
# 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(
|
with sqlite3.connect(
|
||||||
f"file:{self.active_playlist_path}?mode=ro", uri=True, timeout=15
|
f"file:{self.active_playlist_path}?mode=ro", uri=True, timeout=15
|
||||||
) as db_conn:
|
) 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:
|
db_cursor = db_conn.execute(db_query)
|
||||||
results: list[sqlite3.Row] = await db_cursor.fetchall()
|
results: list[sqlite3.Row] = db_cursor.fetchall()
|
||||||
self.active_playlist = [
|
self.active_playlist = [
|
||||||
{
|
{
|
||||||
"uuid": str(uuid().hex),
|
"uuid": str(uuid().hex),
|
||||||
@ -345,7 +345,7 @@ class RadioUtil:
|
|||||||
"artist": double_space.sub(" ", r["artist"]).strip(),
|
"artist": double_space.sub(" ", r["artist"]).strip(),
|
||||||
"song": double_space.sub(" ", r["song"]).strip(),
|
"song": double_space.sub(" ", r["song"]).strip(),
|
||||||
"album": double_space.sub(" ", r["album"]).strip(),
|
"album": double_space.sub(" ", r["album"]).strip(),
|
||||||
"genre": await self.get_genre(
|
"genre": self.get_genre(
|
||||||
double_space.sub(" ", r["artist"]).strip()
|
double_space.sub(" ", r["artist"]).strip()
|
||||||
),
|
),
|
||||||
"artistsong": double_space.sub(
|
"artistsong": double_space.sub(
|
||||||
@ -400,16 +400,22 @@ class RadioUtil:
|
|||||||
)
|
)
|
||||||
tagger = music_tag.load_file(file_path)
|
tagger = music_tag.load_file(file_path)
|
||||||
album_art = tagger["artwork"].first.data
|
album_art = tagger["artwork"].first.data
|
||||||
async with sqlite3.connect(self.album_art_db_path, timeout=2) as db_conn:
|
with sqlite3.connect(self.album_art_db_path, timeout=2) as db_conn:
|
||||||
async with await db_conn.execute(
|
db_cursor = db_conn.execute(
|
||||||
"INSERT OR IGNORE INTO album_art (track_id, album_art) VALUES(?, ?)",
|
"INSERT OR IGNORE INTO album_art (track_id, album_art) VALUES(?, ?)",
|
||||||
(
|
(
|
||||||
track_id,
|
track_id,
|
||||||
album_art,
|
album_art,
|
||||||
),
|
),
|
||||||
) as db_cursor:
|
)
|
||||||
await db_conn.commit()
|
if isinstance(db_cursor.lastrowid, int):
|
||||||
except:
|
db_conn.commit()
|
||||||
|
else:
|
||||||
|
logging.debug("No row inserted for track_id: %s w/ file_path: %s", track_id,
|
||||||
|
file_path)
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug("cache_album_art Exception: %s",
|
||||||
|
str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def get_album_art(self, track_id: int) -> Optional[bytes]:
|
async def get_album_art(self, track_id: int) -> Optional[bytes]:
|
||||||
@ -421,19 +427,21 @@ class RadioUtil:
|
|||||||
Optional[bytes]
|
Optional[bytes]
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
async with sqlite3.connect(self.album_art_db_path, timeout=2) as db_conn:
|
with sqlite3.connect(self.album_art_db_path, timeout=2) as db_conn:
|
||||||
db_conn.row_factory = sqlite3.Row
|
db_conn.row_factory = sqlite3.Row
|
||||||
query: str = "SELECT album_art FROM album_art WHERE track_id = ?"
|
query: str = "SELECT album_art FROM album_art WHERE track_id = ?"
|
||||||
query_params: tuple[int] = (track_id,)
|
query_params: tuple[int] = (track_id,)
|
||||||
|
|
||||||
async with await db_conn.execute(query, query_params) as db_cursor:
|
db_cursor = db_conn.execute(query, query_params)
|
||||||
result: Optional[Union[sqlite3.Row, bool]] = (
|
result: Optional[Union[sqlite3.Row, bool]] = (
|
||||||
await db_cursor.fetchone()
|
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 Exception as e:
|
||||||
|
logging.debug("get_album_art Exception: %s",
|
||||||
|
str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user