mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-20 18:06:08 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user