diff --git a/redbot/cogs/audio/apis/api_utils.py b/redbot/cogs/audio/apis/api_utils.py index 40d550c08..db0e2a02f 100644 --- a/redbot/cogs/audio/apis/api_utils.py +++ b/redbot/cogs/audio/apis/api_utils.py @@ -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 diff --git a/redbot/cogs/audio/apis/global_db.py b/redbot/cogs/audio/apis/global_db.py index fe6f1f5d6..dc5e9e41c 100644 --- a/redbot/cogs/audio/apis/global_db.py +++ b/redbot/cogs/audio/apis/global_db.py @@ -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): diff --git a/redbot/cogs/audio/apis/interface.py b/redbot/cogs/audio/apis/interface.py index 45d07d456..7798b4bac 100644 --- a/redbot/cogs/audio/apis/interface.py +++ b/redbot/cogs/audio/apis/interface.py @@ -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})) diff --git a/redbot/cogs/audio/apis/local_db.py b/redbot/cogs/audio/apis/local_db.py index 39c1d6a98..d8dcf9dbd 100644 --- a/redbot/cogs/audio/apis/local_db.py +++ b/redbot/cogs/audio/apis/local_db.py @@ -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 diff --git a/redbot/cogs/audio/apis/persist_queue_wrapper.py b/redbot/cogs/audio/apis/persist_queue_wrapper.py index 0d9e99109..7fe802803 100644 --- a/redbot/cogs/audio/apis/persist_queue_wrapper.py +++ b/redbot/cogs/audio/apis/persist_queue_wrapper.py @@ -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 diff --git a/redbot/cogs/audio/apis/playlist_interface.py b/redbot/cogs/audio/apis/playlist_interface.py index bfc7e3ccf..a21d1517b 100644 --- a/redbot/cogs/audio/apis/playlist_interface.py +++ b/redbot/cogs/audio/apis/playlist_interface.py @@ -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: diff --git a/redbot/cogs/audio/apis/playlist_wrapper.py b/redbot/cogs/audio/apis/playlist_wrapper.py index a12694643..f38b32cc1 100644 --- a/redbot/cogs/audio/apis/playlist_wrapper.py +++ b/redbot/cogs/audio/apis/playlist_wrapper.py @@ -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: diff --git a/redbot/cogs/audio/apis/spotify.py b/redbot/cogs/audio/apis/spotify.py index edce7899b..faee09139 100644 --- a/redbot/cogs/audio/apis/spotify.py +++ b/redbot/cogs/audio/apis/spotify.py @@ -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: diff --git a/redbot/cogs/audio/apis/youtube.py b/redbot/cogs/audio/apis/youtube.py index aadeb70ea..3bed6d37f 100644 --- a/redbot/cogs/audio/apis/youtube.py +++ b/redbot/cogs/audio/apis/youtube.py @@ -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 " + " " + "for more information." + ) + ) return None else: search_response = await r.json(loads=json.loads) diff --git a/redbot/cogs/audio/audio_dataclasses.py b/redbot/cogs/audio/audio_dataclasses.py index 7dc2a863d..afb7cb6e3 100644 --- a/redbot/cogs/audio/audio_dataclasses.py +++ b/redbot/cogs/audio/audio_dataclasses.py @@ -22,8 +22,11 @@ from urllib.parse import urlparse import lavalink +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter +_ = Translator("Audio", Path(__file__)) + _RE_REMOVE_START: Final[Pattern] = re.compile(r"^(sc|list) ") _RE_YOUTUBE_TIMESTAMP: Final[Pattern] = re.compile(r"[&|?]t=(\d+)s?") _RE_YOUTUBE_INDEX: Final[Pattern] = re.compile(r"&index=(\d+)") diff --git a/redbot/cogs/audio/converters.py b/redbot/cogs/audio/converters.py index 320732952..9558e3dc1 100644 --- a/redbot/cogs/audio/converters.py +++ b/redbot/cogs/audio/converters.py @@ -1,6 +1,7 @@ import argparse import functools import re +from pathlib import Path from typing import Final, MutableMapping, Optional, Pattern, Tuple, Union @@ -16,7 +17,7 @@ from .apis.playlist_interface import get_all_playlist_converter from .errors import NoMatchesFound, TooManyMatches from .utils import PlaylistScope -_ = Translator("Audio", __file__) +_ = Translator("Audio", Path(__file__)) __all__ = [ "ComplexScopeParser", diff --git a/redbot/cogs/audio/core/__init__.py b/redbot/cogs/audio/core/__init__.py index fbc215e50..eed486c69 100644 --- a/redbot/cogs/audio/core/__init__.py +++ b/redbot/cogs/audio/core/__init__.py @@ -2,6 +2,7 @@ import asyncio import json from collections import Counter +from pathlib import Path from typing import Mapping import aiohttp @@ -11,11 +12,13 @@ from redbot.core import Config from redbot.core.bot import Red from redbot.core.commands import Cog from redbot.core.data_manager import cog_data_path -from redbot.core.i18n import cog_i18n +from redbot.core.i18n import Translator, cog_i18n from ..utils import PlaylistScope from . import abc, cog_utils, commands, events, tasks, utilities -from .cog_utils import CompositeMetaClass, _ +from .cog_utils import CompositeMetaClass + +_ = Translator("Audio", Path(__file__)) @cog_i18n(_) diff --git a/redbot/cogs/audio/core/cog_utils.py b/redbot/cogs/audio/core/cog_utils.py index 845e05290..d40672591 100644 --- a/redbot/cogs/audio/core/cog_utils.py +++ b/redbot/cogs/audio/core/cog_utils.py @@ -1,10 +1,8 @@ from abc import ABC -from pathlib import Path from typing import Final from redbot import VersionInfo from redbot.core import commands -from redbot.core.i18n import Translator from ..converters import get_lazy_converter, get_playlist_converter @@ -12,7 +10,6 @@ __version__ = VersionInfo.from_json({"major": 2, "minor": 3, "micro": 0, "releas __author__ = ["aikaterna", "Draper"] -_ = Translator("Audio", Path(__file__).parent) _SCHEMA_VERSION: Final[int] = 3 _OWNER_NOTIFICATION: Final[int] = 1 diff --git a/redbot/cogs/audio/core/commands/audioset.py b/redbot/cogs/audio/core/commands/audioset.py index cf148f0ad..a9bc4a49a 100644 --- a/redbot/cogs/audio/core/commands/audioset.py +++ b/redbot/cogs/audio/core/commands/audioset.py @@ -1,6 +1,7 @@ import asyncio import contextlib import logging +from pathlib import Path from typing import Union @@ -9,6 +10,7 @@ import lavalink from redbot.core import bank, commands from redbot.core.data_manager import cog_data_path +from redbot.core.i18n import Translator from redbot.core.utils.chat_formatting import box, humanize_number from redbot.core.utils.menus import DEFAULT_CONTROLS, menu, start_adding_reactions from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate @@ -18,10 +20,12 @@ from ...converters import ScopeParser from ...errors import MissingGuild, TooManyMatches from ...utils import CacheLevel, PlaylistScope from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, PlaylistConverter, _, __version__ +from ..cog_utils import CompositeMetaClass, PlaylistConverter, __version__ log = logging.getLogger("red.cogs.Audio.cog.Commands.audioset") +_ = Translator("Audio", Path(__file__)) + class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass): @commands.group(name="audioset") @@ -1378,7 +1382,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass): async def command_audioset_audiodb_toggle(self, ctx: commands.Context): """Toggle the server settings. - Default is ON + Default is OFF """ state = await self.config.global_db_enabled() await self.config.global_db_enabled.set(not state) diff --git a/redbot/cogs/audio/core/commands/controller.py b/redbot/cogs/audio/core/commands/controller.py index 5d1cea9a3..03ad1f68b 100644 --- a/redbot/cogs/audio/core/commands/controller.py +++ b/redbot/cogs/audio/core/commands/controller.py @@ -3,6 +3,7 @@ import contextlib import datetime import logging import time +from pathlib import Path from typing import Optional, Union @@ -10,15 +11,17 @@ import discord import lavalink from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import humanize_number from redbot.core.utils.menus import start_adding_reactions from redbot.core.utils.predicates import ReactionPredicate from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Commands.player_controller") +_ = Translator("Audio", Path(__file__)) class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass): @@ -98,9 +101,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass): await self.get_track_description(player.current, self.local_folder_current_path) or "" ) - song += _("\n Requested by: **{track.requester}**") - song += "\n\n{arrow}`{pos}`/`{dur}`" - song = song.format(track=player.current, arrow=arrow, pos=pos, dur=dur) + song += _("\n Requested by: **{track.requester}**").format(track=player.current) + song += "\n\n{arrow}`{pos}`/`{dur}`".format(arrow=arrow, pos=pos, dur=dur) else: song = _("Nothing.") diff --git a/redbot/cogs/audio/core/commands/equalizer.py b/redbot/cogs/audio/core/commands/equalizer.py index 2269e291b..76372cc42 100644 --- a/redbot/cogs/audio/core/commands/equalizer.py +++ b/redbot/cogs/audio/core/commands/equalizer.py @@ -2,20 +2,23 @@ import asyncio import contextlib import logging import re +from pathlib import Path import discord import lavalink from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils.chat_formatting import box, humanize_number, pagify from redbot.core.utils.menus import DEFAULT_CONTROLS, menu, start_adding_reactions from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate from ...equalizer import Equalizer from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Commands.equalizer") +_ = Translator("Audio", Path(__file__)) class EqualizerCommands(MixinMeta, metaclass=CompositeMetaClass): diff --git a/redbot/cogs/audio/core/commands/llset.py b/redbot/cogs/audio/core/commands/llset.py index 16f579a18..2c7ba484d 100644 --- a/redbot/cogs/audio/core/commands/llset.py +++ b/redbot/cogs/audio/core/commands/llset.py @@ -4,11 +4,13 @@ from pathlib import Path import discord from redbot.core import commands +from redbot.core.i18n import Translator from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Commands.lavalink_setup") +_ = Translator("Audio", Path(__file__)) class LavalinkSetupCommands(MixinMeta, metaclass=CompositeMetaClass): diff --git a/redbot/cogs/audio/core/commands/localtracks.py b/redbot/cogs/audio/core/commands/localtracks.py index f0d96919c..18b0cc487 100644 --- a/redbot/cogs/audio/core/commands/localtracks.py +++ b/redbot/cogs/audio/core/commands/localtracks.py @@ -7,13 +7,15 @@ from typing import MutableMapping import discord from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils.menus import DEFAULT_CONTROLS, close_menu, menu, next_page, prev_page from ...audio_dataclasses import LocalPath, Query from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Commands.local_track") +_ = Translator("Audio", Path(__file__)) class LocalTrackCommands(MixinMeta, metaclass=CompositeMetaClass): diff --git a/redbot/cogs/audio/core/commands/miscellaneous.py b/redbot/cogs/audio/core/commands/miscellaneous.py index 604b14d91..fabf62d6a 100644 --- a/redbot/cogs/audio/core/commands/miscellaneous.py +++ b/redbot/cogs/audio/core/commands/miscellaneous.py @@ -3,19 +3,22 @@ import heapq import logging import math import random +from pathlib import Path import discord import lavalink from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import humanize_number, pagify from redbot.core.utils.menus import DEFAULT_CONTROLS, menu from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Commands.miscellaneous") +_ = Translator("Audio", Path(__file__)) class MiscellaneousCommands(MixinMeta, metaclass=CompositeMetaClass): diff --git a/redbot/cogs/audio/core/commands/player.py b/redbot/cogs/audio/core/commands/player.py index 7ee97eda0..eb69d6626 100644 --- a/redbot/cogs/audio/core/commands/player.py +++ b/redbot/cogs/audio/core/commands/player.py @@ -3,6 +3,7 @@ import datetime import logging import math import time +from pathlib import Path from typing import MutableMapping @@ -12,6 +13,7 @@ import lavalink from discord.embeds import EmptyEmbed from redbot.core import commands from redbot.core.commands import UserInputOptional +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.menus import DEFAULT_CONTROLS, close_menu, menu, next_page, prev_page @@ -24,9 +26,10 @@ from ...errors import ( TrackEnqueueError, ) from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Commands.player") +_ = Translator("Audio", Path(__file__)) class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass): @@ -125,6 +128,9 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass): return await self.send_embed_msg( ctx, title=_("Unable To Play Tracks"), description=err.message ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e @commands.command(name="bumpplay") @commands.guild_only() @@ -230,6 +236,9 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass): return await self.send_embed_msg( ctx, title=_("Unable To Play Tracks"), description=err.message ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e if isinstance(tracks, discord.Message): return elif not tracks: @@ -248,7 +257,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass): title = _("Track is not playable.") embed = discord.Embed(title=title) embed.description = _( - "**{suffix}** is not a fully supported format and some " "tracks may not play." + "**{suffix}** is not a fully supported format and some tracks may not play." ).format(suffix=query.suffix) return await self.send_embed_msg(ctx, embed=embed) queue_dur = await self.track_remaining_duration(ctx) @@ -611,6 +620,9 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass): "minutes." ), ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e if not guild_data["auto_play"]: await ctx.invoke(self.command_audioset_autoplay_toggle) @@ -745,6 +757,9 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass): "try again in a few minutes." ), ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e tracks = result.tracks else: @@ -761,6 +776,9 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass): "try again in a few minutes." ), ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e if not tracks: embed = discord.Embed(title=_("Nothing found.")) if await self.config.use_external_lavalink() and query.is_local: @@ -877,6 +895,9 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass): "try again in a few minutes." ), ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e tracks = result.tracks if not tracks: embed = discord.Embed(title=_("Nothing found.")) diff --git a/redbot/cogs/audio/core/commands/playlists.py b/redbot/cogs/audio/core/commands/playlists.py index e4974f053..758fb9e5b 100644 --- a/redbot/cogs/audio/core/commands/playlists.py +++ b/redbot/cogs/audio/core/commands/playlists.py @@ -7,6 +7,7 @@ import tarfile import time from io import BytesIO +from pathlib import Path from typing import cast import discord @@ -15,6 +16,7 @@ import lavalink from redbot.core import commands from redbot.core.commands import UserInputOptional from redbot.core.data_manager import cog_data_path +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import bold, pagify from redbot.core.utils.menus import DEFAULT_CONTROLS, menu @@ -28,9 +30,10 @@ from ...converters import ComplexScopeParser, ScopeParser from ...errors import MissingGuild, TooManyMatches, TrackEnqueueError from ...utils import PlaylistScope from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, LazyGreedyConverter, PlaylistConverter, _ +from ..cog_utils import CompositeMetaClass, LazyGreedyConverter, PlaylistConverter log = logging.getLogger("red.cogs.Audio.cog.Commands.playlist") +_ = Translator("Audio", Path(__file__)) class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): @@ -107,104 +110,110 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): (scope, author, guild, specified_user) = scope_data if not await self._playlist_check(ctx): return - try: - (playlist, playlist_arg, scope) = await self.get_playlist_match( - ctx, playlist_matches, scope, author, guild, specified_user - ) - except TooManyMatches as e: - return await self.send_embed_msg(ctx, title=str(e)) - if playlist is None: - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("Could not match '{arg}' to a playlist").format(arg=playlist_arg), - ) - if not await self.can_manage_playlist(scope, playlist, ctx, author, guild): - return - player = lavalink.get_player(ctx.guild.id) - to_append = await self.fetch_playlist_tracks( - ctx, player, Query.process_input(query, self.local_folder_current_path) - ) - - if isinstance(to_append, discord.Message): - return None - - if not to_append: - return await self.send_embed_msg( - ctx, title=_("Could not find a track matching your query.") - ) - track_list = playlist.tracks - current_count = len(track_list) - to_append_count = len(to_append) - tracks_obj_list = playlist.tracks_obj - not_added = 0 - if current_count + to_append_count > 10000: - to_append = to_append[: 10000 - current_count] - not_added = to_append_count - len(to_append) - to_append_count = len(to_append) - scope_name = self.humanize_scope( - scope, ctx=guild if scope == PlaylistScope.GUILD.value else author - ) - appended = 0 - - if to_append and to_append_count == 1: - to = lavalink.Track(to_append[0]) - if to in tracks_obj_list: + async with ctx.typing(): + try: + (playlist, playlist_arg, scope) = await self.get_playlist_match( + ctx, playlist_matches, scope, author, guild, specified_user + ) + except TooManyMatches as e: + return await self.send_embed_msg(ctx, title=str(e)) + if playlist is None: return await self.send_embed_msg( ctx, - title=_("Skipping track"), - description=_( - "{track} is already in {playlist} (`{id}`) [**{scope}**]." - ).format( - track=to.title, playlist=playlist.name, id=playlist.id, scope=scope_name + title=_("Playlist Not Found"), + description=_("Could not match '{arg}' to a playlist").format( + arg=playlist_arg ), - footer=_("Playlist limit reached: Could not add track.").format(not_added) - if not_added > 0 - else None, ) - else: - appended += 1 - if to_append and to_append_count > 1: - to_append_temp = [] - async for t in AsyncIter(to_append): - to = lavalink.Track(t) - if to not in tracks_obj_list: + if not await self.can_manage_playlist(scope, playlist, ctx, author, guild): + return + player = lavalink.get_player(ctx.guild.id) + to_append = await self.fetch_playlist_tracks( + ctx, player, Query.process_input(query, self.local_folder_current_path) + ) + + if isinstance(to_append, discord.Message): + return None + + if not to_append: + return await self.send_embed_msg( + ctx, title=_("Could not find a track matching your query.") + ) + track_list = playlist.tracks + current_count = len(track_list) + to_append_count = len(to_append) + tracks_obj_list = playlist.tracks_obj + not_added = 0 + if current_count + to_append_count > 10000: + to_append = to_append[: 10000 - current_count] + not_added = to_append_count - len(to_append) + to_append_count = len(to_append) + scope_name = self.humanize_scope( + scope, ctx=guild if scope == PlaylistScope.GUILD.value else author + ) + appended = 0 + + if to_append and to_append_count == 1: + to = lavalink.Track(to_append[0]) + if to in tracks_obj_list: + return await self.send_embed_msg( + ctx, + title=_("Skipping track"), + description=_( + "{track} is already in {playlist} (`{id}`) [**{scope}**]." + ).format( + track=to.title, + playlist=playlist.name, + id=playlist.id, + scope=scope_name, + ), + footer=_("Playlist limit reached: Could not add track.").format(not_added) + if not_added > 0 + else None, + ) + else: appended += 1 - to_append_temp.append(t) - to_append = to_append_temp - if appended > 0: - track_list.extend(to_append) - update = {"tracks": track_list, "url": None} - await playlist.edit(update) + if to_append and to_append_count > 1: + to_append_temp = [] + async for t in AsyncIter(to_append): + to = lavalink.Track(t) + if to not in tracks_obj_list: + appended += 1 + to_append_temp.append(t) + to_append = to_append_temp + if appended > 0: + track_list.extend(to_append) + update = {"tracks": track_list, "url": None} + await playlist.edit(update) - if to_append_count == 1 and appended == 1: - track_title = to_append[0]["info"]["title"] - return await self.send_embed_msg( + if to_append_count == 1 and appended == 1: + track_title = to_append[0]["info"]["title"] + return await self.send_embed_msg( + ctx, + title=_("Track added"), + description=_("{track} appended to {playlist} (`{id}`) [**{scope}**].").format( + track=track_title, playlist=playlist.name, id=playlist.id, scope=scope_name + ), + ) + + desc = _("{num} tracks appended to {playlist} (`{id}`) [**{scope}**].").format( + num=appended, playlist=playlist.name, id=playlist.id, scope=scope_name + ) + if to_append_count > appended: + diff = to_append_count - appended + desc += _( + "\n{existing} {plural} already in the playlist and were skipped." + ).format(existing=diff, plural=_("tracks are") if diff != 1 else _("track is")) + + embed = discord.Embed(title=_("Playlist Modified"), description=desc) + await self.send_embed_msg( ctx, - title=_("Track added"), - description=_("{track} appended to {playlist} (`{id}`) [**{scope}**].").format( - track=track_title, playlist=playlist.name, id=playlist.id, scope=scope_name - ), + embed=embed, + footer=_("Playlist limit reached: Could not add track.").format(not_added) + if not_added > 0 + else None, ) - desc = _("{num} tracks appended to {playlist} (`{id}`) [**{scope}**].").format( - num=appended, playlist=playlist.name, id=playlist.id, scope=scope_name - ) - if to_append_count > appended: - diff = to_append_count - appended - desc += _("\n{existing} {plural} already in the playlist and were skipped.").format( - existing=diff, plural=_("tracks are") if diff != 1 else _("track is") - ) - - embed = discord.Embed(title=_("Playlist Modified"), description=desc) - await self.send_embed_msg( - ctx, - embed=embed, - footer=_("Playlist limit reached: Could not add track.").format(not_added) - if not_added > 0 - else None, - ) - @commands.cooldown(1, 150, commands.BucketType.member) @command_playlist.command( name="copy", usage=" [args]", cooldown_after_parsing=True @@ -282,65 +291,70 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): specified_to_user, ) = scope_data to_scope = to_scope or PlaylistScope.GUILD.value - try: - from_playlist, playlist_arg, from_scope = await self.get_playlist_match( - ctx, playlist_matches, from_scope, from_author, from_guild, specified_from_user - ) - except TooManyMatches as e: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg(ctx, title=str(e)) + async with ctx.typing(): + try: + from_playlist, playlist_arg, from_scope = await self.get_playlist_match( + ctx, playlist_matches, from_scope, from_author, from_guild, specified_from_user + ) + except TooManyMatches as e: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg(ctx, title=str(e)) + + if from_playlist is None: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_("Could not match '{arg}' to a playlist.").format( + arg=playlist_arg + ), + ) + + temp_playlist = cast(Playlist, FakePlaylist(to_author.id, to_scope)) + if not await self.can_manage_playlist( + to_scope, temp_playlist, ctx, to_author, to_guild + ): + ctx.command.reset_cooldown(ctx) + return + + to_playlist = await create_playlist( + ctx, + self.playlist_api, + to_scope, + from_playlist.name, + from_playlist.url, + from_playlist.tracks, + to_author, + to_guild, + ) + if to_scope == PlaylistScope.GLOBAL.value: + to_scope_name = _("the Global") + elif to_scope == PlaylistScope.USER.value: + to_scope_name = to_author + else: + to_scope_name = to_guild + + if from_scope == PlaylistScope.GLOBAL.value: + from_scope_name = _("the Global") + elif from_scope == PlaylistScope.USER.value: + from_scope_name = from_author + else: + from_scope_name = from_guild - if from_playlist is None: - ctx.command.reset_cooldown(ctx) return await self.send_embed_msg( ctx, - title=_("Playlist Not Found"), - description=_("Could not match '{arg}' to a playlist.").format(arg=playlist_arg), + title=_("Playlist Copied"), + description=_( + "Playlist {name} (`{from_id}`) copied from {from_scope} to {to_scope} (`{to_id}`)." + ).format( + name=from_playlist.name, + from_id=from_playlist.id, + from_scope=self.humanize_scope(from_scope, ctx=from_scope_name), + to_scope=self.humanize_scope(to_scope, ctx=to_scope_name), + to_id=to_playlist.id, + ), ) - temp_playlist = cast(Playlist, FakePlaylist(to_author.id, to_scope)) - if not await self.can_manage_playlist(to_scope, temp_playlist, ctx, to_author, to_guild): - ctx.command.reset_cooldown(ctx) - return - - to_playlist = await create_playlist( - ctx, - self.playlist_api, - to_scope, - from_playlist.name, - from_playlist.url, - from_playlist.tracks, - to_author, - to_guild, - ) - if to_scope == PlaylistScope.GLOBAL.value: - to_scope_name = _("the Global") - elif to_scope == PlaylistScope.USER.value: - to_scope_name = to_author - else: - to_scope_name = to_guild - - if from_scope == PlaylistScope.GLOBAL.value: - from_scope_name = _("the Global") - elif from_scope == PlaylistScope.USER.value: - from_scope_name = from_author - else: - from_scope_name = from_guild - - return await self.send_embed_msg( - ctx, - title=_("Playlist Copied"), - description=_( - "Playlist {name} (`{from_id}`) copied from {from_scope} to {to_scope} (`{to_id}`)." - ).format( - name=from_playlist.name, - from_id=from_playlist.id, - from_scope=self.humanize_scope(from_scope, ctx=from_scope_name), - to_scope=self.humanize_scope(to_scope, ctx=to_scope_name), - to_id=to_playlist.id, - ), - ) - @command_playlist.command(name="create", usage=" [args]") async def command_playlist_create( self, ctx: commands.Context, playlist_name: str, *, scope_data: ScopeParser = None @@ -392,28 +406,29 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): scope_name = self.humanize_scope( scope, ctx=guild if scope == PlaylistScope.GUILD.value else author ) - if not await self.can_manage_playlist(scope, temp_playlist, ctx, author, guild): - return - playlist_name = playlist_name.split(" ")[0].strip('"')[:32] - if playlist_name.isnumeric(): + async with ctx.typing(): + if not await self.can_manage_playlist(scope, temp_playlist, ctx, author, guild): + return + playlist_name = playlist_name.split(" ")[0].strip('"')[:32] + if playlist_name.isnumeric(): + return await self.send_embed_msg( + ctx, + title=_("Invalid Playlist Name"), + description=_( + "Playlist names must be a single word (up to 32 " + "characters) and not numbers only." + ), + ) + playlist = await create_playlist( + ctx, self.playlist_api, scope, playlist_name, None, None, author, guild + ) return await self.send_embed_msg( ctx, - title=_("Invalid Playlist Name"), - description=_( - "Playlist names must be a single word (up to 32 " - "characters) and not numbers only." + title=_("Playlist Created"), + description=_("Empty playlist {name} (`{id}`) [**{scope}**] created.").format( + name=playlist.name, id=playlist.id, scope=scope_name ), ) - playlist = await create_playlist( - ctx, self.playlist_api, scope, playlist_name, None, None, author, guild - ) - return await self.send_embed_msg( - ctx, - title=_("Playlist Created"), - description=_("Empty playlist {name} (`{id}`) [**{scope}**] created.").format( - name=playlist.name, id=playlist.id, scope=scope_name - ), - ) @command_playlist.command(name="delete", aliases=["del"], usage=" [args]") async def command_playlist_delete( @@ -465,40 +480,42 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): if scope_data is None: scope_data = [None, ctx.author, ctx.guild, False] scope, author, guild, specified_user = scope_data - - try: - playlist, playlist_arg, scope = await self.get_playlist_match( - ctx, playlist_matches, scope, author, guild, specified_user + async with ctx.typing(): + try: + playlist, playlist_arg, scope = await self.get_playlist_match( + ctx, playlist_matches, scope, author, guild, specified_user + ) + except TooManyMatches as e: + return await self.send_embed_msg(ctx, title=str(e)) + if playlist is None: + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_("Could not match '{arg}' to a playlist.").format( + arg=playlist_arg + ), + ) + if not await self.can_manage_playlist(scope, playlist, ctx, author, guild): + return + scope_name = self.humanize_scope( + scope, ctx=guild if scope == PlaylistScope.GUILD.value else author ) - except TooManyMatches as e: - return await self.send_embed_msg(ctx, title=str(e)) - if playlist is None: - return await self.send_embed_msg( + await delete_playlist( + self.bot, + self.playlist_api, + scope, + playlist.id, + guild or ctx.guild, + author or ctx.author, + ) + + await self.send_embed_msg( ctx, - title=_("Playlist Not Found"), - description=_("Could not match '{arg}' to a playlist.").format(arg=playlist_arg), + title=_("Playlist Deleted"), + description=_("{name} (`{id}`) [**{scope}**] playlist deleted.").format( + name=playlist.name, id=playlist.id, scope=scope_name + ), ) - if not await self.can_manage_playlist(scope, playlist, ctx, author, guild): - return - scope_name = self.humanize_scope( - scope, ctx=guild if scope == PlaylistScope.GUILD.value else author - ) - await delete_playlist( - self.bot, - self.playlist_api, - scope, - playlist.id, - guild or ctx.guild, - author or ctx.author, - ) - - await self.send_embed_msg( - ctx, - title=_("Playlist Deleted"), - description=_("{name} (`{id}`) [**{scope}**] playlist deleted.").format( - name=playlist.name, id=playlist.id, scope=scope_name - ), - ) @commands.cooldown(1, 30, commands.BucketType.member) @command_playlist.command( @@ -688,81 +705,85 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): if scope_data is None: scope_data = [None, ctx.author, ctx.guild, False] scope, author, guild, specified_user = scope_data - - try: - playlist, playlist_arg, scope = await self.get_playlist_match( - ctx, playlist_matches, scope, author, guild, specified_user - ) - except TooManyMatches as e: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg(ctx, title=str(e)) - if playlist is None: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("Could not match '{arg}' to a playlist.").format(arg=playlist_arg), - ) - - schema = 2 - version = "v3" if v2 is False else "v2" - - if not playlist.tracks: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg(ctx, title=_("That playlist has no tracks.")) - if version == "v2": - v2_valid_urls = ["https://www.youtube.com/watch?v=", "https://soundcloud.com/"] - song_list = [] - async for track in AsyncIter(playlist.tracks): - if track["info"]["uri"].startswith(tuple(v2_valid_urls)): - song_list.append(track["info"]["uri"]) - playlist_data = { - "author": playlist.author, - "link": playlist.url, - "playlist": song_list, - "name": playlist.name, - } - file_name = playlist.name - else: - # TODO: Keep new playlists backwards compatible, Remove me in a few releases - playlist_data = playlist.to_json() - playlist_songs_backwards_compatible = [ - track["info"]["uri"] for track in playlist.tracks - ] - playlist_data["playlist"] = playlist_songs_backwards_compatible - playlist_data["link"] = playlist.url - file_name = playlist.id - playlist_data.update({"schema": schema, "version": version}) - playlist_data = json.dumps(playlist_data).encode("utf-8") - to_write = BytesIO() - to_write.write(playlist_data) - to_write.seek(0) - if to_write.getbuffer().nbytes > ctx.guild.filesize_limit - 10000: - datapath = cog_data_path(raw_name="Audio") - temp_file = datapath / f"{file_name}.txt" - temp_tar = datapath / f"{file_name}.tar.gz" - with temp_file.open("wb") as playlist_file: - playlist_file.write(to_write.read()) - - with tarfile.open(str(temp_tar), "w:gz") as tar: - tar.add( - str(temp_file), arcname=str(temp_file.relative_to(datapath)), recursive=False - ) + async with ctx.typing(): try: - if os.path.getsize(str(temp_tar)) > ctx.guild.filesize_limit - 10000: - await ctx.send(_("This playlist is too large to be send in this server.")) - else: - await ctx.send( - content=_("Playlist is too large, here is the compressed version."), - file=discord.File(str(temp_tar)), + playlist, playlist_arg, scope = await self.get_playlist_match( + ctx, playlist_matches, scope, author, guild, specified_user + ) + except TooManyMatches as e: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg(ctx, title=str(e)) + if playlist is None: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_("Could not match '{arg}' to a playlist.").format( + arg=playlist_arg + ), + ) + + schema = 2 + version = "v3" if v2 is False else "v2" + + if not playlist.tracks: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg(ctx, title=_("That playlist has no tracks.")) + if version == "v2": + v2_valid_urls = ["https://www.youtube.com/watch?v=", "https://soundcloud.com/"] + song_list = [] + async for track in AsyncIter(playlist.tracks): + if track["info"]["uri"].startswith(tuple(v2_valid_urls)): + song_list.append(track["info"]["uri"]) + playlist_data = { + "author": playlist.author, + "link": playlist.url, + "playlist": song_list, + "name": playlist.name, + } + file_name = playlist.name + else: + # TODO: Keep new playlists backwards compatible, Remove me in a few releases + playlist_data = playlist.to_json() + playlist_songs_backwards_compatible = [ + track["info"]["uri"] for track in playlist.tracks + ] + playlist_data["playlist"] = playlist_songs_backwards_compatible + playlist_data["link"] = playlist.url + file_name = playlist.id + playlist_data.update({"schema": schema, "version": version}) + playlist_data = json.dumps(playlist_data).encode("utf-8") + to_write = BytesIO() + to_write.write(playlist_data) + to_write.seek(0) + if to_write.getbuffer().nbytes > ctx.guild.filesize_limit - 10000: + datapath = cog_data_path(raw_name="Audio") + temp_file = datapath / f"{file_name}.txt" + temp_tar = datapath / f"{file_name}.tar.gz" + with temp_file.open("wb") as playlist_file: + playlist_file.write(to_write.read()) + + with tarfile.open(str(temp_tar), "w:gz") as tar: + tar.add( + str(temp_file), + arcname=str(temp_file.relative_to(datapath)), + recursive=False, ) - except Exception as exc: - debug_exc_log(log, exc, "Failed to send playlist to channel") - temp_file.unlink() - temp_tar.unlink() - else: - await ctx.send(file=discord.File(to_write, filename=f"{file_name}.txt")) - to_write.close() + try: + if os.path.getsize(str(temp_tar)) > ctx.guild.filesize_limit - 10000: + await ctx.send(_("This playlist is too large to be send in this server.")) + else: + await ctx.send( + content=_("Playlist is too large, here is the compressed version."), + file=discord.File(str(temp_tar)), + ) + except Exception as exc: + debug_exc_log(log, exc, "Failed to send playlist to channel") + temp_file.unlink() + temp_tar.unlink() + else: + await ctx.send(file=discord.File(to_write, filename=f"{file_name}.txt")) + to_write.close() @commands.cooldown(1, 10, commands.BucketType.member) @command_playlist.command( @@ -817,75 +838,80 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): if scope_data is None: scope_data = [None, ctx.author, ctx.guild, False] scope, author, guild, specified_user = scope_data - try: - playlist, playlist_arg, scope = await self.get_playlist_match( - ctx, playlist_matches, scope, author, guild, specified_user - ) - except TooManyMatches as e: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg(ctx, title=str(e)) - scope_name = self.humanize_scope( - scope, ctx=guild if scope == PlaylistScope.GUILD.value else author - ) - - if playlist is None: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("Could not match '{arg}' to a playlist.").format(arg=playlist_arg), - ) - track_len = len(playlist.tracks) - - msg = "​" - if track_len > 0: - spaces = "\N{EN SPACE}" * (len(str(len(playlist.tracks))) + 2) - async for track_idx, track in AsyncIter(playlist.tracks).enumerate(start=1): - query = Query.process_input(track["info"]["uri"], self.local_folder_current_path) - if query.is_local: - if track["info"]["title"] != "Unknown title": - msg += "`{}.` **{} - {}**\n{}{}\n".format( - track_idx, - track["info"]["author"], - track["info"]["title"], - spaces, - query.to_string_user(), - ) - else: - msg += "`{}.` {}\n".format(track_idx, query.to_string_user()) - else: - msg += "`{}.` **[{}]({})**\n".format( - track_idx, track["info"]["title"], track["info"]["uri"] - ) - - else: - msg = "No tracks." - - if not playlist.url: - embed_title = _("Playlist info for {playlist_name} (`{id}`) [**{scope}**]:\n").format( - playlist_name=playlist.name, id=playlist.id, scope=scope_name - ) - else: - embed_title = _( - "Playlist info for {playlist_name} (`{id}`) [**{scope}**]:\nURL: {url}" - ).format( - playlist_name=playlist.name, url=playlist.url, id=playlist.id, scope=scope_name - ) - - page_list = [] - pages = list(pagify(msg, delims=["\n"], page_length=2000)) - total_pages = len(pages) - async for numb, page in AsyncIter(pages).enumerate(start=1): - embed = discord.Embed( - colour=await ctx.embed_colour(), title=embed_title, description=page - ) - author_obj = self.bot.get_user(playlist.author) or playlist.author or _("Unknown") - embed.set_footer( - text=_("Page {page}/{pages} | Author: {author_name} | {num} track(s)").format( - author_name=author_obj, num=track_len, pages=total_pages, page=numb + async with ctx.typing(): + try: + playlist, playlist_arg, scope = await self.get_playlist_match( + ctx, playlist_matches, scope, author, guild, specified_user ) + except TooManyMatches as e: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg(ctx, title=str(e)) + scope_name = self.humanize_scope( + scope, ctx=guild if scope == PlaylistScope.GUILD.value else author ) - page_list.append(embed) + + if playlist is None: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_("Could not match '{arg}' to a playlist.").format( + arg=playlist_arg + ), + ) + track_len = len(playlist.tracks) + + msg = "​" + if track_len > 0: + spaces = "\N{EN SPACE}" * (len(str(len(playlist.tracks))) + 2) + async for track_idx, track in AsyncIter(playlist.tracks).enumerate(start=1): + query = Query.process_input( + track["info"]["uri"], self.local_folder_current_path + ) + if query.is_local: + if track["info"]["title"] != "Unknown title": + msg += "`{}.` **{} - {}**\n{}{}\n".format( + track_idx, + track["info"]["author"], + track["info"]["title"], + spaces, + query.to_string_user(), + ) + else: + msg += "`{}.` {}\n".format(track_idx, query.to_string_user()) + else: + msg += "`{}.` **[{}]({})**\n".format( + track_idx, track["info"]["title"], track["info"]["uri"] + ) + + else: + msg = "No tracks." + + if not playlist.url: + embed_title = _( + "Playlist info for {playlist_name} (`{id}`) [**{scope}**]:\n" + ).format(playlist_name=playlist.name, id=playlist.id, scope=scope_name) + else: + embed_title = _( + "Playlist info for {playlist_name} (`{id}`) [**{scope}**]:\nURL: {url}" + ).format( + playlist_name=playlist.name, url=playlist.url, id=playlist.id, scope=scope_name + ) + + page_list = [] + pages = list(pagify(msg, delims=["\n"], page_length=2000)) + total_pages = len(pages) + async for numb, page in AsyncIter(pages).enumerate(start=1): + embed = discord.Embed( + colour=await ctx.embed_colour(), title=embed_title, description=page + ) + author_obj = self.bot.get_user(playlist.author) or playlist.author or _("Unknown") + embed.set_footer( + text=_("Page {page}/{pages} | Author: {author_name} | {num} track(s)").format( + author_name=author_obj, num=track_len, pages=total_pages, page=numb + ) + ) + page_list.append(embed) await menu(ctx, page_list, DEFAULT_CONTROLS) @commands.cooldown(1, 15, commands.BucketType.guild) @@ -935,111 +961,113 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): if scope_data is None: scope_data = [None, ctx.author, ctx.guild, False] scope, author, guild, specified_user = scope_data + async with ctx.typing(): + if scope is None: - if scope is None: - - global_matches = await get_all_playlist( - scope=PlaylistScope.GLOBAL.value, - bot=self.bot, - guild=guild, - author=author, - specified_user=specified_user, - playlist_api=self.playlist_api, - ) - guild_matches = await get_all_playlist( - scope=PlaylistScope.GUILD.value, - bot=self.bot, - guild=guild, - author=author, - specified_user=specified_user, - playlist_api=self.playlist_api, - ) - user_matches = await get_all_playlist( - scope=PlaylistScope.USER.value, - bot=self.bot, - guild=guild, - author=author, - specified_user=specified_user, - playlist_api=self.playlist_api, - ) - playlists = [*global_matches, *guild_matches, *user_matches] - name = None - if not playlists: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("No saved playlists available in this server.").format( - scope=name - ), - ) - else: - try: - playlists = await get_all_playlist( - scope=scope, + global_matches = await get_all_playlist( + scope=PlaylistScope.GLOBAL.value, bot=self.bot, guild=guild, author=author, specified_user=specified_user, playlist_api=self.playlist_api, ) - except MissingGuild: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Missing Arguments"), - description=_("You need to specify the Guild ID for the guild to lookup."), + guild_matches = await get_all_playlist( + scope=PlaylistScope.GUILD.value, + bot=self.bot, + guild=guild, + author=author, + specified_user=specified_user, + playlist_api=self.playlist_api, ) - - if scope == PlaylistScope.GUILD.value: - name = f"{guild.name}" - elif scope == PlaylistScope.USER.value: - name = f"{author}" - else: - name = _("Global") - - if not playlists and specified_user: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("No saved playlists for {scope} created by {author}.").format( - scope=name, author=author - ), + user_matches = await get_all_playlist( + scope=PlaylistScope.USER.value, + bot=self.bot, + guild=guild, + author=author, + specified_user=specified_user, + playlist_api=self.playlist_api, ) - elif not playlists: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("No saved playlists for {scope}.").format(scope=name), - ) - - playlist_list = [] - space = "\N{EN SPACE}" - async for playlist in AsyncIter(playlists): - playlist_list.append( - ("\n" + space * 4).join( - ( - bold(playlist.name), - _("ID: {id}").format(id=playlist.id), - _("Tracks: {num}").format(num=len(playlist.tracks)), - _("Author: {name}").format( - name=self.bot.get_user(playlist.author) - or playlist.author - or _("Unknown") + playlists = [*global_matches, *guild_matches, *user_matches] + name = None + if not playlists: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_("No saved playlists available in this server.").format( + scope=name ), - _("Scope: {scope}\n").format(scope=self.humanize_scope(playlist.scope)), + ) + else: + try: + playlists = await get_all_playlist( + scope=scope, + bot=self.bot, + guild=guild, + author=author, + specified_user=specified_user, + playlist_api=self.playlist_api, + ) + except MissingGuild: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Missing Arguments"), + description=_("You need to specify the Guild ID for the guild to lookup."), + ) + + if scope == PlaylistScope.GUILD.value: + name = f"{guild.name}" + elif scope == PlaylistScope.USER.value: + name = f"{author}" + else: + name = _("Global") + + if not playlists and specified_user: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_( + "No saved playlists for {scope} created by {author}." + ).format(scope=name, author=author), + ) + elif not playlists: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_("No saved playlists for {scope}.").format(scope=name), + ) + + playlist_list = [] + space = "\N{EN SPACE}" + async for playlist in AsyncIter(playlists): + playlist_list.append( + ("\n" + space * 4).join( + ( + bold(playlist.name), + _("ID: {id}").format(id=playlist.id), + _("Tracks: {num}").format(num=len(playlist.tracks)), + _("Author: {name}").format( + name=self.bot.get_user(playlist.author) + or playlist.author + or _("Unknown") + ), + _("Scope: {scope}\n").format( + scope=self.humanize_scope(playlist.scope) + ), + ) ) ) - ) - abc_names = sorted(playlist_list, key=str.lower) - len_playlist_list_pages = math.ceil(len(abc_names) / 5) - playlist_embeds = [] + abc_names = sorted(playlist_list, key=str.lower) + len_playlist_list_pages = math.ceil(len(abc_names) / 5) + playlist_embeds = [] - async for page_num in AsyncIter(range(1, len_playlist_list_pages + 1)): - embed = await self._build_playlist_list_page(ctx, page_num, abc_names, name) - playlist_embeds.append(embed) + async for page_num in AsyncIter(range(1, len_playlist_list_pages + 1)): + embed = await self._build_playlist_list_page(ctx, page_num, abc_names, name) + playlist_embeds.append(embed) await menu(ctx, playlist_embeds, DEFAULT_CONTROLS) @command_playlist.command(name="queue", usage=" [args]", cooldown_after_parsing=True) @@ -1199,61 +1227,69 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): if scope_data is None: scope_data = [None, ctx.author, ctx.guild, False] scope, author, guild, specified_user = scope_data - try: - playlist, playlist_arg, scope = await self.get_playlist_match( - ctx, playlist_matches, scope, author, guild, specified_user + async with ctx.typing(): + try: + playlist, playlist_arg, scope = await self.get_playlist_match( + ctx, playlist_matches, scope, author, guild, specified_user + ) + except TooManyMatches as e: + return await self.send_embed_msg(ctx, title=str(e)) + scope_name = self.humanize_scope( + scope, ctx=guild if scope == PlaylistScope.GUILD.value else author ) - except TooManyMatches as e: - return await self.send_embed_msg(ctx, title=str(e)) - scope_name = self.humanize_scope( - scope, ctx=guild if scope == PlaylistScope.GUILD.value else author - ) - if playlist is None: - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("Could not match '{arg}' to a playlist.").format(arg=playlist_arg), - ) - if not await self.can_manage_playlist(scope, playlist, ctx, author, guild): - return + if playlist is None: + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_("Could not match '{arg}' to a playlist.").format( + arg=playlist_arg + ), + ) + if not await self.can_manage_playlist(scope, playlist, ctx, author, guild): + return - track_list = playlist.tracks - clean_list = [track for track in track_list if url != track["info"]["uri"]] - if len(track_list) == len(clean_list): - return await self.send_embed_msg(ctx, title=_("URL not in playlist.")) - del_count = len(track_list) - len(clean_list) - if not clean_list: - await delete_playlist( - playlist_api=self.playlist_api, - bot=self.bot, - scope=playlist.scope, - playlist_id=playlist.id, - guild=guild, - author=playlist.author, - ) - return await self.send_embed_msg(ctx, title=_("No tracks left, removing playlist.")) - update = {"tracks": clean_list, "url": None} - await playlist.edit(update) - if del_count > 1: - await self.send_embed_msg( - ctx, - title=_("Playlist Modified"), - description=_( - "{num} entries have been removed " - "from the playlist {playlist_name} (`{id}`) [**{scope}**]." - ).format( - num=del_count, playlist_name=playlist.name, id=playlist.id, scope=scope_name - ), - ) - else: - await self.send_embed_msg( - ctx, - title=_("Playlist Modified"), - description=_( - "The track has been removed from the playlist: " - "{playlist_name} (`{id}`) [**{scope}**]." - ).format(playlist_name=playlist.name, id=playlist.id, scope=scope_name), - ) + track_list = playlist.tracks + clean_list = [track for track in track_list if url != track["info"]["uri"]] + if len(track_list) == len(clean_list): + return await self.send_embed_msg(ctx, title=_("URL not in playlist.")) + del_count = len(track_list) - len(clean_list) + if not clean_list: + await delete_playlist( + playlist_api=self.playlist_api, + bot=self.bot, + scope=playlist.scope, + playlist_id=playlist.id, + guild=guild, + author=playlist.author, + ) + return await self.send_embed_msg( + ctx, title=_("No tracks left, removing playlist.") + ) + update = {"tracks": clean_list, "url": None} + await playlist.edit(update) + if del_count > 1: + await self.send_embed_msg( + ctx, + title=_("Playlist Modified"), + description=_( + "{num} entries have been removed " + "from the playlist {playlist_name} (`{id}`) [**{scope}**]." + ).format( + num=del_count, + playlist_name=playlist.name, + id=playlist.id, + scope=scope_name, + ), + ) + else: + await self.send_embed_msg( + ctx, + title=_("Playlist Modified"), + description=_( + "The track has been removed from the playlist: " + "{playlist_name} (`{id}`) [**{scope}**]." + ).format(playlist_name=playlist.name, id=playlist.id, scope=scope_name), + ) @command_playlist.command( name="save", usage=" [args]", cooldown_after_parsing=True @@ -1313,65 +1349,71 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): scope_name = self.humanize_scope( scope, ctx=guild if scope == PlaylistScope.GUILD.value else author ) - temp_playlist = cast(Playlist, FakePlaylist(author.id, scope)) - if not await self.can_manage_playlist(scope, temp_playlist, ctx, author, guild): - return ctx.command.reset_cooldown(ctx) - playlist_name = playlist_name.split(" ")[0].strip('"')[:32] - if playlist_name.isnumeric(): - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Invalid Playlist Name"), - description=_( - "Playlist names must be a single word (up to 32 " - "characters) and not numbers only." - ), - ) - if not await self._playlist_check(ctx): - ctx.command.reset_cooldown(ctx) - return - player = lavalink.get_player(ctx.guild.id) - tracklist = await self.fetch_playlist_tracks( - ctx, player, Query.process_input(playlist_url, self.local_folder_current_path) - ) - if isinstance(tracklist, discord.Message): - return None - if tracklist is not None: - playlist_length = len(tracklist) - not_added = 0 - if playlist_length > 10000: - tracklist = tracklist[:10000] - not_added = playlist_length - 10000 - - playlist = await create_playlist( - ctx, - self.playlist_api, - scope, - playlist_name, - playlist_url, - tracklist, - author, - guild, - ) - if playlist is not None: + async with ctx.typing(): + temp_playlist = cast(Playlist, FakePlaylist(author.id, scope)) + if not await self.can_manage_playlist(scope, temp_playlist, ctx, author, guild): + return ctx.command.reset_cooldown(ctx) + playlist_name = playlist_name.split(" ")[0].strip('"')[:32] + if playlist_name.isnumeric(): + ctx.command.reset_cooldown(ctx) return await self.send_embed_msg( ctx, - title=_("Playlist Created"), + title=_("Invalid Playlist Name"), description=_( - "Playlist {name} (`{id}`) [**{scope}**] saved: {num} tracks added." - ).format( - name=playlist.name, num=len(tracklist), id=playlist.id, scope=scope_name + "Playlist names must be a single word (up to 32 " + "characters) and not numbers only." ), - footer=_("Playlist limit reached: Could not add {} tracks.").format(not_added) - if not_added > 0 - else None, ) - else: - return await self.send_embed_msg( + if not await self._playlist_check(ctx): + ctx.command.reset_cooldown(ctx) + return + player = lavalink.get_player(ctx.guild.id) + tracklist = await self.fetch_playlist_tracks( + ctx, player, Query.process_input(playlist_url, self.local_folder_current_path) + ) + if isinstance(tracklist, discord.Message): + return None + if tracklist is not None: + playlist_length = len(tracklist) + not_added = 0 + if playlist_length > 10000: + tracklist = tracklist[:10000] + not_added = playlist_length - 10000 + + playlist = await create_playlist( ctx, - title=_("Playlist Couldn't be created"), - description=_("Unable to create your playlist."), + self.playlist_api, + scope, + playlist_name, + playlist_url, + tracklist, + author, + guild, ) + if playlist is not None: + return await self.send_embed_msg( + ctx, + title=_("Playlist Created"), + description=_( + "Playlist {name} (`{id}`) [**{scope}**] saved: {num} tracks added." + ).format( + name=playlist.name, + num=len(tracklist), + id=playlist.id, + scope=scope_name, + ), + footer=_("Playlist limit reached: Could not add {} tracks.").format( + not_added + ) + if not_added > 0 + else None, + ) + else: + return await self.send_embed_msg( + ctx, + title=_("Playlist Couldn't be created"), + description=_("Unable to create your playlist."), + ) @commands.cooldown(1, 30, commands.BucketType.member) @command_playlist.command( @@ -1440,120 +1482,122 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): description=_("You need the DJ role to start playing playlists."), ) return False - - try: - playlist, playlist_arg, scope = await self.get_playlist_match( - ctx, playlist_matches, scope, author, guild, specified_user - ) - except TooManyMatches as e: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg(ctx, title=str(e)) - if playlist is None: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("Could not match '{arg}' to a playlist").format(arg=playlist_arg), - ) - - if not await self._playlist_check(ctx): - ctx.command.reset_cooldown(ctx) - return - jukebox_price = await self.config.guild(ctx.guild).jukebox_price() - if not await self.maybe_charge_requester(ctx, jukebox_price): - ctx.command.reset_cooldown(ctx) - return - maxlength = await self.config.guild(ctx.guild).maxlength() - author_obj = self.bot.get_user(ctx.author.id) - track_len = 0 - try: - player = lavalink.get_player(ctx.guild.id) - tracks = playlist.tracks_obj - empty_queue = not player.queue - async for track in AsyncIter(tracks): - if len(player.queue) >= 10000: - continue - query = Query.process_input(track, self.local_folder_current_path) - if not await self.is_query_allowed( - self.config, + async with ctx.typing(): + try: + playlist, playlist_arg, scope = await self.get_playlist_match( + ctx, playlist_matches, scope, author, guild, specified_user + ) + except TooManyMatches as e: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg(ctx, title=str(e)) + if playlist is None: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( ctx, - f"{track.title} {track.author} {track.uri} " f"{str(query)}", - query_obj=query, - ): - if IS_DEBUG: - log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})") - continue - query = Query.process_input(track.uri, self.local_folder_current_path) - if query.is_local: - local_path = LocalPath(track.uri, self.local_folder_current_path) - if not await self.localtracks_folder_exists(ctx): - pass - if not local_path.exists() and not local_path.is_file(): - continue - if maxlength > 0 and not self.is_track_length_allowed(track, maxlength): - continue - track.extras.update( - { - "enqueue_time": int(time.time()), - "vc": player.channel.id, - "requester": ctx.author.id, - } + title=_("Playlist Not Found"), + description=_("Could not match '{arg}' to a playlist").format( + arg=playlist_arg + ), ) - player.add(author_obj, track) - self.bot.dispatch( - "red_audio_track_enqueue", player.channel.guild, track, ctx.author - ) - track_len += 1 - player.maybe_shuffle(0 if empty_queue else 1) - if len(tracks) > track_len: - maxlength_msg = _(" {bad_tracks} tracks cannot be queued.").format( - bad_tracks=(len(tracks) - track_len) - ) - else: - maxlength_msg = "" - if scope == PlaylistScope.GUILD.value: - scope_name = f"{guild.name}" - elif scope == PlaylistScope.USER.value: - scope_name = f"{author}" - else: - scope_name = "Global" - embed = discord.Embed( - title=_("Playlist Enqueued"), - description=_( - "{name} - (`{id}`) [**{scope}**]\nAdded {num} " - "tracks to the queue.{maxlength_msg}" - ).format( - num=track_len, - maxlength_msg=maxlength_msg, - name=playlist.name, - id=playlist.id, - scope=scope_name, - ), - ) - await self.send_embed_msg(ctx, embed=embed) - if not player.current: - await player.play() - return - except RuntimeError: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("Playlist {id} does not exist in {scope} scope.").format( - id=playlist_arg, scope=self.humanize_scope(scope, the=True) - ), - ) - except MissingGuild: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Missing Arguments"), - description=_("You need to specify the Guild ID for the guild to lookup."), - ) - except TypeError: - if playlist: - return await ctx.invoke(self.command_play, query=playlist.url) + if not await self._playlist_check(ctx): + ctx.command.reset_cooldown(ctx) + return + jukebox_price = await self.config.guild(ctx.guild).jukebox_price() + if not await self.maybe_charge_requester(ctx, jukebox_price): + ctx.command.reset_cooldown(ctx) + return + maxlength = await self.config.guild(ctx.guild).maxlength() + author_obj = self.bot.get_user(ctx.author.id) + track_len = 0 + try: + player = lavalink.get_player(ctx.guild.id) + tracks = playlist.tracks_obj + empty_queue = not player.queue + async for track in AsyncIter(tracks): + if len(player.queue) >= 10000: + continue + query = Query.process_input(track, self.local_folder_current_path) + if not await self.is_query_allowed( + self.config, + ctx, + f"{track.title} {track.author} {track.uri} " f"{str(query)}", + query_obj=query, + ): + if IS_DEBUG: + log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})") + continue + query = Query.process_input(track.uri, self.local_folder_current_path) + if query.is_local: + local_path = LocalPath(track.uri, self.local_folder_current_path) + if not await self.localtracks_folder_exists(ctx): + pass + if not local_path.exists() and not local_path.is_file(): + continue + if maxlength > 0 and not self.is_track_length_allowed(track, maxlength): + continue + track.extras.update( + { + "enqueue_time": int(time.time()), + "vc": player.channel.id, + "requester": ctx.author.id, + } + ) + player.add(author_obj, track) + self.bot.dispatch( + "red_audio_track_enqueue", player.channel.guild, track, ctx.author + ) + track_len += 1 + player.maybe_shuffle(0 if empty_queue else 1) + if len(tracks) > track_len: + maxlength_msg = _(" {bad_tracks} tracks cannot be queued.").format( + bad_tracks=(len(tracks) - track_len) + ) + else: + maxlength_msg = "" + if scope == PlaylistScope.GUILD.value: + scope_name = f"{guild.name}" + elif scope == PlaylistScope.USER.value: + scope_name = f"{author}" + else: + scope_name = "Global" + + embed = discord.Embed( + title=_("Playlist Enqueued"), + description=_( + "{name} - (`{id}`) [**{scope}**]\nAdded {num} " + "tracks to the queue.{maxlength_msg}" + ).format( + num=track_len, + maxlength_msg=maxlength_msg, + name=playlist.name, + id=playlist.id, + scope=scope_name, + ), + ) + await self.send_embed_msg(ctx, embed=embed) + if not player.current: + await player.play() + return + except RuntimeError: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_("Playlist {id} does not exist in {scope} scope.").format( + id=playlist_arg, scope=self.humanize_scope(scope, the=True) + ), + ) + except MissingGuild: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Missing Arguments"), + description=_("You need to specify the Guild ID for the guild to lookup."), + ) + except TypeError: + if playlist: + return await ctx.invoke(self.command_play, query=playlist.url) @commands.cooldown(1, 60, commands.BucketType.member) @command_playlist.command( @@ -1608,116 +1652,125 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): if scope_data is None: scope_data = [None, ctx.author, ctx.guild, False] scope, author, guild, specified_user = scope_data - try: - playlist, playlist_arg, scope = await self.get_playlist_match( - ctx, playlist_matches, scope, author, guild, specified_user - ) - except TooManyMatches as e: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg(ctx, title=str(e)) + embeds = None + async with ctx.typing(): + try: + playlist, playlist_arg, scope = await self.get_playlist_match( + ctx, playlist_matches, scope, author, guild, specified_user + ) + except TooManyMatches as e: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg(ctx, title=str(e)) - if playlist is None: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("Could not match '{arg}' to a playlist.").format(arg=playlist_arg), - ) - - if not await self._playlist_check(ctx): - ctx.command.reset_cooldown(ctx) - return - try: - if not await self.can_manage_playlist(scope, playlist, ctx, author, guild): - return - if playlist.url: - player = lavalink.get_player(ctx.guild.id) - added, removed, playlist = await self._maybe_update_playlist(ctx, player, playlist) - else: + if playlist is None: ctx.command.reset_cooldown(ctx) return await self.send_embed_msg( ctx, - title=_("Invalid Playlist"), - description=_("Custom playlists cannot be updated."), - ) - except RuntimeError: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("Playlist {id} does not exist in {scope} scope.").format( - id=playlist_arg, scope=self.humanize_scope(scope, the=True) - ), - ) - except MissingGuild: - return await self.send_embed_msg( - ctx, - title=_("Missing Arguments"), - description=_("You need to specify the Guild ID for the guild to lookup."), - ) - else: - scope_name = self.humanize_scope( - scope, ctx=guild if scope == PlaylistScope.GUILD.value else author - ) - if added or removed: - _colour = await ctx.embed_colour() - removed_embeds = [] - added_embeds = [] - total_added = len(added) - total_removed = len(removed) - total_pages = math.ceil(total_removed / 10) + math.ceil(total_added / 10) - page_count = 0 - if removed: - removed_text = "" - async for i, track in AsyncIter(removed).enumerate(start=1): - if len(track.title) > 40: - track_title = str(track.title).replace("[", "") - track_title = "{}...".format((track_title[:40]).rstrip(" ")) - else: - track_title = track.title - removed_text += f"`{i}.` **[{track_title}]({track.uri})**\n" - if i % 10 == 0 or i == total_removed: - page_count += 1 - embed = discord.Embed( - title=_("Tracks removed"), colour=_colour, description=removed_text - ) - text = _("Page {page_num}/{total_pages}").format( - page_num=page_count, total_pages=total_pages - ) - embed.set_footer(text=text) - removed_embeds.append(embed) - removed_text = "" - if added: - added_text = "" - async for i, track in AsyncIter(added).enumerate(start=1): - if len(track.title) > 40: - track_title = str(track.title).replace("[", "") - track_title = "{}...".format((track_title[:40]).rstrip(" ")) - else: - track_title = track.title - added_text += f"`{i}.` **[{track_title}]({track.uri})**\n" - if i % 10 == 0 or i == total_added: - page_count += 1 - embed = discord.Embed( - title=_("Tracks added"), colour=_colour, description=added_text - ) - text = _("Page {page_num}/{total_pages}").format( - page_num=page_count, total_pages=total_pages - ) - embed.set_footer(text=text) - added_embeds.append(embed) - added_text = "" - embeds = removed_embeds + added_embeds - await menu(ctx, embeds, DEFAULT_CONTROLS) - else: - return await self.send_embed_msg( - ctx, - title=_("Playlist Has Not Been Modified"), - description=_("No changes for {name} (`{id}`) [**{scope}**].").format( - id=playlist.id, name=playlist.name, scope=scope_name + title=_("Playlist Not Found"), + description=_("Could not match '{arg}' to a playlist.").format( + arg=playlist_arg ), ) + if not await self._playlist_check(ctx): + ctx.command.reset_cooldown(ctx) + return + try: + if not await self.can_manage_playlist(scope, playlist, ctx, author, guild): + return + if playlist.url: + player = lavalink.get_player(ctx.guild.id) + added, removed, playlist = await self._maybe_update_playlist( + ctx, player, playlist + ) + else: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Invalid Playlist"), + description=_("Custom playlists cannot be updated."), + ) + except RuntimeError: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_("Playlist {id} does not exist in {scope} scope.").format( + id=playlist_arg, scope=self.humanize_scope(scope, the=True) + ), + ) + except MissingGuild: + return await self.send_embed_msg( + ctx, + title=_("Missing Arguments"), + description=_("You need to specify the Guild ID for the guild to lookup."), + ) + else: + scope_name = self.humanize_scope( + scope, ctx=guild if scope == PlaylistScope.GUILD.value else author + ) + if added or removed: + _colour = await ctx.embed_colour() + removed_embeds = [] + added_embeds = [] + total_added = len(added) + total_removed = len(removed) + total_pages = math.ceil(total_removed / 10) + math.ceil(total_added / 10) + page_count = 0 + if removed: + removed_text = "" + async for i, track in AsyncIter(removed).enumerate(start=1): + if len(track.title) > 40: + track_title = str(track.title).replace("[", "") + track_title = "{}...".format((track_title[:40]).rstrip(" ")) + else: + track_title = track.title + removed_text += f"`{i}.` **[{track_title}]({track.uri})**\n" + if i % 10 == 0 or i == total_removed: + page_count += 1 + embed = discord.Embed( + title=_("Tracks removed"), + colour=_colour, + description=removed_text, + ) + text = _("Page {page_num}/{total_pages}").format( + page_num=page_count, total_pages=total_pages + ) + embed.set_footer(text=text) + removed_embeds.append(embed) + removed_text = "" + if added: + added_text = "" + async for i, track in AsyncIter(added).enumerate(start=1): + if len(track.title) > 40: + track_title = str(track.title).replace("[", "") + track_title = "{}...".format((track_title[:40]).rstrip(" ")) + else: + track_title = track.title + added_text += f"`{i}.` **[{track_title}]({track.uri})**\n" + if i % 10 == 0 or i == total_added: + page_count += 1 + embed = discord.Embed( + title=_("Tracks added"), colour=_colour, description=added_text + ) + text = _("Page {page_num}/{total_pages}").format( + page_num=page_count, total_pages=total_pages + ) + embed.set_footer(text=text) + added_embeds.append(embed) + added_text = "" + embeds = removed_embeds + added_embeds + else: + return await self.send_embed_msg( + ctx, + title=_("Playlist Has Not Been Modified"), + description=_("No changes for {name} (`{id}`) [**{scope}**].").format( + id=playlist.id, name=playlist.name, scope=scope_name + ), + ) + if embeds: + await menu(ctx, embeds, DEFAULT_CONTROLS) + @command_playlist.command(name="upload", usage="[args]") @commands.is_owner() async def command_playlist_upload( @@ -1770,110 +1823,115 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): scope, author, guild, specified_user = scope_data scope = scope or PlaylistScope.GUILD.value temp_playlist = cast(Playlist, FakePlaylist(author.id, scope)) - if not await self.can_manage_playlist(scope, temp_playlist, ctx, author, guild): - return + async with ctx.typing(): + if not await self.can_manage_playlist(scope, temp_playlist, ctx, author, guild): + return + if not await self._playlist_check(ctx): + return + player = lavalink.get_player(ctx.guild.id) - if not await self._playlist_check(ctx): - return - player = lavalink.get_player(ctx.guild.id) + if not ctx.message.attachments: + await self.send_embed_msg( + ctx, + title=_( + "Please upload the playlist file. Any other message will cancel this " + "operation." + ), + ) + try: + file_message = await self.bot.wait_for( + "message", timeout=30.0, check=MessagePredicate.same_context(ctx) + ) + except asyncio.TimeoutError: + return await self.send_embed_msg( + ctx, title=_("No file detected, try again later.") + ) + else: + file_message = ctx.message + try: + file_url = file_message.attachments[0].url + except IndexError: + return await self.send_embed_msg(ctx, title=_("Upload cancelled.")) + file_suffix = file_url.rsplit(".", 1)[1] + if file_suffix != "txt": + return await self.send_embed_msg( + ctx, title=_("Only Red playlist files can be uploaded.") + ) + try: + async with self.session.request("GET", file_url) as r: + uploaded_playlist = await r.json( + content_type="text/plain", encoding="utf-8", loads=json.loads + ) + except UnicodeDecodeError: + return await self.send_embed_msg(ctx, title=_("Not a valid playlist file.")) - if not ctx.message.attachments: - await self.send_embed_msg( - ctx, - title=_( - "Please upload the playlist file. Any other message will cancel this " - "operation." - ), + new_schema = uploaded_playlist.get("schema", 1) >= 2 + version = uploaded_playlist.get("version", "v2") + + if new_schema and version == "v3": + uploaded_playlist_url = uploaded_playlist.get("playlist_url", None) + track_list = uploaded_playlist.get("tracks", []) + else: + uploaded_playlist_url = uploaded_playlist.get("link", None) + track_list = uploaded_playlist.get("playlist", []) + if len(track_list) > 10000: + return await self.send_embed_msg(ctx, title=_("This playlist is too large.")) + uploaded_playlist_name = uploaded_playlist.get( + "name", (file_url.split("/")[6]).split(".")[0] ) try: - file_message = await self.bot.wait_for( - "message", timeout=30.0, check=MessagePredicate.same_context(ctx) - ) - except asyncio.TimeoutError: - return await self.send_embed_msg( - ctx, title=_("No file detected, try again later.") - ) - else: - file_message = ctx.message - try: - file_url = file_message.attachments[0].url - except IndexError: - return await self.send_embed_msg(ctx, title=_("Upload cancelled.")) - file_suffix = file_url.rsplit(".", 1)[1] - if file_suffix != "txt": - return await self.send_embed_msg( - ctx, title=_("Only Red playlist files can be uploaded.") - ) - try: - async with self.session.request("GET", file_url) as r: - uploaded_playlist = await r.json( - content_type="text/plain", encoding="utf-8", loads=json.loads - ) - except UnicodeDecodeError: - return await self.send_embed_msg(ctx, title=_("Not a valid playlist file.")) - - new_schema = uploaded_playlist.get("schema", 1) >= 2 - version = uploaded_playlist.get("version", "v2") - - if new_schema and version == "v3": - uploaded_playlist_url = uploaded_playlist.get("playlist_url", None) - track_list = uploaded_playlist.get("tracks", []) - else: - uploaded_playlist_url = uploaded_playlist.get("link", None) - track_list = uploaded_playlist.get("playlist", []) - if len(track_list) > 10000: - return await self.send_embed_msg(ctx, title=_("This playlist is too large.")) - uploaded_playlist_name = uploaded_playlist.get( - "name", (file_url.split("/")[6]).split(".")[0] - ) - try: - if self.api_interface is not None and ( - not uploaded_playlist_url - or not self.match_yt_playlist(uploaded_playlist_url) - or not ( - await self.api_interface.fetch_track( + if self.api_interface is not None and ( + not uploaded_playlist_url + or not self.match_yt_playlist(uploaded_playlist_url) + or not ( + await self.api_interface.fetch_track( + ctx, + player, + Query.process_input( + uploaded_playlist_url, self.local_folder_current_path + ), + ) + )[0].tracks + ): + if version == "v3": + return await self._load_v3_playlist( + ctx, + scope, + uploaded_playlist_name, + uploaded_playlist_url, + track_list, + author, + guild, + ) + return await self._load_v2_playlist( ctx, - player, - Query.process_input(uploaded_playlist_url, self.local_folder_current_path), - ) - )[0].tracks - ): - if version == "v3": - return await self._load_v3_playlist( - ctx, - scope, - uploaded_playlist_name, - uploaded_playlist_url, track_list, + player, + uploaded_playlist_url, + uploaded_playlist_name, + scope, author, guild, ) - return await self._load_v2_playlist( - ctx, - track_list, - player, - uploaded_playlist_url, - uploaded_playlist_name, - scope, - author, - guild, + return await ctx.invoke( + self.command_playlist_save, + playlist_name=uploaded_playlist_name, + playlist_url=uploaded_playlist_url, + scope_data=(scope, author, guild, specified_user), ) - return await ctx.invoke( - self.command_playlist_save, - playlist_name=uploaded_playlist_name, - playlist_url=uploaded_playlist_url, - scope_data=(scope, author, guild, specified_user), - ) - except TrackEnqueueError: - self.update_player_lock(ctx, False) - return await self.send_embed_msg( - ctx, - title=_("Unable to Get Track"), - description=_( - "I'm unable to get a track from Lavalink at the moment, try again in a few " - "minutes." - ), - ) + except TrackEnqueueError: + self.update_player_lock(ctx, False) + return await self.send_embed_msg( + ctx, + title=_("Unable to Get Track"), + description=_( + "I'm unable to get a track from Lavalink at the moment, try again in a few " + "minutes." + ), + ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e @commands.cooldown(1, 60, commands.BucketType.member) @command_playlist.command( @@ -1929,43 +1987,44 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass): if scope_data is None: scope_data = [None, ctx.author, ctx.guild, False] scope, author, guild, specified_user = scope_data - - new_name = new_name.split(" ")[0].strip('"')[:32] - if new_name.isnumeric(): - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Invalid Playlist Name"), - description=_( - "Playlist names must be a single word (up to 32 " - "characters) and not numbers only." - ), + async with ctx.typing(): + new_name = new_name.split(" ")[0].strip('"')[:32] + if new_name.isnumeric(): + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Invalid Playlist Name"), + description=_( + "Playlist names must be a single word (up to 32 " + "characters) and not numbers only." + ), + ) + try: + playlist, playlist_arg, scope = await self.get_playlist_match( + ctx, playlist_matches, scope, author, guild, specified_user + ) + except TooManyMatches as e: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg(ctx, title=str(e)) + if playlist is None: + ctx.command.reset_cooldown(ctx) + return await self.send_embed_msg( + ctx, + title=_("Playlist Not Found"), + description=_("Could not match '{arg}' to a playlist.").format( + arg=playlist_arg + ), + ) + if not await self.can_manage_playlist(scope, playlist, ctx, author, guild): + ctx.command.reset_cooldown(ctx) + return + scope_name = self.humanize_scope( + scope, ctx=guild if scope == PlaylistScope.GUILD.value else author ) - - try: - playlist, playlist_arg, scope = await self.get_playlist_match( - ctx, playlist_matches, scope, author, guild, specified_user + old_name = playlist.name + update = {"name": new_name} + await playlist.edit(update) + msg = _("'{old}' playlist has been renamed to '{new}' (`{id}`) [**{scope}**]").format( + old=bold(old_name), new=bold(playlist.name), id=playlist.id, scope=scope_name ) - except TooManyMatches as e: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg(ctx, title=str(e)) - if playlist is None: - ctx.command.reset_cooldown(ctx) - return await self.send_embed_msg( - ctx, - title=_("Playlist Not Found"), - description=_("Could not match '{arg}' to a playlist.").format(arg=playlist_arg), - ) - if not await self.can_manage_playlist(scope, playlist, ctx, author, guild): - ctx.command.reset_cooldown(ctx) - return - scope_name = self.humanize_scope( - scope, ctx=guild if scope == PlaylistScope.GUILD.value else author - ) - old_name = playlist.name - update = {"name": new_name} - await playlist.edit(update) - msg = _("'{old}' playlist has been renamed to '{new}' (`{id}`) [**{scope}**]").format( - old=bold(old_name), new=bold(playlist.name), id=playlist.id, scope=scope_name - ) - await self.send_embed_msg(ctx, title=_("Playlist Modified"), description=msg) + await self.send_embed_msg(ctx, title=_("Playlist Modified"), description=msg) diff --git a/redbot/cogs/audio/core/commands/queue.py b/redbot/cogs/audio/core/commands/queue.py index cc527407f..fe8937260 100644 --- a/redbot/cogs/audio/core/commands/queue.py +++ b/redbot/cogs/audio/core/commands/queue.py @@ -3,6 +3,7 @@ import contextlib import datetime import logging import math +from pathlib import Path from typing import MutableMapping, Optional @@ -10,6 +11,7 @@ import discord import lavalink from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.menus import ( DEFAULT_CONTROLS, @@ -22,9 +24,10 @@ from redbot.core.utils.menus import ( from redbot.core.utils.predicates import ReactionPredicate from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Commands.queue") +_ = Translator("Audio", Path(__file__)) class QueueCommands(MixinMeta, metaclass=CompositeMetaClass): @@ -71,9 +74,8 @@ class QueueCommands(MixinMeta, metaclass=CompositeMetaClass): await self.get_track_description(player.current, self.local_folder_current_path) or "" ) - song += _("\n Requested by: **{track.requester}**") - song += "\n\n{arrow}`{pos}`/`{dur}`" - song = song.format(track=player.current, arrow=arrow, pos=pos, dur=dur) + song += _("\n Requested by: **{track.requester}**").format(track=player.current) + song += f"\n\n{arrow}`{pos}`/`{dur}`" embed = discord.Embed(title=_("Now Playing"), description=song) guild_data = await self.config.guild(ctx.guild).all() if guild_data["thumbnail"] and player.current and player.current.thumbnail: diff --git a/redbot/cogs/audio/core/events/cog.py b/redbot/cogs/audio/core/events/cog.py index 79385f49c..682f59e1e 100644 --- a/redbot/cogs/audio/core/events/cog.py +++ b/redbot/cogs/audio/core/events/cog.py @@ -2,6 +2,7 @@ import asyncio import datetime import logging import time +from pathlib import Path from typing import Optional @@ -9,6 +10,7 @@ import discord import lavalink from redbot.core import commands +from redbot.core.i18n import Translator from ...apis.playlist_interface import Playlist, delete_playlist, get_playlist from ...audio_logging import debug_exc_log @@ -17,6 +19,7 @@ from ..abc import MixinMeta from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Events.audio") +_ = Translator("Audio", Path(__file__)) class AudioEvents(MixinMeta, metaclass=CompositeMetaClass): diff --git a/redbot/cogs/audio/core/events/dpy.py b/redbot/cogs/audio/core/events/dpy.py index b5f5315ea..76ca3981f 100644 --- a/redbot/cogs/audio/core/events/dpy.py +++ b/redbot/cogs/audio/core/events/dpy.py @@ -13,15 +13,16 @@ import lavalink from aiohttp import ClientConnectorError from discord.ext.commands import CheckFailure from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils.chat_formatting import box, humanize_list from ...audio_logging import debug_exc_log from ...errors import TrackEnqueueError from ..abc import MixinMeta -from ..cog_utils import HUMANIZED_PERM, CompositeMetaClass, _ +from ..cog_utils import HUMANIZED_PERM, CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Events.dpy") - +_ = Translator("Audio", Path(__file__)) RE_CONVERSION: Final[Pattern] = re.compile('Converting to "(.*)" failed for parameter "(.*)".') diff --git a/redbot/cogs/audio/core/events/lavalink.py b/redbot/cogs/audio/core/events/lavalink.py index 6a7dbd331..876e9c10a 100644 --- a/redbot/cogs/audio/core/events/lavalink.py +++ b/redbot/cogs/audio/core/events/lavalink.py @@ -1,15 +1,18 @@ import asyncio import contextlib import logging +from pathlib import Path import discord import lavalink +from redbot.core.i18n import Translator from ...errors import DatabaseError, TrackEnqueueError from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Events.lavalink") +_ = Translator("Audio", Path(__file__)) class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass): @@ -174,6 +177,7 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass): player.current = None if not guild_id: return + guild_id = int(guild_id) self._error_counter.setdefault(guild_id, 0) if guild_id not in self._error_counter: self._error_counter[guild_id] = 0 diff --git a/redbot/cogs/audio/core/events/red.py b/redbot/cogs/audio/core/events/red.py index 5010cc9b9..ae373ce29 100644 --- a/redbot/cogs/audio/core/events/red.py +++ b/redbot/cogs/audio/core/events/red.py @@ -1,12 +1,15 @@ import asyncio import logging +from pathlib import Path from typing import Literal, Mapping from redbot.core import commands +from redbot.core.i18n import Translator from ..abc import MixinMeta from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Events.red") +_ = Translator("Audio", Path(__file__)) class RedEvents(MixinMeta, metaclass=CompositeMetaClass): @@ -15,11 +18,11 @@ class RedEvents(MixinMeta, metaclass=CompositeMetaClass): self, service_name: str, api_tokens: Mapping[str, str] ) -> None: if service_name == "youtube": - self.api_interface.youtube_api.update_token(api_tokens) + await self.api_interface.youtube_api.update_token(api_tokens) elif service_name == "spotify": - self.api_interface.spotify_api.update_token(api_tokens) + await self.api_interface.spotify_api.update_token(api_tokens) elif service_name == "audiodb": - self.api_interface.global_cache_api.update_token(api_tokens) + await self.api_interface.global_cache_api.update_token(api_tokens) async def red_delete_data_for_user( self, diff --git a/redbot/cogs/audio/core/tasks/lavalink.py b/redbot/cogs/audio/core/tasks/lavalink.py index 05f970554..f314f785f 100644 --- a/redbot/cogs/audio/core/tasks/lavalink.py +++ b/redbot/cogs/audio/core/tasks/lavalink.py @@ -1,14 +1,17 @@ import asyncio import logging +from pathlib import Path import lavalink +from redbot.core.i18n import Translator from ...errors import LavalinkDownloadFailed from ...manager import ServerManager from ..abc import MixinMeta from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Tasks.lavalink") +_ = Translator("Audio", Path(__file__)) class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass): diff --git a/redbot/cogs/audio/core/tasks/player.py b/redbot/cogs/audio/core/tasks/player.py index d09b83d06..a35b91914 100644 --- a/redbot/cogs/audio/core/tasks/player.py +++ b/redbot/cogs/audio/core/tasks/player.py @@ -1,11 +1,13 @@ import asyncio import logging import time +from pathlib import Path from typing import Dict import lavalink +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from ...audio_logging import debug_exc_log @@ -13,6 +15,7 @@ from ..abc import MixinMeta from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Tasks.player") +_ = Translator("Audio", Path(__file__)) class PlayerTasks(MixinMeta, metaclass=CompositeMetaClass): diff --git a/redbot/cogs/audio/core/tasks/startup.py b/redbot/cogs/audio/core/tasks/startup.py index 18e01de7b..56f1aca1e 100644 --- a/redbot/cogs/audio/core/tasks/startup.py +++ b/redbot/cogs/audio/core/tasks/startup.py @@ -2,12 +2,14 @@ import asyncio import datetime import itertools import logging +from pathlib import Path from typing import Optional import lavalink from redbot.core.data_manager import cog_data_path +from redbot.core.i18n import Translator from redbot.core.utils._internal_utils import send_to_owners_with_prefix_replaced from redbot.core.utils.dbtools import APSWConnectionWrapper @@ -16,9 +18,10 @@ from ...apis.playlist_wrapper import PlaylistWrapper from ...audio_logging import debug_exc_log from ...utils import task_callback from ..abc import MixinMeta -from ..cog_utils import _, _OWNER_NOTIFICATION, _SCHEMA_VERSION, CompositeMetaClass +from ..cog_utils import _OWNER_NOTIFICATION, _SCHEMA_VERSION, CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Tasks.startup") +_ = Translator("Audio", Path(__file__)) class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass): diff --git a/redbot/cogs/audio/core/utilities/formatting.py b/redbot/cogs/audio/core/utilities/formatting.py index 84e7f694d..4cc4500c5 100644 --- a/redbot/cogs/audio/core/utilities/formatting.py +++ b/redbot/cogs/audio/core/utilities/formatting.py @@ -3,6 +3,7 @@ import logging import math import re import time +from pathlib import Path from typing import List, Optional @@ -11,16 +12,17 @@ import lavalink from discord.embeds import EmptyEmbed from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, escape from ...audio_dataclasses import LocalPath, Query from ...audio_logging import IS_DEBUG from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Utilities.formatting") - +_ = Translator("Audio", Path(__file__)) RE_SQUARE = re.compile(r"[\[\]]") @@ -157,7 +159,7 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass): if not await self.is_query_allowed( self.config, ctx, - f"{search_choice.title} {search_choice.author} {search_choice.uri} " f"{str(query)}", + f"{search_choice.title} {search_choice.author} {search_choice.uri} {str(query)}", query_obj=query, ): if IS_DEBUG: @@ -295,7 +297,7 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass): if shorten: string = f"{track.author} - {track.title}" if len(string) > 40: - string = "{}...".format((string[:40]).rstrip(" ")) + string = f"{(string[:40]).rstrip(' ')}..." string = f'**{escape(f"{string}", formatting=True)}**' else: string = ( @@ -306,7 +308,7 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass): if shorten: string = f"{track.title}" if len(string) > 40: - string = "{}...".format((string[:40]).rstrip(" ")) + string = f"{(string[:40]).rstrip(' ')}..." string = f'**{escape(f"{string}", formatting=True)}**' else: string = f'**{escape(f"{track.title}", formatting=True)}**' + escape( @@ -315,7 +317,7 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass): else: string = query.to_string_user() if shorten and len(string) > 40: - string = "{}...".format((string[:40]).rstrip(" ")) + string = f"{(string[:40]).rstrip(' ')}..." string = f'**{escape(f"{string}", formatting=True)}**' else: if track.is_stream: @@ -330,13 +332,13 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass): title = track.title string = f"{title}" if shorten and len(string) > 40: - string = "{}...".format((string[:40]).rstrip(" ")) + string = f"{(string[:40]).rstrip(' ')}..." string = re.sub(RE_SQUARE, "", string) string = f"**[{escape(string, formatting=True)}]({track.uri}) **" elif hasattr(track, "to_string_user") and track.is_local: string = track.to_string_user() + " " if shorten and len(string) > 40: - string = "{}...".format((string[:40]).rstrip(" ")) + string = f"{(string[:40]).rstrip(' ')}..." string = f'**{escape(f"{string}", formatting=True)}**' return string @@ -391,7 +393,7 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass): async def draw_time(self, ctx) -> str: player = lavalink.get_player(ctx.guild.id) paused = player.paused - pos = player.position + pos = player.position or 1 dur = getattr(player.current, "length", player.position or 1) sections = 12 loc_time = round((pos / dur if dur != 0 else pos) * sections) diff --git a/redbot/cogs/audio/core/utilities/local_tracks.py b/redbot/cogs/audio/core/utilities/local_tracks.py index 09ff73c75..62d2d7b19 100644 --- a/redbot/cogs/audio/core/utilities/local_tracks.py +++ b/redbot/cogs/audio/core/utilities/local_tracks.py @@ -8,14 +8,16 @@ import lavalink from fuzzywuzzy import process from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from ...audio_dataclasses import LocalPath, Query from ...errors import TrackEnqueueError from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Utilities.local_tracks") +_ = Translator("Audio", Path(__file__)) class LocalTrackUtilities(MixinMeta, metaclass=CompositeMetaClass): diff --git a/redbot/cogs/audio/core/utilities/miscellaneous.py b/redbot/cogs/audio/core/utilities/miscellaneous.py index f2d15670b..c99522c1b 100644 --- a/redbot/cogs/audio/core/utilities/miscellaneous.py +++ b/redbot/cogs/audio/core/utilities/miscellaneous.py @@ -5,6 +5,7 @@ import functools import json import logging import re +from pathlib import Path from typing import Any, Final, Mapping, MutableMapping, Pattern, Union, cast @@ -14,16 +15,17 @@ import lavalink from discord.embeds import EmptyEmbed from redbot.core import bank, commands from redbot.core.commands import Context +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import humanize_number from ...apis.playlist_interface import get_all_playlist_for_migration23 from ...utils import PlaylistScope, task_callback from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Utilities.miscellaneous") - +_ = Translator("Audio", Path(__file__)) _RE_TIME_CONVERTER: Final[Pattern] = re.compile(r"(?:(\d+):)?([0-5]?[0-9]):([0-5][0-9])") _prefer_lyrics_cache = {} @@ -204,11 +206,8 @@ class MiscellaneousUtilities(MixinMeta, metaclass=CompositeMetaClass): async def queue_duration(self, ctx: commands.Context) -> int: player = lavalink.get_player(ctx.guild.id) - duration = [] - async for i in AsyncIter(range(len(player.queue))): - if not player.queue[i].is_stream: - duration.append(player.queue[i].length) - queue_dur = sum(duration) + dur = [i.length async for i in AsyncIter(player.queue, steps=50)] + queue_dur = sum(dur) if not player.queue: queue_dur = 0 try: diff --git a/redbot/cogs/audio/core/utilities/player.py b/redbot/cogs/audio/core/utilities/player.py index 77d8e3b77..663d250fe 100644 --- a/redbot/cogs/audio/core/utilities/player.py +++ b/redbot/cogs/audio/core/utilities/player.py @@ -1,5 +1,6 @@ import logging import time +from pathlib import Path from typing import List, Optional, Tuple, Union @@ -9,6 +10,7 @@ import lavalink from discord.embeds import EmptyEmbed from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import bold, escape @@ -17,9 +19,10 @@ from ...audio_logging import IS_DEBUG, debug_exc_log from ...errors import QueryUnauthorized, SpotifyFetchError, TrackEnqueueError from ...utils import Notifier from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Utilities.player") +_ = Translator("Audio", Path(__file__)) class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass): @@ -279,6 +282,9 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass): return await self.send_embed_msg( ctx, title=error.message.format(prefix=ctx.prefix) ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e self.update_player_lock(ctx, False) try: if enqueue_tracks: @@ -327,16 +333,21 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass): "\nUse `{prefix}audioset spotifyapi` for instructions." ).format(prefix=ctx.prefix), ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e elif query.is_album or query.is_playlist: - self.update_player_lock(ctx, True) - track_list = await self.fetch_spotify_playlist( - ctx, - "album" if query.is_album else "playlist", - query, - enqueue_tracks, - forced=forced, - ) - self.update_player_lock(ctx, False) + try: + self.update_player_lock(ctx, True) + track_list = await self.fetch_spotify_playlist( + ctx, + "album" if query.is_album else "playlist", + query, + enqueue_tracks, + forced=forced, + ) + finally: + self.update_player_lock(ctx, False) return track_list else: return await self.send_embed_msg( @@ -387,6 +398,9 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass): "try again in a few minutes." ), ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e tracks = result.tracks playlist_data = result.playlist_info if not enqueue: @@ -581,6 +595,9 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass): if await self.bot.is_owner(ctx.author): desc = _("Please check your console or logs for details.") return await self.send_embed_msg(ctx, title=title, description=desc) + except Exception as e: + self.update_player_lock(ctx, False) + raise e description = await self.get_track_description( single_track, self.local_folder_current_path ) @@ -659,7 +676,8 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass): except Exception as e: self.update_player_lock(ctx, False) raise e - self.update_player_lock(ctx, False) + finally: + self.update_player_lock(ctx, False) return track_list async def set_player_settings(self, ctx: commands.Context) -> None: diff --git a/redbot/cogs/audio/core/utilities/playlists.py b/redbot/cogs/audio/core/utilities/playlists.py index 1b4cea9bf..2d11bf1ea 100644 --- a/redbot/cogs/audio/core/utilities/playlists.py +++ b/redbot/cogs/audio/core/utilities/playlists.py @@ -4,6 +4,7 @@ import datetime import json import logging import math +from pathlib import Path from typing import List, MutableMapping, Optional, Tuple, Union @@ -12,6 +13,7 @@ import lavalink from discord.embeds import EmptyEmbed from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box from redbot.core.utils.menus import start_adding_reactions @@ -23,9 +25,10 @@ from ...audio_logging import debug_exc_log from ...errors import TooManyMatches, TrackEnqueueError from ...utils import Notifier, PlaylistScope from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Utilities.playlists") +_ = Translator("Audio", Path(__file__)) class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass): @@ -413,6 +416,9 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass): "try again in a few minutes." ), ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e track = result.tracks[0] except Exception as err: @@ -599,6 +605,9 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass): "minutes." ), ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e tracks = result.tracks if not tracks: @@ -625,6 +634,9 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass): "minutes." ), ) + except Exception as e: + self.update_player_lock(ctx, False) + raise e tracks = result.tracks diff --git a/redbot/cogs/audio/core/utilities/queue.py b/redbot/cogs/audio/core/utilities/queue.py index 5c031fed6..06ae4e0d3 100644 --- a/redbot/cogs/audio/core/utilities/queue.py +++ b/redbot/cogs/audio/core/utilities/queue.py @@ -1,5 +1,6 @@ import logging import math +from pathlib import Path from typing import List, Tuple @@ -8,14 +9,16 @@ import lavalink from fuzzywuzzy import process from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import humanize_number from ...audio_dataclasses import LocalPath, Query from ..abc import MixinMeta -from ..cog_utils import CompositeMetaClass, _ +from ..cog_utils import CompositeMetaClass log = logging.getLogger("red.cogs.Audio.cog.Utilities.queue") +_ = Translator("Audio", Path(__file__)) class QueueUtilities(MixinMeta, metaclass=CompositeMetaClass): diff --git a/redbot/cogs/audio/equalizer.py b/redbot/cogs/audio/equalizer.py index 3f77a89c7..0d7d7ea82 100644 --- a/redbot/cogs/audio/equalizer.py +++ b/redbot/cogs/audio/equalizer.py @@ -1,7 +1,12 @@ # The equalizer class and some audio eq functions are derived from # 180093157554388993's work, with his permission +from pathlib import Path from typing import Final +from redbot.core.i18n import Translator + +_ = Translator("Audio", Path(__file__)) + class Equalizer: def __init__(self): diff --git a/redbot/cogs/audio/errors.py b/redbot/cogs/audio/errors.py index 5a3d3ecfd..197f30f1d 100644 --- a/redbot/cogs/audio/errors.py +++ b/redbot/cogs/audio/errors.py @@ -1,5 +1,11 @@ +from pathlib import Path + import aiohttp +from redbot.core.i18n import Translator + +_ = Translator("Audio", Path(__file__)) + class AudioError(Exception): """Base exception for errors in the Audio cog.""" @@ -91,6 +97,10 @@ class SpotifyFetchError(SpotifyApiError): class YouTubeApiError(ApiError): """Base exception for YouTube Data API errors.""" + def __init__(self, message, *args): + self.message = message + super().__init__(*args) + class DatabaseError(AudioError): """Base exception for database errors in the Audio cog.""" diff --git a/redbot/cogs/audio/manager.py b/redbot/cogs/audio/manager.py index f4d927ebb..68b508545 100644 --- a/redbot/cogs/audio/manager.py +++ b/redbot/cogs/audio/manager.py @@ -10,17 +10,18 @@ import shutil import sys import tempfile import time - from typing import ClassVar, Final, List, Optional, Pattern, Tuple import aiohttp +from tqdm import tqdm from redbot.core import data_manager -from tqdm import tqdm +from redbot.core.i18n import Translator from .errors import LavalinkDownloadFailed from .utils import task_callback +_ = Translator("Audio", pathlib.Path(__file__)) log = logging.getLogger("red.audio.manager") JAR_VERSION: Final[str] = "3.3.1.4" JAR_BUILD: Final[int] = 1115 diff --git a/redbot/cogs/audio/utils.py b/redbot/cogs/audio/utils.py index 206d9383b..16833d006 100644 --- a/redbot/cogs/audio/utils.py +++ b/redbot/cogs/audio/utils.py @@ -4,13 +4,16 @@ import logging import time from enum import Enum, unique +from pathlib import Path from typing import MutableMapping import discord from redbot.core import commands +from redbot.core.i18n import Translator log = logging.getLogger("red.cogs.Audio.task.callback") +_ = Translator("Audio", Path(__file__)) class CacheLevel: