Audio Fixes (#4492)

* handles #4491

* add typing indicators to audio playlists commands like discussed with aika.

* recheck perms upon change of token to avoid needing a reload.

* Ensure the player lock is always released... on rewrite to this as a callback to the task.

* ffs

* resolves#4495

* missed one

* aaaaaaaaa

* fix https://canary.discord.com/channels/133049272517001216/387398816317440000/766711707921678396

* some tweaks

* Clear errors to users around YouTube Quota
This commit is contained in:
Draper
2020-10-20 17:57:02 +01:00
committed by GitHub
parent 335e2a7c25
commit e31196d19f
39 changed files with 1255 additions and 1014 deletions

View File

@@ -3,18 +3,21 @@ import json
import logging
from collections import namedtuple
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, MutableMapping, Optional, Union
import discord
import lavalink
from redbot.core.bot import Red
from redbot.core.i18n import Translator
from redbot.core.utils.chat_formatting import humanize_list
from ..errors import InvalidPlaylistScope, MissingAuthor, MissingGuild
from ..utils import PlaylistScope
log = logging.getLogger("red.cogs.Audio.api.utils")
_ = Translator("Audio", Path(__file__))
@dataclass

View File

@@ -4,6 +4,7 @@ import json
import logging
from copy import copy
from pathlib import Path
from typing import TYPE_CHECKING, Mapping, Optional, Union
import aiohttp
@@ -12,6 +13,7 @@ from lavalink.rest_api import LoadResult
from redbot.core import Config
from redbot.core.bot import Red
from redbot.core.commands import Cog
from redbot.core.i18n import Translator
from ..audio_dataclasses import Query
from ..audio_logging import IS_DEBUG, debug_exc_log
@@ -20,7 +22,7 @@ if TYPE_CHECKING:
from .. import Audio
_API_URL = "https://api.redbot.app/"
_ = Translator("Audio", Path(__file__))
log = logging.getLogger("red.cogs.Audio.api.GlobalDB")
@@ -38,8 +40,9 @@ class GlobalCacheWrapper:
self._token: Mapping[str, str] = {}
self.cog = cog
def update_token(self, new_token: Mapping[str, str]):
async def update_token(self, new_token: Mapping[str, str]):
self._token = new_token
await self.get_perms()
async def _get_api_key(
self,
@@ -165,7 +168,6 @@ class GlobalCacheWrapper:
global_api_user = copy(self.cog.global_api_user)
await self._get_api_key()
is_enabled = await self.config.global_db_enabled()
await self._get_api_key()
if (not is_enabled) or self.api_key is None:
return global_api_user
with contextlib.suppress(Exception):

View File

@@ -7,6 +7,7 @@ import random
import time
from collections import namedtuple
from pathlib import Path
from typing import TYPE_CHECKING, Callable, List, MutableMapping, Optional, Tuple, Union, cast
import aiohttp
@@ -23,7 +24,7 @@ from redbot.core.utils.dbtools import APSWConnectionWrapper
from ..audio_dataclasses import Query
from ..audio_logging import IS_DEBUG, debug_exc_log
from ..errors import DatabaseError, SpotifyFetchError, TrackEnqueueError
from ..errors import DatabaseError, SpotifyFetchError, TrackEnqueueError, YouTubeApiError
from ..utils import CacheLevel, Notifier
from .api_utils import LavalinkCacheFetchForGlobalResult
from .global_db import GlobalCacheWrapper
@@ -37,7 +38,7 @@ from .youtube import YouTubeWrapper
if TYPE_CHECKING:
from .. import Audio
_ = Translator("Audio", __file__)
_ = Translator("Audio", Path(__file__))
log = logging.getLogger("red.cogs.Audio.api.AudioAPIInterface")
_TOP_100_US = "https://www.youtube.com/playlist?list=PL4fGSI1pDJn5rWitrRWFKdm-ulaFiIyoK"
# TODO: Get random from global Cache
@@ -209,6 +210,7 @@ class AudioAPIInterface:
track_count = 0
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
youtube_cache = CacheLevel.set_youtube().is_subset(current_cache_level)
youtube_api_error = None
async for track in AsyncIter(tracks):
if isinstance(track, str):
break
@@ -248,9 +250,13 @@ class AudioAPIInterface:
debug_exc_log(log, exc, f"Failed to fetch {track_info} from YouTube table")
if val is None:
val = await self.fetch_youtube_query(
ctx, track_info, current_cache_level=current_cache_level
)
try:
val = await self.fetch_youtube_query(
ctx, track_info, current_cache_level=current_cache_level
)
except YouTubeApiError as err:
val = None
youtube_api_error = err.message
if youtube_cache and val:
task = ("update", ("youtube", {"track": track_info}))
self.append_task(ctx, *task)
@@ -261,6 +267,13 @@ class AudioAPIInterface:
track_count += 1
if notifier is not None and ((track_count % 2 == 0) or (track_count == total_tracks)):
await notifier.notify_user(current=track_count, total=total_tracks, key="youtube")
if notifier is not None and youtube_api_error:
error_embed = discord.Embed(
colour=await ctx.embed_colour(),
title=_("Failing to get tracks, skipping remaining."),
)
await notifier.update_embed(error_embed)
break
if CacheLevel.set_spotify().is_subset(current_cache_level):
task = ("insert", ("spotify", database_entries))
self.append_task(ctx, *task)
@@ -438,6 +451,7 @@ class AudioAPIInterface:
global_entry = globaldb_toggle and query_global
track_list: List = []
has_not_allowed = False
youtube_api_error = None
try:
current_cache_level = CacheLevel(await self.config.cache_level())
guild_data = await self.config.guild(ctx.guild).all()
@@ -461,7 +475,6 @@ class AudioAPIInterface:
return track_list
database_entries = []
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)
async for track_count, track in AsyncIter(tracks_from_spotify).enumerate(start=1):
@@ -506,52 +519,61 @@ class AudioAPIInterface:
llresponse = LoadResult(llresponse)
val = llresponse or None
if val is None:
val = await self.fetch_youtube_query(
ctx, track_info, current_cache_level=current_cache_level
)
if youtube_cache and val and llresponse is None:
task = ("update", ("youtube", {"track": track_info}))
self.append_task(ctx, *task)
try:
val = await self.fetch_youtube_query(
ctx, track_info, current_cache_level=current_cache_level
)
except YouTubeApiError as err:
val = None
youtube_api_error = err.message
if not youtube_api_error:
if youtube_cache and val and llresponse is None:
task = ("update", ("youtube", {"track": track_info}))
self.append_task(ctx, *task)
if isinstance(llresponse, LoadResult):
track_object = llresponse.tracks
elif val:
result = None
if should_query_global:
llresponse = await self.global_cache_api.get_call(val)
if llresponse:
if llresponse.get("loadType") == "V2_COMPACT":
llresponse["loadType"] = "V2_COMPAT"
llresponse = LoadResult(llresponse)
result = llresponse or None
if not result:
try:
(result, called_api) = await self.fetch_track(
ctx,
player,
Query.process_input(val, self.cog.local_folder_current_path),
forced=forced,
should_query_global=not should_query_global,
)
except (RuntimeError, aiohttp.ServerDisconnectedError):
lock(ctx, False)
error_embed = discord.Embed(
colour=await ctx.embed_colour(),
title=_("The connection was reset while loading the playlist."),
)
if notifier is not None:
await notifier.update_embed(error_embed)
break
except asyncio.TimeoutError:
lock(ctx, False)
error_embed = discord.Embed(
colour=await ctx.embed_colour(),
title=_("Player timeout, skipping remaining tracks."),
)
if notifier is not None:
await notifier.update_embed(error_embed)
break
track_object = result.tracks
if isinstance(llresponse, LoadResult):
track_object = llresponse.tracks
elif val:
result = None
if should_query_global:
llresponse = await self.global_cache_api.get_call(val)
if llresponse:
if llresponse.get("loadType") == "V2_COMPACT":
llresponse["loadType"] = "V2_COMPAT"
llresponse = LoadResult(llresponse)
result = llresponse or None
if not result:
try:
(result, called_api) = await self.fetch_track(
ctx,
player,
Query.process_input(val, self.cog.local_folder_current_path),
forced=forced,
should_query_global=not should_query_global,
)
except (RuntimeError, aiohttp.ServerDisconnectedError):
lock(ctx, False)
error_embed = discord.Embed(
colour=await ctx.embed_colour(),
title=_(
"The connection was reset while loading the playlist."
),
)
if notifier is not None:
await notifier.update_embed(error_embed)
break
except asyncio.TimeoutError:
lock(ctx, False)
error_embed = discord.Embed(
colour=await ctx.embed_colour(),
title=_("Player timeout, skipping remaining tracks."),
)
if notifier is not None:
await notifier.update_embed(error_embed)
break
track_object = result.tracks
else:
track_object = []
else:
track_object = []
if (track_count % 2 == 0) or (track_count == total_tracks):
@@ -567,13 +589,16 @@ class AudioAPIInterface:
seconds=seconds,
)
if consecutive_fails >= (100 if global_entry else 10):
if youtube_api_error or consecutive_fails >= (20 if global_entry else 10):
error_embed = discord.Embed(
colour=await ctx.embed_colour(),
title=_("Failing to get tracks, skipping remaining."),
)
if notifier is not None:
await notifier.update_embed(error_embed)
if youtube_api_error:
lock(ctx, False)
raise SpotifyFetchError(message=youtube_api_error)
break
if not track_object:
consecutive_fails += 1
@@ -631,16 +656,6 @@ class AudioAPIInterface:
if not player.current:
await player.play()
if not track_list and not has_not_allowed:
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`."
)
)
player.maybe_shuffle()
if enqueue and tracks_from_spotify:
if total_tracks > enqueued_tracks:
maxlength_msg = _(" {bad_tracks} tracks cannot be queued.").format(
@@ -667,6 +682,16 @@ class AudioAPIInterface:
if notifier is not None:
await notifier.update_embed(embed)
lock(ctx, False)
if not track_list and not has_not_allowed:
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`."
)
)
player.maybe_shuffle()
if spotify_cache:
task = ("insert", ("spotify", database_entries))
@@ -718,9 +743,12 @@ class AudioAPIInterface:
except Exception as exc:
debug_exc_log(log, exc, f"Failed to fetch {track_info} from YouTube table")
if val is None:
youtube_url = await self.fetch_youtube_query(
ctx, track_info, current_cache_level=current_cache_level
)
try:
youtube_url = await self.fetch_youtube_query(
ctx, track_info, current_cache_level=current_cache_level
)
except YouTubeApiError as err:
youtube_url = None
else:
if cache_enabled:
task = ("update", ("youtube", {"track": track_info}))

View File

@@ -4,6 +4,7 @@ import datetime
import logging
import random
import time
from pathlib import Path
from types import SimpleNamespace
from typing import TYPE_CHECKING, Callable, List, MutableMapping, Optional, Tuple, Union
@@ -11,6 +12,7 @@ from typing import TYPE_CHECKING, Callable, List, MutableMapping, Optional, Tupl
from redbot.core import Config
from redbot.core.bot import Red
from redbot.core.commands import Cog
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core.utils.dbtools import APSWConnectionWrapper
@@ -59,7 +61,7 @@ if TYPE_CHECKING:
log = logging.getLogger("red.cogs.Audio.api.LocalDB")
_ = Translator("Audio", Path(__file__))
_SCHEMA_VERSION = 3

View File

@@ -2,6 +2,7 @@ import concurrent
import json
import logging
import time
from pathlib import Path
from types import SimpleNamespace
from typing import TYPE_CHECKING, List, Union
@@ -11,6 +12,7 @@ import lavalink
from redbot.core import Config
from redbot.core.bot import Red
from redbot.core.commands import Cog
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core.utils.dbtools import APSWConnectionWrapper
@@ -33,6 +35,7 @@ from ..sql_statements import (
from .api_utils import QueueFetchResult
log = logging.getLogger("red.cogs.Audio.api.PersistQueueWrapper")
_ = Translator("Audio", Path(__file__))
if TYPE_CHECKING:
from .. import Audio

View File

@@ -1,4 +1,5 @@
import logging
from pathlib import Path
from typing import List, MutableMapping, Optional, Union
@@ -7,6 +8,7 @@ import lavalink
from redbot.core import Config, commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from ..errors import NotAllowed
@@ -15,6 +17,7 @@ from .api_utils import PlaylistFetchResult, prepare_config_scope, standardize_sc
from .playlist_wrapper import PlaylistWrapper
log = logging.getLogger("red.cogs.Audio.api.PlaylistsInterface")
_ = Translator("Audio", Path(__file__))
class Playlist:

View File

@@ -1,12 +1,14 @@
import concurrent
import json
import logging
from pathlib import Path
from types import SimpleNamespace
from typing import List, MutableMapping, Optional
from redbot.core import Config
from redbot.core.bot import Red
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core.utils.dbtools import APSWConnectionWrapper
@@ -33,6 +35,7 @@ from ..utils import PlaylistScope
from .api_utils import PlaylistFetchResult
log = logging.getLogger("red.cogs.Audio.api.Playlists")
_ = Translator("Audio", Path(__file__))
class PlaylistWrapper:

View File

@@ -3,6 +3,7 @@ import contextlib
import json
import logging
import time
from pathlib import Path
from typing import TYPE_CHECKING, List, Mapping, MutableMapping, Optional, Tuple, Union
@@ -19,7 +20,7 @@ from ..errors import SpotifyFetchError
if TYPE_CHECKING:
from .. import Audio
_ = Translator("Audio", __file__)
_ = Translator("Audio", Path(__file__))
log = logging.getLogger("red.cogs.Audio.api.Spotify")
@@ -104,7 +105,7 @@ class SpotifyWrapper:
log.debug(f"Issue making GET request to {url}: [{r.status}] {data}")
return data
def update_token(self, new_token: Mapping[str, str]):
async def update_token(self, new_token: Mapping[str, str]):
self._token = new_token
async def get_token(self) -> None:

View File

@@ -1,5 +1,6 @@
import json
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Mapping, Optional, Union
@@ -8,6 +9,7 @@ import aiohttp
from redbot.core import Config
from redbot.core.bot import Red
from redbot.core.commands import Cog
from redbot.core.i18n import Translator
from ..errors import YouTubeApiError
@@ -15,7 +17,7 @@ if TYPE_CHECKING:
from .. import Audio
log = logging.getLogger("red.cogs.Audio.api.YouTube")
_ = Translator("Audio", Path(__file__))
SEARCH_ENDPOINT = "https://www.googleapis.com/youtube/v3/search"
@@ -32,7 +34,7 @@ class YouTubeWrapper:
self._token: Mapping[str, str] = {}
self.cog = cog
def update_token(self, new_token: Mapping[str, str]):
async def update_token(self, new_token: Mapping[str, str]):
self._token = new_token
async def _get_api_key(
@@ -54,11 +56,28 @@ class YouTubeWrapper:
"type": "video",
}
async with self.session.request("GET", SEARCH_ENDPOINT, params=params) as r:
if r.status in [400, 404]:
if r.status == 400:
if r.reason == "Bad Request":
raise YouTubeApiError(
_(
"Your YouTube Data API token is invalid.\n"
"Check the YouTube API key again and follow the instructions "
"at `{prefix}audioset youtubeapi`."
)
)
return None
elif r.status in [403, 429]:
if r.reason == "quotaExceeded":
raise YouTubeApiError("Your YouTube Data API quota has been reached.")
elif r.status == 404:
return None
elif r.status == 403:
if r.reason in ["Forbidden", "quotaExceeded"]:
raise YouTubeApiError(
_(
"YouTube API error code: 403\nYour YouTube API key may have "
"reached the account's query limit for today. Please check "
"<https://developers.google.com/youtube/v3/getting-started#quota> "
"for more information."
)
)
return None
else:
search_response = await r.json(loads=json.loads)