[3.2][Audio] Part 6 (Last? maybe?) (#3244)

* 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>
This commit is contained in:
Draper
2020-01-04 01:36:09 +00:00
committed by Michael H
parent 1d2dd19244
commit 95e8d60729
43 changed files with 4128 additions and 1938 deletions

View File

@@ -4,25 +4,10 @@ import contextlib
import datetime
import json
import logging
import os
import random
import time
import traceback
from collections import namedtuple
from typing import Callable, Dict, List, Mapping, Optional, Tuple, Union
try:
from sqlite3 import Error as SQLError
from databases import Database
HAS_SQL = True
_ERROR = None
except ImportError as err:
_ERROR = "".join(traceback.format_exception_only(type(err), err)).strip()
HAS_SQL = False
SQLError = err.__class__
Database = None
from typing import Callable, List, MutableMapping, Optional, TYPE_CHECKING, Tuple, Union, NoReturn
import aiohttp
import discord
@@ -32,129 +17,38 @@ from lavalink.rest_api import LoadResult
from redbot.core import Config, commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from . import audio_dataclasses
from .errors import InvalidTableError, SpotifyFetchError, YouTubeApiError, DatabaseError
from .databases import CacheInterface, SQLError
from .errors import DatabaseError, SpotifyFetchError, YouTubeApiError, TrackEnqueueError
from .playlists import get_playlist
from .utils import CacheLevel, Notifier, is_allowed, queue_duration, track_limit
log = logging.getLogger("red.audio.cache")
_ = Translator("Audio", __file__)
_DROP_YOUTUBE_TABLE = "DROP TABLE youtube;"
_CREATE_YOUTUBE_TABLE = """
CREATE TABLE IF NOT EXISTS youtube(
id INTEGER PRIMARY KEY AUTOINCREMENT,
track_info TEXT,
youtube_url TEXT,
last_updated TEXT,
last_fetched TEXT
);
"""
_CREATE_UNIQUE_INDEX_YOUTUBE_TABLE = (
"CREATE UNIQUE INDEX IF NOT EXISTS idx_youtube_url ON youtube (track_info, youtube_url);"
)
_INSERT_YOUTUBE_TABLE = """
INSERT OR REPLACE INTO
youtube(track_info, youtube_url, last_updated, last_fetched)
VALUES (:track_info, :track_url, :last_updated, :last_fetched);
"""
_QUERY_YOUTUBE_TABLE = "SELECT * FROM youtube WHERE track_info=:track;"
_UPDATE_YOUTUBE_TABLE = """UPDATE youtube
SET last_fetched=:last_fetched
WHERE track_info=:track;"""
_DROP_SPOTIFY_TABLE = "DROP TABLE spotify;"
_CREATE_UNIQUE_INDEX_SPOTIFY_TABLE = (
"CREATE UNIQUE INDEX IF NOT EXISTS idx_spotify_uri ON spotify (id, type, uri);"
)
_CREATE_SPOTIFY_TABLE = """
CREATE TABLE IF NOT EXISTS spotify(
id TEXT,
type TEXT,
uri TEXT,
track_name TEXT,
artist_name TEXT,
song_url TEXT,
track_info TEXT,
last_updated TEXT,
last_fetched TEXT
);
"""
_INSERT_SPOTIFY_TABLE = """
INSERT OR REPLACE INTO
spotify(id, type, uri, track_name, artist_name,
song_url, track_info, last_updated, last_fetched)
VALUES (:id, :type, :uri, :track_name, :artist_name,
:song_url, :track_info, :last_updated, :last_fetched);
"""
_QUERY_SPOTIFY_TABLE = "SELECT * FROM spotify WHERE uri=:uri;"
_UPDATE_SPOTIFY_TABLE = """UPDATE spotify
SET last_fetched=:last_fetched
WHERE uri=:uri;"""
_DROP_LAVALINK_TABLE = "DROP TABLE lavalink;"
_CREATE_LAVALINK_TABLE = """
CREATE TABLE IF NOT EXISTS lavalink(
query TEXT,
data BLOB,
last_updated TEXT,
last_fetched TEXT
);
"""
_CREATE_UNIQUE_INDEX_LAVALINK_TABLE = (
"CREATE UNIQUE INDEX IF NOT EXISTS idx_lavalink_query ON lavalink (query);"
)
_INSERT_LAVALINK_TABLE = """
INSERT OR REPLACE INTO
lavalink(query, data, last_updated, last_fetched)
VALUES (:query, :data, :last_updated, :last_fetched);
"""
_QUERY_LAVALINK_TABLE = "SELECT * FROM lavalink WHERE query=:query;"
_QUERY_LAST_FETCHED_LAVALINK_TABLE = (
"SELECT * FROM lavalink "
"WHERE last_fetched LIKE :day1"
" OR last_fetched LIKE :day2"
" OR last_fetched LIKE :day3"
" OR last_fetched LIKE :day4"
" OR last_fetched LIKE :day5"
" OR last_fetched LIKE :day6"
" OR last_fetched LIKE :day7;"
)
_UPDATE_LAVALINK_TABLE = """UPDATE lavalink
SET last_fetched=:last_fetched
WHERE query=:query;"""
_PARSER = {
"youtube": {
"insert": _INSERT_YOUTUBE_TABLE,
"youtube_url": {"query": _QUERY_YOUTUBE_TABLE},
"update": _UPDATE_YOUTUBE_TABLE,
},
"spotify": {
"insert": _INSERT_SPOTIFY_TABLE,
"track_info": {"query": _QUERY_SPOTIFY_TABLE},
"update": _UPDATE_SPOTIFY_TABLE,
},
"lavalink": {
"insert": _INSERT_LAVALINK_TABLE,
"data": {"query": _QUERY_LAVALINK_TABLE, "played": _QUERY_LAST_FETCHED_LAVALINK_TABLE},
"update": _UPDATE_LAVALINK_TABLE,
},
}
_TOP_100_GLOBALS = "https://www.youtube.com/playlist?list=PL4fGSI1pDJn6puJdseH2Rt9sMvt9E2M4i"
_TOP_100_US = "https://www.youtube.com/playlist?list=PL4fGSI1pDJn5rWitrRWFKdm-ulaFiIyoK"
if TYPE_CHECKING:
_database: CacheInterface
_bot: Red
_config: Config
else:
_database = None
_bot = None
_config = None
def _pass_config_to_apis(config: Config, bot: Red):
global _database, _config, _bot
if _config is None:
_config = config
if _bot is None:
_bot = bot
if _database is None:
_database = CacheInterface()
class SpotifyAPI:
"""Wrapper for the Spotify API."""
@@ -162,17 +56,19 @@ class SpotifyAPI:
def __init__(self, bot: Red, session: aiohttp.ClientSession):
self.bot = bot
self.session = session
self.spotify_token = None
self.spotify_token: Optional[MutableMapping[str, Union[str, int]]] = None
self.client_id = None
self.client_secret = None
@staticmethod
async def _check_token(token: dict):
async def _check_token(token: MutableMapping):
now = int(time.time())
return token["expires_at"] - now < 60
@staticmethod
def _make_token_auth(client_id: Optional[str], client_secret: Optional[str]) -> dict:
def _make_token_auth(
client_id: Optional[str], client_secret: Optional[str]
) -> MutableMapping[str, Union[str, int]]:
if client_id is None:
client_id = ""
if client_secret is None:
@@ -181,7 +77,9 @@ class SpotifyAPI:
auth_header = base64.b64encode((client_id + ":" + client_secret).encode("ascii"))
return {"Authorization": "Basic %s" % auth_header.decode("ascii")}
async def _make_get(self, url: str, headers: dict = None, params: dict = None) -> dict:
async def _make_get(
self, url: str, headers: MutableMapping = None, params: MutableMapping = None
) -> MutableMapping[str, str]:
if params is None:
params = {}
async with self.session.request("GET", url, params=params, headers=headers) as r:
@@ -193,13 +91,12 @@ class SpotifyAPI:
)
return await r.json()
async def _get_auth(self):
if self.client_id is None or self.client_secret is None:
tokens = await self.bot.get_shared_api_tokens("spotify")
self.client_id = tokens.get("client_id", "")
self.client_secret = tokens.get("client_secret", "")
async def _get_auth(self) -> NoReturn:
tokens = await self.bot.get_shared_api_tokens("spotify")
self.client_id = tokens.get("client_id", "")
self.client_secret = tokens.get("client_secret", "")
async def _request_token(self) -> dict:
async def _request_token(self) -> MutableMapping[str, Union[str, int]]:
await self._get_auth()
payload = {"grant_type": "client_credentials"}
@@ -223,7 +120,9 @@ class SpotifyAPI:
log.debug("Created a new access token for Spotify: {0}".format(token))
return self.spotify_token["access_token"]
async def post_call(self, url: str, payload: dict, headers: dict = None) -> dict:
async def post_call(
self, url: str, payload: MutableMapping, headers: MutableMapping = None
) -> MutableMapping[str, Union[str, int]]:
async with self.session.post(url, data=payload, headers=headers) as r:
if r.status != 200:
log.debug(
@@ -233,13 +132,15 @@ class SpotifyAPI:
)
return await r.json()
async def get_call(self, url: str, params: dict) -> dict:
async def get_call(
self, url: str, params: MutableMapping
) -> MutableMapping[str, Union[str, int]]:
token = await self._get_spotify_token()
return await self._make_get(
url, params=params, headers={"Authorization": "Bearer {0}".format(token)}
)
async def get_categories(self) -> List[Dict[str, str]]:
async def get_categories(self) -> List[MutableMapping]:
url = "https://api.spotify.com/v1/browse/categories"
params = {}
result = await self.get_call(url, params=params)
@@ -278,10 +179,9 @@ class YouTubeAPI:
self.session = session
self.api_key = None
async def _get_api_key(self,) -> Optional[str]:
if self.api_key is None:
tokens = await self.bot.get_shared_api_tokens("youtube")
self.api_key = tokens.get("api_key", "")
async def _get_api_key(self,) -> str:
tokens = await self.bot.get_shared_api_tokens("youtube")
self.api_key = tokens.get("api_key", "")
return self.api_key
async def get_call(self, query: str) -> Optional[str]:
@@ -310,122 +210,39 @@ class YouTubeAPI:
@cog_i18n(_)
class MusicCache:
"""
Handles music queries to the Spotify and Youtube Data API.
"""Handles music queries to the Spotify and Youtube Data API.
Always tries the Cache first.
"""
def __init__(self, bot: Red, session: aiohttp.ClientSession, path: str):
def __init__(self, bot: Red, session: aiohttp.ClientSession):
self.bot = bot
self.spotify_api: SpotifyAPI = SpotifyAPI(bot, session)
self.youtube_api: YouTubeAPI = YouTubeAPI(bot, session)
self._session: aiohttp.ClientSession = session
if HAS_SQL:
self.database: Database = Database(
f'sqlite:///{os.path.abspath(str(os.path.join(path, "cache.db")))}'
)
else:
self.database = None
self.database = _database
self._tasks: dict = {}
self._tasks: MutableMapping = {}
self._lock: asyncio.Lock = asyncio.Lock()
self.config: Optional[Config] = None
async def initialize(self, config: Config):
if HAS_SQL:
await self.database.connect()
await self.database.execute(query="PRAGMA temp_store = 2;")
await self.database.execute(query="PRAGMA journal_mode = wal;")
await self.database.execute(query="PRAGMA wal_autocheckpoint;")
await self.database.execute(query="PRAGMA read_uncommitted = 1;")
await self.database.execute(query=_CREATE_LAVALINK_TABLE)
await self.database.execute(query=_CREATE_UNIQUE_INDEX_LAVALINK_TABLE)
await self.database.execute(query=_CREATE_YOUTUBE_TABLE)
await self.database.execute(query=_CREATE_UNIQUE_INDEX_YOUTUBE_TABLE)
await self.database.execute(query=_CREATE_SPOTIFY_TABLE)
await self.database.execute(query=_CREATE_UNIQUE_INDEX_SPOTIFY_TABLE)
self.config = config
async def close(self):
if HAS_SQL:
await self.database.execute(query="PRAGMA optimize;")
await self.database.disconnect()
async def insert(self, table: str, values: List[dict]):
# if table == "spotify":
# return
if HAS_SQL:
query = _PARSER.get(table, {}).get("insert")
if query is None:
raise InvalidTableError(f"{table} is not a valid table in the database.")
await self.database.execute_many(query=query, values=values)
async def update(self, table: str, values: Dict[str, str]):
# if table == "spotify":
# return
if HAS_SQL:
table = _PARSER.get(table, {})
sql_query = table.get("update")
time_now = str(datetime.datetime.now(datetime.timezone.utc))
values["last_fetched"] = time_now
if not table:
raise InvalidTableError(f"{table} is not a valid table in the database.")
await self.database.fetch_one(query=sql_query, values=values)
async def fetch_one(
self, table: str, query: str, values: Dict[str, str]
) -> Tuple[Optional[str], bool]:
table = _PARSER.get(table, {})
sql_query = table.get(query, {}).get("query")
if HAS_SQL:
if not table:
raise InvalidTableError(f"{table} is not a valid table in the database.")
row = await self.database.fetch_one(query=sql_query, values=values)
last_updated = getattr(row, "last_updated", None)
need_update = True
with contextlib.suppress(TypeError):
if last_updated:
last_update = datetime.datetime.fromisoformat(
last_updated
) + datetime.timedelta(days=await self.config.cache_age())
last_update.replace(tzinfo=datetime.timezone.utc)
need_update = last_update < datetime.datetime.now(datetime.timezone.utc)
return getattr(row, query, None), need_update if table != "spotify" else True
else:
return None, True
# TODO: Create a task to remove entries
# from DB that haven't been fetched in x days ... customizable by Owner
async def fetch_all(self, table: str, query: str, values: Dict[str, str]) -> List[Mapping]:
if HAS_SQL:
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.")
return await self.database.fetch_all(query=sql_query, values=values)
return []
await _database.init()
@staticmethod
def _spotify_format_call(qtype: str, key: str) -> Tuple[str, dict]:
def _spotify_format_call(qtype: str, key: str) -> Tuple[str, MutableMapping]:
params = {}
if qtype == "album":
query = "https://api.spotify.com/v1/albums/{0}/tracks".format(key)
query = f"https://api.spotify.com/v1/albums/{key}/tracks"
elif qtype == "track":
query = "https://api.spotify.com/v1/tracks/{0}".format(key)
query = f"https://api.spotify.com/v1/tracks/{key}"
else:
query = "https://api.spotify.com/v1/playlists/{0}/tracks".format(key)
query = f"https://api.spotify.com/v1/playlists/{key}/tracks"
return query, params
@staticmethod
def _get_spotify_track_info(track_data: dict) -> Tuple[str, ...]:
def _get_spotify_track_info(track_data: MutableMapping) -> Tuple[str, ...]:
artist_name = track_data["artists"][0]["name"]
track_name = track_data["name"]
track_info = f"{track_name} {artist_name}"
@@ -451,7 +268,7 @@ class MusicCache:
total_tracks = len(tracks)
database_entries = []
track_count = 0
time_now = str(datetime.datetime.now(datetime.timezone.utc))
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
youtube_cache = CacheLevel.set_youtube().is_subset(current_cache_level)
for track in tracks:
if track.get("error", {}).get("message") == "invalid id":
@@ -484,7 +301,7 @@ class MusicCache:
if youtube_cache:
update = True
with contextlib.suppress(SQLError):
val, update = await self.fetch_one(
(val, update) = await self.database.fetch_one(
"youtube", "youtube_url", {"track": track_info}
)
if update:
@@ -517,7 +334,7 @@ class MusicCache:
) -> str:
track_url = await self.youtube_api.get_call(track_info)
if CacheLevel.set_youtube().is_subset(current_cache_level) and track_url:
time_now = str(datetime.datetime.now(datetime.timezone.utc))
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
task = (
"insert",
(
@@ -540,12 +357,12 @@ class MusicCache:
query_type: str,
uri: str,
recursive: Union[str, bool] = False,
params=None,
params: MutableMapping = None,
notifier: Optional[Notifier] = None,
) -> Union[dict, List[str]]:
) -> Union[MutableMapping, List[str]]:
if recursive is False:
call, params = self._spotify_format_call(query_type, uri)
(call, params) = self._spotify_format_call(query_type, uri)
results = await self.spotify_api.get_call(call, params)
else:
results = await self.spotify_api.get_call(recursive, params)
@@ -608,8 +425,7 @@ class MusicCache:
skip_youtube: bool = False,
notifier: Optional[Notifier] = None,
) -> List[str]:
"""
Queries the Database then falls back to Spotify and YouTube APIs.
"""Queries the Database then falls back to Spotify and YouTube APIs.
Parameters
----------
@@ -628,14 +444,12 @@ class MusicCache:
List[str]
List of Youtube URLs.
"""
current_cache_level = (
CacheLevel(await self.config.cache_level()) if HAS_SQL else CacheLevel.none()
)
current_cache_level = CacheLevel(await self.config.cache_level())
cache_enabled = CacheLevel.set_spotify().is_subset(current_cache_level)
if query_type == "track" and cache_enabled:
update = True
with contextlib.suppress(SQLError):
val, update = await self.fetch_one(
(val, update) = await self.database.fetch_one(
"spotify", "track_info", {"uri": f"spotify:track:{uri}"}
)
if update:
@@ -673,9 +487,7 @@ class MusicCache:
track_list = []
has_not_allowed = False
try:
current_cache_level = (
CacheLevel(await self.config.cache_level()) if HAS_SQL else CacheLevel.none()
)
current_cache_level = CacheLevel(await self.config.cache_level())
guild_data = await self.config.guild(ctx.guild).all()
# now = int(time.time())
@@ -698,7 +510,7 @@ class MusicCache:
return track_list
database_entries = []
time_now = str(datetime.datetime.now(datetime.timezone.utc))
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
youtube_cache = CacheLevel.set_youtube().is_subset(current_cache_level)
spotify_cache = CacheLevel.set_spotify().is_subset(current_cache_level)
@@ -730,7 +542,7 @@ class MusicCache:
if youtube_cache:
update = True
with contextlib.suppress(SQLError):
val, update = await self.fetch_one(
(val, update) = await self.database.fetch_one(
"youtube", "youtube_url", {"track": track_info}
)
if update:
@@ -745,7 +557,7 @@ class MusicCache:
if val:
try:
result, called_api = await self.lavalink_query(
(result, called_api) = await self.lavalink_query(
ctx, player, audio_dataclasses.Query.process_input(val)
)
except (RuntimeError, aiohttp.ServerDisconnectedError):
@@ -760,7 +572,7 @@ class MusicCache:
lock(ctx, False)
error_embed = discord.Embed(
colour=await ctx.embed_colour(),
title=_("Player timedout, skipping remaning tracks."),
title=_("Player timeout, skipping remaining tracks."),
)
await notifier.update_embed(error_embed)
break
@@ -771,16 +583,6 @@ class MusicCache:
key = "lavalink"
seconds = "???"
second_key = None
# if track_count == 2:
# five_time = int(time.time()) - now
# if track_count >= 2:
# remain_tracks = total_tracks - track_count
# time_remain = (remain_tracks / 2) * five_time
# if track_count < total_tracks:
# seconds = dynamic_time(int(time_remain))
# if track_count == total_tracks:
# seconds = "0s"
# second_key = "lavalink_time"
await notifier.notify_user(
current=track_count,
total=total_tracks,
@@ -837,16 +639,14 @@ class MusicCache:
await player.play()
if len(track_list) == 0:
if not has_not_allowed:
embed3 = discord.Embed(
colour=await ctx.embed_colour(),
title=_(
raise SpotifyFetchError(
message=_(
"Nothing found.\nThe YouTube API key may be invalid "
"or you may be rate limited on YouTube's search service.\n"
"Check the YouTube API key again and follow the instructions "
"at `{prefix}audioset youtubeapi`."
).format(prefix=ctx.prefix),
).format(prefix=ctx.prefix)
)
await ctx.send(embed=embed3)
player.maybe_shuffle()
if enqueue and tracks_from_spotify:
if total_tracks > enqueued_tracks:
@@ -885,15 +685,15 @@ class MusicCache:
return track_list
async def youtube_query(self, ctx: commands.Context, track_info: str) -> str:
current_cache_level = (
CacheLevel(await self.config.cache_level()) if HAS_SQL else CacheLevel.none()
)
current_cache_level = CacheLevel(await self.config.cache_level())
cache_enabled = CacheLevel.set_youtube().is_subset(current_cache_level)
val = None
if cache_enabled:
update = True
with contextlib.suppress(SQLError):
val, update = await self.fetch_one("youtube", "youtube_url", {"track": track_info})
(val, update) = await self.database.fetch_one(
"youtube", "youtube_url", {"track": track_info}
)
if update:
val = None
if val is None:
@@ -914,10 +714,8 @@ class MusicCache:
query: audio_dataclasses.Query,
forced: bool = False,
) -> Tuple[LoadResult, bool]:
"""
A replacement for :code:`lavalink.Player.load_tracks`.
This will try to get a valid cached entry first if not found or if in valid
it will then call the lavalink API.
"""A replacement for :code:`lavalink.Player.load_tracks`. This will try to get a valid
cached entry first if not found or if in valid it will then call the lavalink API.
Parameters
----------
@@ -934,9 +732,7 @@ class MusicCache:
Tuple[lavalink.LoadResult, bool]
Tuple with the Load result and whether or not the API was called.
"""
current_cache_level = (
CacheLevel(await self.config.cache_level()) if HAS_SQL else CacheLevel.none()
)
current_cache_level = CacheLevel(await self.config.cache_level())
cache_enabled = CacheLevel.set_lavalink().is_subset(current_cache_level)
val = None
_raw_query = audio_dataclasses.Query.process_input(query)
@@ -944,14 +740,15 @@ class MusicCache:
if cache_enabled and not forced and not _raw_query.is_local:
update = True
with contextlib.suppress(SQLError):
val, update = await self.fetch_one("lavalink", "data", {"query": query})
(val, update) = await self.database.fetch_one("lavalink", "data", {"query": query})
if update:
val = None
if val:
if val and not isinstance(val, str):
log.debug(f"Querying Local Database for {query}")
task = ("update", ("lavalink", {"query": query}))
self.append_task(ctx, *task)
if val and not forced:
data = json.loads(val)
data = val
data["query"] = query
results = LoadResult(data)
called_api = False
@@ -965,6 +762,8 @@ class MusicCache:
results = await player.load_tracks(query)
except KeyError:
results = None
except RuntimeError:
raise TrackEnqueueError
if results is None:
results = LoadResult({"loadType": "LOAD_FAILED", "playlistInfo": {}, "tracks": []})
if (
@@ -975,7 +774,7 @@ class MusicCache:
and results.tracks
):
with contextlib.suppress(SQLError):
time_now = str(datetime.datetime.now(datetime.timezone.utc))
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
task = (
"insert",
(
@@ -1003,10 +802,12 @@ class MusicCache:
tasks = self._tasks[ctx.message.id]
del self._tasks[ctx.message.id]
await asyncio.gather(
*[self.insert(*a) for a in tasks["insert"]], return_exceptions=True
*[self.database.insert(*a) for a in tasks["insert"]],
return_exceptions=True,
)
await asyncio.gather(
*[self.update(*a) for a in tasks["update"]], return_exceptions=True
*[self.database.update(*a) for a in tasks["update"]],
return_exceptions=True,
)
log.debug(f"Completed database writes for {lock_id} " f"({lock_author})")
@@ -1015,16 +816,16 @@ class MusicCache:
log.debug("Running pending writes to database")
with contextlib.suppress(Exception):
tasks = {"update": [], "insert": []}
for k, task in self._tasks.items():
for (k, task) in self._tasks.items():
for t, args in task.items():
tasks[t].append(args)
self._tasks = {}
await asyncio.gather(
*[self.insert(*a) for a in tasks["insert"]], return_exceptions=True
*[self.database.insert(*a) for a in tasks["insert"]], return_exceptions=True
)
await asyncio.gather(
*[self.update(*a) for a in tasks["update"]], return_exceptions=True
*[self.database.update(*a) for a in tasks["update"]], return_exceptions=True
)
log.debug("Completed pending writes to database have finished")
@@ -1034,29 +835,26 @@ class MusicCache:
self._tasks[lock_id] = {"update": [], "insert": []}
self._tasks[lock_id][event].append(task)
async def play_random(self):
async def get_random_from_db(self):
tracks = []
try:
query_data = {}
for i in range(1, 8):
date = (
"%"
+ str(
(
datetime.datetime.now(datetime.timezone.utc)
- datetime.timedelta(days=i)
).date()
)
+ "%"
)
query_data[f"day{i}"] = date
date = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=7)
date = int(date.timestamp())
query_data["day"] = date
max_age = await self.config.cache_age()
maxage = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(
days=max_age
)
maxage_int = int(time.mktime(maxage.timetuple()))
query_data["maxage"] = maxage_int
vals = await self.fetch_all("lavalink", "data", query_data)
recently_played = [r.data for r in vals if r]
vals = await self.database.fetch_all("lavalink", "data", query_data)
recently_played = [r.tracks for r in vals if r]
if recently_played:
track = random.choice(recently_played)
results = LoadResult(json.loads(track))
results = LoadResult(track)
tracks = list(results.tracks)
except Exception:
tracks = []
@@ -1065,9 +863,7 @@ class MusicCache:
async def autoplay(self, player: lavalink.Player):
autoplaylist = await self.config.guild(player.channel.guild).autoplaylist()
current_cache_level = (
CacheLevel(await self.config.cache_level()) if HAS_SQL else CacheLevel.none()
)
current_cache_level = CacheLevel(await self.config.cache_level())
cache_enabled = CacheLevel.set_lavalink().is_subset(current_cache_level)
playlist = None
tracks = None
@@ -1084,10 +880,10 @@ class MusicCache:
if not tracks or not getattr(playlist, "tracks", None):
if cache_enabled:
tracks = await self.play_random()
tracks = await self.get_random_from_db()
if not tracks:
ctx = namedtuple("Context", "message")
results, called_api = await self.lavalink_query(
(results, called_api) = await self.lavalink_query(
ctx(player.channel.guild),
player,
audio_dataclasses.Query.process_input(_TOP_100_US),
@@ -1124,7 +920,7 @@ class MusicCache:
continue
valid = True
track.extras = {"autoplay": True}
track.extras["autoplay"] = True
player.add(player.channel.guild.me, track)
self.bot.dispatch(
"red_audio_track_auto_play", player.channel.guild, track, player.channel.guild.me