mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
* Removes `MAX_BALANCE` from bank, user `bank.get_max_balance()` now `[p]bankset maxbal` can be used to set the maximum bank balance Signed-off-by: Guy <guyreis96@gmail.com> * Initial Commit Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * I need to make sure I keep aika on her toes. Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * Fixes a few missing kwargs and case consistency Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * Fixes a few missing kwargs and case consistency v2 and typos Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * Reset cooldowns + add changelogs Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * Add 3 extra file formats. Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * IRDUMB - fix capitalization. Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * Fix a silent error, and some incorrect messages. Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com> * Remove unnecessary emojis from queue when they are not needed Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com> * Remove duplicated call in `[p]playlist update` Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com> * Remove duplicated call in `[p]playlist update` Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com> * Resolve conflicts Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * Bring all files up to date + Black Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * Facepalm Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * *Sigh* Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * *Sigh* 2.0 Signed-off-by: Draper <27962761+Drapersniper@users.noreply.github.com> * Merge branch 'V3/develop' of https://github.com/Cog-Creators/Red-DiscordBot into audio-misc-pt1 Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> # Resolve Conflicts: # redbot/cogs/audio/audio.py # redbot/cogs/audio/utils.py * Import missing Typecheck Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Fix Broken docstrings Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Sort Local Tracks Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * 🤦 Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Reorder the sorting of local tracks, `alphanumerical lower then alphanumerical upper` `a comes before A, but B comes after A` Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Black formatting Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Make the local file sorting case insensitive Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Add global blacklist/whitelist + fix some issues with original server based whitelist/blacklist Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Remove the pre-commit yaml Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Nottin to see Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Further improvement to the blacklists Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Further improvement to the blacklists Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Fix the __str__ method on LocalTracks Object * Rename LocalTracks.to_string_hidden() to LocalTracks.to_string_user() To keep it inline with the Query object * Remove encoding pragmas + a few typo fixes * Update some typehints + fix some typos * Remove this duplicate call * Black * fix capitalization * Address preda's review Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Remove the API from the audio cog - Is in direct conflict with goals stated in #2804 - Features this was intended to enable can be enabled in other more appropriate ways later on * changelog * Address Aika's review * Black * *sigh* dont use github web ui * Fuck windows Long live linux... *sigh* no lets ensure windows users can still use local tracks * Merge branch 'V3/develop' of https://github.com/Cog-Creators/Red-DiscordBot into refactoring Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> # Conflicts: # redbot/cogs/audio/audio.py * 👀 + chore Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * facepalm Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * facepalm... again y u h8 me bruh Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * fuk this fuk u tube fuck python fuck all Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * awyehfqwajefhnqeffawefqa eqewarfqaesf qwef qaf qwfr Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * fuck everything Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * oh lord saviour resus i love you just make this work Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Change logic to be no errors within last 10 seconds... this should be a valid work around discord ratelimits caused by the spam * Remove auto deletion Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * See I did a ting Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * irdumb Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * black Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Add an is_url attribute to Query objects * chore Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Black * Address Aikas review Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Hyperlink Playlist names Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Make shit bold Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * why was this here Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * why was this here Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Initial commit * Workinnng Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Improve SQL Statements + migrate from SQL Alchemy + Databases to APSW Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * apsw tested and working Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * chose Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Migrate Playlist to DB 3 TODO 1 Migrate Config to Schema 3 without playlists and update get_playlist methods Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Revert "Migrate Playlist to DB 3 TODO 1 Migrate Config to Schema 3 without playlists and update get_playlist methods" This reverts commit 4af33cff Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Implement schema migration * Lets not touch the deps since #3192 is already adding them * chore * *sigh* Black * Follow the existing logic and always default Playlist to guild scope * wghqjegqf black * Update usage of last_fetched and last_updated to be Ints... However column migration still pending * Some bug fixes * Update usage of last_fetched and last_updated to be Ints... However column migration still pending * working Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * partial match Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * better partial match Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * black Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * I thought i done this before * Delete 3195.misc.1.rst Wrong PR * Thanks Sinbad Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Thanks Sinbad Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Log Errors in init ... Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Update error logs. Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Create index Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * :Drapersweat: Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Chore Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Revert "Chore" This reverts commit edcc9a9f UGHHHH Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Allow removing tracks from queue by URL Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Words matter Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * *sigh* Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * chore Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * arghhh CONFLICTS Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Review sinbads latest comment .. ToDo.. Nuke existing playlist - check version and set version Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * migrate the DB schema to v3 (to keep in line with the schema visioning of Config * Add a Todo * *sigh* conflicts and black * *sigh* black * Passively delete playlist deletion mechanism * Delete Old entries on startup * Since we are dropping the table mightaware make these into JSON for future proofing * Don't Dump strings in JSON field ? :think: * Move some things around to make easier to use 1 connection to the Audio DB * Move some things around to make easier to use 1 connection to the Audio DB * *sigh* * Clean up api * *sigh* black * Red + reorder some variables * 🤦 * how could i forget this ....... * Black Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Black Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Black Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * #automagically Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * FINAFUCKINGLY Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * FINAFUCKINGLY Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Remove unused config default Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Remove the API from the audio Cog (Properly) Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Missed these changes Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * ARGHHH Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Some fixes I've noticed while running through the code line by line Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Translation + UX (show playlist author ID if can't find user) Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * *sigh* missed this one Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * this is no longer needed .... Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * 🤦 Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * fix new lines in error messages Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Black * Sinbads Review Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Sinbads Review Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * *sigh* copy paste Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * imrpove backups Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Im a fucking idiot Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Fix #3238 Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * chore Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * humans Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * humans Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * add play alias to playlists Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Im dumb ... Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Im dumb ... Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * fix new line Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * fix new line Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * show playlist count on playlist picker Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * DJ/Vote system fixes Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * DJ/Vote system fixes Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * *sigh* fix currency check Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * show playlist count on playlist picker Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * DJ/Vote system fixes Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * DJ/Vote system fixes Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * *sigh* fix currency check Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Fix duplicate messages on timeout Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * fix SQL Statement logic Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * fix SQL Statement logic Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Markdown escape Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Markdown escape Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Markdown escape fix Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Markdown escape fix Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * clean up local cache more frequently Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * clean up db more frequently Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Await in hell Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * *sigh* im dumb Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * *sigh* im dumb Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Black cuz I hate red Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Black cuz I hate red Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * StringIO to ByteIO Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * StringIO to ByteIO Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * *sigh* im dumb Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * :Facepalm: the whole purpose of this is so its offline so this can be backed up without being blocking Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Run write queries on ThreadPoolExecutor Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Backup Audio.db Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * *sigh* im dumb Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * blaaaack Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * *sigh* Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * formatting Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * remove duplicated string of code Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * ffs awaits Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> Co-authored-by: Michael H <michael@michaelhall.tech>
373 lines
13 KiB
Python
373 lines
13 KiB
Python
import asyncio
|
|
import concurrent.futures
|
|
import contextlib
|
|
import datetime
|
|
import json
|
|
import logging
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Union, MutableMapping, Mapping
|
|
|
|
import apsw
|
|
|
|
from redbot.core import Config
|
|
from redbot.core.bot import Red
|
|
from redbot.core.data_manager import cog_data_path
|
|
|
|
from .errors import InvalidTableError
|
|
from .sql_statements import *
|
|
from .utils import PlaylistScope
|
|
|
|
log = logging.getLogger("red.audio.database")
|
|
|
|
if TYPE_CHECKING:
|
|
database_connection: apsw.Connection
|
|
_bot: Red
|
|
_config: Config
|
|
else:
|
|
_config = None
|
|
_bot = None
|
|
database_connection = None
|
|
|
|
|
|
SCHEMA_VERSION = 3
|
|
SQLError = apsw.ExecutionCompleteError
|
|
|
|
|
|
_PARSER: Mapping = {
|
|
"youtube": {
|
|
"insert": YOUTUBE_UPSERT,
|
|
"youtube_url": {"query": YOUTUBE_QUERY},
|
|
"update": YOUTUBE_UPDATE,
|
|
},
|
|
"spotify": {
|
|
"insert": SPOTIFY_UPSERT,
|
|
"track_info": {"query": SPOTIFY_QUERY},
|
|
"update": SPOTIFY_UPDATE,
|
|
},
|
|
"lavalink": {
|
|
"insert": LAVALINK_UPSERT,
|
|
"data": {"query": LAVALINK_QUERY, "played": LAVALINK_QUERY_LAST_FETCHED_RANDOM},
|
|
"update": LAVALINK_UPDATE,
|
|
},
|
|
}
|
|
|
|
|
|
def _pass_config_to_databases(config: Config, bot: Red):
|
|
global _config, _bot, database_connection
|
|
if _config is None:
|
|
_config = config
|
|
if _bot is None:
|
|
_bot = bot
|
|
if database_connection is None:
|
|
database_connection = apsw.Connection(
|
|
str(cog_data_path(_bot.get_cog("Audio")) / "Audio.db")
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class PlaylistFetchResult:
|
|
playlist_id: int
|
|
playlist_name: str
|
|
scope_id: int
|
|
author_id: int
|
|
playlist_url: Optional[str] = None
|
|
tracks: List[MutableMapping] = field(default_factory=lambda: [])
|
|
|
|
def __post_init__(self):
|
|
if isinstance(self.tracks, str):
|
|
self.tracks = json.loads(self.tracks)
|
|
|
|
|
|
@dataclass
|
|
class CacheFetchResult:
|
|
query: Optional[Union[str, MutableMapping]]
|
|
last_updated: int
|
|
|
|
def __post_init__(self):
|
|
if isinstance(self.last_updated, int):
|
|
self.updated_on: datetime.datetime = datetime.datetime.fromtimestamp(self.last_updated)
|
|
if isinstance(self.query, str) and all(
|
|
k in self.query for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]
|
|
):
|
|
self.query = json.loads(self.query)
|
|
|
|
|
|
@dataclass
|
|
class CacheLastFetchResult:
|
|
tracks: List[MutableMapping] = field(default_factory=lambda: [])
|
|
|
|
def __post_init__(self):
|
|
if isinstance(self.tracks, str):
|
|
self.tracks = json.loads(self.tracks)
|
|
|
|
|
|
@dataclass
|
|
class CacheGetAllLavalink:
|
|
query: str
|
|
data: List[MutableMapping] = field(default_factory=lambda: [])
|
|
|
|
def __post_init__(self):
|
|
if isinstance(self.data, str):
|
|
self.data = json.loads(self.data)
|
|
|
|
|
|
class CacheInterface:
|
|
def __init__(self):
|
|
self.database = database_connection.cursor()
|
|
|
|
@staticmethod
|
|
def close():
|
|
with contextlib.suppress(Exception):
|
|
database_connection.close()
|
|
|
|
async def init(self):
|
|
self.database.execute(PRAGMA_SET_temp_store)
|
|
self.database.execute(PRAGMA_SET_journal_mode)
|
|
self.database.execute(PRAGMA_SET_read_uncommitted)
|
|
self.maybe_migrate()
|
|
|
|
self.database.execute(LAVALINK_CREATE_TABLE)
|
|
self.database.execute(LAVALINK_CREATE_INDEX)
|
|
self.database.execute(YOUTUBE_CREATE_TABLE)
|
|
self.database.execute(YOUTUBE_CREATE_INDEX)
|
|
self.database.execute(SPOTIFY_CREATE_TABLE)
|
|
self.database.execute(SPOTIFY_CREATE_INDEX)
|
|
|
|
await self.clean_up_old_entries()
|
|
|
|
async def clean_up_old_entries(self):
|
|
max_age = await _config.cache_age()
|
|
maxage = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(days=max_age)
|
|
maxage_int = int(time.mktime(maxage.timetuple()))
|
|
values = {"maxage": maxage_int}
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
|
|
executor.submit(self.database.execute, LAVALINK_DELETE_OLD_ENTRIES, values)
|
|
executor.submit(self.database.execute, YOUTUBE_DELETE_OLD_ENTRIES, values)
|
|
executor.submit(self.database.execute, SPOTIFY_DELETE_OLD_ENTRIES, values)
|
|
|
|
def maybe_migrate(self):
|
|
current_version = self.database.execute(PRAGMA_FETCH_user_version).fetchone()
|
|
if isinstance(current_version, tuple):
|
|
current_version = current_version[0]
|
|
if current_version == SCHEMA_VERSION:
|
|
return
|
|
self.database.execute(PRAGMA_SET_user_version, {"version": SCHEMA_VERSION})
|
|
|
|
async def insert(self, table: str, values: List[MutableMapping]):
|
|
try:
|
|
query = _PARSER.get(table, {}).get("insert")
|
|
if query is None:
|
|
raise InvalidTableError(f"{table} is not a valid table in the database.")
|
|
self.database.execute("BEGIN;")
|
|
self.database.executemany(query, values)
|
|
self.database.execute("COMMIT;")
|
|
except Exception as err:
|
|
log.debug("Error during audio db insert", exc_info=err)
|
|
|
|
async def update(self, table: str, values: Dict[str, Union[str, int]]):
|
|
try:
|
|
table = _PARSER.get(table, {})
|
|
sql_query = table.get("update")
|
|
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
|
values["last_fetched"] = time_now
|
|
if not table:
|
|
raise InvalidTableError(f"{table} is not a valid table in the database.")
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
executor.submit(self.database.execute, sql_query, values)
|
|
except Exception as err:
|
|
log.debug("Error during audio db update", exc_info=err)
|
|
|
|
async def fetch_one(
|
|
self, table: str, query: str, values: Dict[str, Union[str, int]]
|
|
) -> Tuple[Optional[str], bool]:
|
|
table = _PARSER.get(table, {})
|
|
sql_query = table.get(query, {}).get("query")
|
|
if not table:
|
|
raise InvalidTableError(f"{table} is not a valid table in the database.")
|
|
max_age = await _config.cache_age()
|
|
maxage = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(days=max_age)
|
|
maxage_int = int(time.mktime(maxage.timetuple()))
|
|
values.update({"maxage": maxage_int})
|
|
output = self.database.execute(sql_query, values).fetchone() or (None, 0)
|
|
result = CacheFetchResult(*output)
|
|
return result.query, False
|
|
|
|
async def fetch_all(
|
|
self, table: str, query: str, values: Dict[str, Union[str, int]]
|
|
) -> List[CacheLastFetchResult]:
|
|
table = _PARSER.get(table, {})
|
|
sql_query = table.get(query, {}).get("played")
|
|
if not table:
|
|
raise InvalidTableError(f"{table} is not a valid table in the database.")
|
|
|
|
output = []
|
|
for index, row in enumerate(self.database.execute(sql_query, values), start=1):
|
|
if index % 50 == 0:
|
|
await asyncio.sleep(0.01)
|
|
output.append(CacheLastFetchResult(*row))
|
|
return output
|
|
|
|
async def fetch_random(
|
|
self, table: str, query: str, values: Dict[str, Union[str, int]]
|
|
) -> CacheLastFetchResult:
|
|
table = _PARSER.get(table, {})
|
|
sql_query = table.get(query, {}).get("played")
|
|
if not table:
|
|
raise InvalidTableError(f"{table} is not a valid table in the database.")
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
for future in concurrent.futures.as_completed(
|
|
[executor.submit(self.database.execute, sql_query, values)]
|
|
):
|
|
try:
|
|
row = future.result()
|
|
row = row.fetchone()
|
|
except Exception as exc:
|
|
log.debug(f"Failed to completed random fetch from database", exc_info=exc)
|
|
return CacheLastFetchResult(*row)
|
|
|
|
|
|
class PlaylistInterface:
|
|
def __init__(self):
|
|
self.cursor = database_connection.cursor()
|
|
self.cursor.execute(PRAGMA_SET_temp_store)
|
|
self.cursor.execute(PRAGMA_SET_journal_mode)
|
|
self.cursor.execute(PRAGMA_SET_read_uncommitted)
|
|
self.cursor.execute(PLAYLIST_CREATE_TABLE)
|
|
self.cursor.execute(PLAYLIST_CREATE_INDEX)
|
|
|
|
@staticmethod
|
|
def close():
|
|
with contextlib.suppress(Exception):
|
|
database_connection.close()
|
|
|
|
@staticmethod
|
|
def get_scope_type(scope: str) -> int:
|
|
if scope == PlaylistScope.GLOBAL.value:
|
|
table = 1
|
|
elif scope == PlaylistScope.USER.value:
|
|
table = 3
|
|
else:
|
|
table = 2
|
|
return table
|
|
|
|
def fetch(self, scope: str, playlist_id: int, scope_id: int) -> PlaylistFetchResult:
|
|
scope_type = self.get_scope_type(scope)
|
|
row = (
|
|
self.cursor.execute(
|
|
PLAYLIST_FETCH,
|
|
({"playlist_id": playlist_id, "scope_id": scope_id, "scope_type": scope_type}),
|
|
).fetchone()
|
|
or []
|
|
)
|
|
|
|
return PlaylistFetchResult(*row) if row else None
|
|
|
|
async def fetch_all(
|
|
self, scope: str, scope_id: int, author_id=None
|
|
) -> List[PlaylistFetchResult]:
|
|
scope_type = self.get_scope_type(scope)
|
|
if author_id is not None:
|
|
output = []
|
|
for index, row in enumerate(
|
|
self.cursor.execute(
|
|
PLAYLIST_FETCH_ALL_WITH_FILTER,
|
|
({"scope_type": scope_type, "scope_id": scope_id, "author_id": author_id}),
|
|
),
|
|
start=1,
|
|
):
|
|
if index % 50 == 0:
|
|
await asyncio.sleep(0.01)
|
|
output.append(row)
|
|
else:
|
|
output = []
|
|
for index, row in enumerate(
|
|
self.cursor.execute(
|
|
PLAYLIST_FETCH_ALL, ({"scope_type": scope_type, "scope_id": scope_id})
|
|
),
|
|
start=1,
|
|
):
|
|
if index % 50 == 0:
|
|
await asyncio.sleep(0.01)
|
|
output.append(row)
|
|
return [PlaylistFetchResult(*row) for row in output] if output else []
|
|
|
|
async def fetch_all_converter(
|
|
self, scope: str, playlist_name, playlist_id
|
|
) -> List[PlaylistFetchResult]:
|
|
scope_type = self.get_scope_type(scope)
|
|
try:
|
|
playlist_id = int(playlist_id)
|
|
except Exception:
|
|
playlist_id = -1
|
|
|
|
output = []
|
|
for index, row in enumerate(
|
|
self.cursor.execute(
|
|
PLAYLIST_FETCH_ALL_CONVERTER,
|
|
(
|
|
{
|
|
"scope_type": scope_type,
|
|
"playlist_name": playlist_name,
|
|
"playlist_id": playlist_id,
|
|
}
|
|
),
|
|
),
|
|
start=1,
|
|
):
|
|
if index % 50 == 0:
|
|
await asyncio.sleep(0.01)
|
|
output.append(row)
|
|
return [PlaylistFetchResult(*row) for row in output] if output else []
|
|
|
|
def delete(self, scope: str, playlist_id: int, scope_id: int):
|
|
scope_type = self.get_scope_type(scope)
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
executor.submit(
|
|
self.cursor.execute,
|
|
PLAYLIST_DELETE,
|
|
({"playlist_id": playlist_id, "scope_id": scope_id, "scope_type": scope_type}),
|
|
)
|
|
|
|
def delete_scheduled(self):
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
executor.submit(self.cursor.execute, PLAYLIST_DELETE_SCHEDULED)
|
|
|
|
def drop(self, scope: str):
|
|
scope_type = self.get_scope_type(scope)
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
executor.submit(
|
|
self.cursor.execute, PLAYLIST_DELETE_SCOPE, ({"scope_type": scope_type})
|
|
)
|
|
|
|
def create_table(self, scope: str):
|
|
scope_type = self.get_scope_type(scope)
|
|
return self.cursor.execute(PLAYLIST_CREATE_TABLE, ({"scope_type": scope_type}))
|
|
|
|
def upsert(
|
|
self,
|
|
scope: str,
|
|
playlist_id: int,
|
|
playlist_name: str,
|
|
scope_id: int,
|
|
author_id: int,
|
|
playlist_url: Optional[str],
|
|
tracks: List[MutableMapping],
|
|
):
|
|
scope_type = self.get_scope_type(scope)
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
executor.submit(
|
|
self.cursor.execute,
|
|
PLAYLIST_UPSERT,
|
|
{
|
|
"scope_type": str(scope_type),
|
|
"playlist_id": int(playlist_id),
|
|
"playlist_name": str(playlist_name),
|
|
"scope_id": int(scope_id),
|
|
"author_id": int(author_id),
|
|
"playlist_url": playlist_url,
|
|
"tracks": json.dumps(tracks),
|
|
},
|
|
)
|