mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2026-02-05 06:53:00 -05:00
Merge branch 'V3/develop' into V3/feature/mutes
This commit is contained in:
@@ -116,12 +116,14 @@ class Admin(commands.Cog):
|
||||
:param role:
|
||||
:return:
|
||||
"""
|
||||
return ctx.author.top_role > role
|
||||
return ctx.author.top_role > role or ctx.author == ctx.guild.owner
|
||||
|
||||
async def _addrole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
|
||||
async def _addrole(
|
||||
self, ctx: commands.Context, member: discord.Member, role: discord.Role, *, check_user=True
|
||||
):
|
||||
if member is None:
|
||||
member = ctx.author
|
||||
if not self.pass_user_hierarchy_check(ctx, role):
|
||||
if check_user and not self.pass_user_hierarchy_check(ctx, role):
|
||||
await ctx.send(_(USER_HIERARCHY_ISSUE_ADD).format(role=role, member=member))
|
||||
return
|
||||
if not self.pass_hierarchy_check(ctx, role):
|
||||
@@ -141,10 +143,12 @@ class Admin(commands.Cog):
|
||||
)
|
||||
)
|
||||
|
||||
async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
|
||||
async def _removerole(
|
||||
self, ctx: commands.Context, member: discord.Member, role: discord.Role, *, check_user=True
|
||||
):
|
||||
if member is None:
|
||||
member = ctx.author
|
||||
if not self.pass_user_hierarchy_check(ctx, role):
|
||||
if check_user and not self.pass_user_hierarchy_check(ctx, role):
|
||||
await ctx.send(_(USER_HIERARCHY_ISSUE_REMOVE).format(role=role, member=member))
|
||||
return
|
||||
if not self.pass_hierarchy_check(ctx, role):
|
||||
@@ -365,7 +369,7 @@ class Admin(commands.Cog):
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
await self._addrole(ctx, ctx.author, selfrole)
|
||||
await self._addrole(ctx, ctx.author, selfrole, check_user=False)
|
||||
|
||||
@selfrole.command(name="remove")
|
||||
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||
@@ -376,7 +380,7 @@ class Admin(commands.Cog):
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
await self._removerole(ctx, ctx.author, selfrole)
|
||||
await self._removerole(ctx, ctx.author, selfrole, check_user=False)
|
||||
|
||||
@selfrole.command(name="list")
|
||||
async def selfrole_list(self, ctx: commands.Context):
|
||||
@@ -406,6 +410,13 @@ class Admin(commands.Cog):
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
if not self.pass_user_hierarchy_check(ctx, role):
|
||||
await ctx.send(
|
||||
_(
|
||||
"I cannot let you add {role.name} as a selfrole because that role is higher than or equal to your highest role in the Discord hierarchy."
|
||||
).format(role=role)
|
||||
)
|
||||
return
|
||||
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||
if role.id not in curr_selfroles:
|
||||
curr_selfroles.append(role.id)
|
||||
@@ -421,6 +432,13 @@ class Admin(commands.Cog):
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
if not self.pass_user_hierarchy_check(ctx, role):
|
||||
await ctx.send(
|
||||
_(
|
||||
"I cannot let you remove {role.name} from being a selfrole because that role is higher than or equal to your highest role in the Discord hierarchy."
|
||||
).format(role=role)
|
||||
)
|
||||
return
|
||||
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||
curr_selfroles.remove(role.id)
|
||||
|
||||
|
||||
@@ -746,13 +746,17 @@ class MusicCache:
|
||||
(val, update) = await self.database.fetch_one("lavalink", "data", {"query": query})
|
||||
if update:
|
||||
val = None
|
||||
if val and not isinstance(val, str):
|
||||
if val and isinstance(val, dict):
|
||||
log.debug(f"Querying Local Database for {query}")
|
||||
task = ("update", ("lavalink", {"query": query}))
|
||||
self.append_task(ctx, *task)
|
||||
if val and not forced:
|
||||
else:
|
||||
val = None
|
||||
if val and not forced and isinstance(val, dict):
|
||||
data = val
|
||||
data["query"] = query
|
||||
if data.get("loadType") == "V2_COMPACT":
|
||||
data["loadType"] = "V2_COMPAT"
|
||||
results = LoadResult(data)
|
||||
called_api = False
|
||||
if results.has_error:
|
||||
@@ -778,21 +782,25 @@ class MusicCache:
|
||||
):
|
||||
with contextlib.suppress(SQLError):
|
||||
time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||||
task = (
|
||||
"insert",
|
||||
(
|
||||
"lavalink",
|
||||
[
|
||||
{
|
||||
"query": query,
|
||||
"data": json.dumps(results._raw),
|
||||
"last_updated": time_now,
|
||||
"last_fetched": time_now,
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
self.append_task(ctx, *task)
|
||||
data = json.dumps(results._raw)
|
||||
if all(
|
||||
k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]
|
||||
):
|
||||
task = (
|
||||
"insert",
|
||||
(
|
||||
"lavalink",
|
||||
[
|
||||
{
|
||||
"query": query,
|
||||
"data": data,
|
||||
"last_updated": time_now,
|
||||
"last_fetched": time_now,
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
self.append_task(ctx, *task)
|
||||
return results, called_api
|
||||
|
||||
async def run_tasks(self, ctx: Optional[commands.Context] = None, _id=None):
|
||||
@@ -853,10 +861,12 @@ class MusicCache:
|
||||
query_data["maxage"] = maxage_int
|
||||
|
||||
vals = await self.database.fetch_all("lavalink", "data", query_data)
|
||||
recently_played = [r.tracks for r in vals if r]
|
||||
recently_played = [r.tracks for r in vals if r if isinstance(tracks, dict)]
|
||||
|
||||
if recently_played:
|
||||
track = random.choice(recently_played)
|
||||
if track.get("loadType") == "V2_COMPACT":
|
||||
track["loadType"] = "V2_COMPAT"
|
||||
results = LoadResult(track)
|
||||
tracks = list(results.tracks)
|
||||
except Exception:
|
||||
|
||||
@@ -245,15 +245,20 @@ class Audio(commands.Cog):
|
||||
for t in tracks_in_playlist:
|
||||
uri = t.get("info", {}).get("uri")
|
||||
if uri:
|
||||
t = {"loadType": "V2_COMPACT", "tracks": [t], "query": uri}
|
||||
database_entries.append(
|
||||
{
|
||||
"query": uri,
|
||||
"data": json.dumps(t),
|
||||
"last_updated": time_now,
|
||||
"last_fetched": time_now,
|
||||
}
|
||||
)
|
||||
t = {"loadType": "V2_COMPAT", "tracks": [t], "query": uri}
|
||||
data = json.dumps(t)
|
||||
if all(
|
||||
k in data
|
||||
for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]
|
||||
):
|
||||
database_entries.append(
|
||||
{
|
||||
"query": uri,
|
||||
"data": data,
|
||||
"last_updated": time_now,
|
||||
"last_fetched": time_now,
|
||||
}
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
if guild_playlist:
|
||||
all_playlist[str(guild_id)] = guild_playlist
|
||||
@@ -530,17 +535,18 @@ class Audio(commands.Cog):
|
||||
player_check = await self._players_check()
|
||||
await self._status_check(*player_check)
|
||||
|
||||
if not autoplay and event_type == lavalink.LavalinkEvents.QUEUE_END and notify:
|
||||
notify_channel = player.fetch("channel")
|
||||
if notify_channel:
|
||||
notify_channel = self.bot.get_channel(notify_channel)
|
||||
await self._embed_msg(notify_channel, title=_("Queue Ended."))
|
||||
elif not autoplay and event_type == lavalink.LavalinkEvents.QUEUE_END and disconnect:
|
||||
self.bot.dispatch("red_audio_audio_disconnect", guild)
|
||||
await player.disconnect()
|
||||
if event_type == lavalink.LavalinkEvents.QUEUE_END and status:
|
||||
player_check = await self._players_check()
|
||||
await self._status_check(*player_check)
|
||||
if event_type == lavalink.LavalinkEvents.QUEUE_END:
|
||||
if not autoplay:
|
||||
notify_channel = player.fetch("channel")
|
||||
if notify_channel and notify:
|
||||
notify_channel = self.bot.get_channel(notify_channel)
|
||||
await self._embed_msg(notify_channel, title=_("Queue Ended."))
|
||||
if disconnect:
|
||||
self.bot.dispatch("red_audio_audio_disconnect", guild)
|
||||
await player.disconnect()
|
||||
if status:
|
||||
player_check = await self._players_check()
|
||||
await self._status_check(*player_check)
|
||||
|
||||
if event_type in [
|
||||
lavalink.LavalinkEvents.TRACK_EXCEPTION,
|
||||
@@ -690,7 +696,7 @@ class Audio(commands.Cog):
|
||||
async def dc(self, ctx: commands.Context):
|
||||
"""Toggle the bot auto-disconnecting when done playing.
|
||||
|
||||
This setting takes precedence over [p]audioset emptydisconnect.
|
||||
This setting takes precedence over `[p]audioset emptydisconnect`.
|
||||
"""
|
||||
|
||||
disconnect = await self.config.guild(ctx.guild).disconnect()
|
||||
@@ -1117,7 +1123,7 @@ class Audio(commands.Cog):
|
||||
"""Set a playlist to auto-play songs from.
|
||||
|
||||
**Usage**:
|
||||
[p]audioset autoplay playlist_name_OR_id args
|
||||
`[p]audioset autoplay playlist_name_OR_id [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -1140,9 +1146,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]audioset autoplay MyGuildPlaylist
|
||||
[p]audioset autoplay MyGlobalPlaylist --scope Global
|
||||
[p]audioset autoplay PersonalPlaylist --scope User --author Draper
|
||||
`[p]audioset autoplay MyGuildPlaylist`
|
||||
`[p]audioset autoplay MyGlobalPlaylist --scope Global`
|
||||
`[p]audioset autoplay PersonalPlaylist --scope User --author Draper`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -1253,7 +1259,10 @@ class Audio(commands.Cog):
|
||||
@audioset.command()
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def emptydisconnect(self, ctx: commands.Context, seconds: int):
|
||||
"""Auto-disconnect from channel when bot is alone in it for x seconds, 0 to disable."""
|
||||
"""Auto-disconnect from channel when bot is alone in it for x seconds, 0 to disable.
|
||||
|
||||
`[p]audioset dc` takes precedence over this setting.
|
||||
"""
|
||||
if seconds < 0:
|
||||
return await self._embed_msg(
|
||||
ctx, title=_("Invalid Time"), description=_("Seconds can't be less than zero.")
|
||||
@@ -2443,7 +2452,11 @@ class Audio(commands.Cog):
|
||||
if not await self._localtracks_check(ctx):
|
||||
return
|
||||
|
||||
return audio_data.subfolders_in_tree() if search_subfolders else audio_data.subfolders()
|
||||
return (
|
||||
await audio_data.subfolders_in_tree()
|
||||
if search_subfolders
|
||||
else await audio_data.subfolders()
|
||||
)
|
||||
|
||||
async def _folder_list(
|
||||
self, ctx: commands.Context, query: audio_dataclasses.Query
|
||||
@@ -2454,9 +2467,9 @@ class Audio(commands.Cog):
|
||||
if not query.track.exists():
|
||||
return
|
||||
return (
|
||||
query.track.tracks_in_tree()
|
||||
await query.track.tracks_in_tree()
|
||||
if query.search_subfolders
|
||||
else query.track.tracks_in_folder()
|
||||
else await query.track.tracks_in_folder()
|
||||
)
|
||||
|
||||
async def _folder_tracks(
|
||||
@@ -2495,9 +2508,9 @@ class Audio(commands.Cog):
|
||||
return
|
||||
|
||||
return (
|
||||
query.track.tracks_in_tree()
|
||||
await query.track.tracks_in_tree()
|
||||
if query.search_subfolders
|
||||
else query.track.tracks_in_folder()
|
||||
else await query.track.tracks_in_folder()
|
||||
)
|
||||
|
||||
async def _localtracks_check(self, ctx: commands.Context) -> bool:
|
||||
@@ -2948,8 +2961,7 @@ class Audio(commands.Cog):
|
||||
return await self._embed_msg(ctx, embed=embed)
|
||||
elif isinstance(tracks, discord.Message):
|
||||
return
|
||||
queue_dur = await queue_duration(ctx)
|
||||
lavalink.utils.format_time(queue_dur)
|
||||
queue_dur = await track_remaining_duration(ctx)
|
||||
index = query.track_index
|
||||
seek = 0
|
||||
if query.start_time:
|
||||
@@ -3996,7 +4008,7 @@ class Audio(commands.Cog):
|
||||
The track(s) will be appended to the end of the playlist.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist append playlist_name_OR_id track_name_OR_url args
|
||||
`[p]playlist append playlist_name_OR_id track_name_OR_url [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -4019,10 +4031,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist append MyGuildPlaylist Hello by Adele
|
||||
[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global
|
||||
[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global
|
||||
--Author Draper#6666
|
||||
`[p]playlist append MyGuildPlaylist Hello by Adele`
|
||||
`[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global`
|
||||
`[p]playlist append MyGlobalPlaylist Hello by Adele --scope Global --Author Draper#6666`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -4144,8 +4155,8 @@ class Audio(commands.Cog):
|
||||
else None,
|
||||
)
|
||||
|
||||
@commands.cooldown(1, 300, commands.BucketType.member)
|
||||
@playlist.command(name="copy", usage="<id_or_name> [args]")
|
||||
@commands.cooldown(1, 150, commands.BucketType.member)
|
||||
@playlist.command(name="copy", usage="<id_or_name> [args]", cooldown_after_parsing=True)
|
||||
async def _playlist_copy(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
@@ -4157,7 +4168,7 @@ class Audio(commands.Cog):
|
||||
"""Copy a playlist from one scope to another.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist copy playlist_name_OR_id args
|
||||
`[p]playlist copy playlist_name_OR_id [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -4184,11 +4195,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist copy MyGuildPlaylist --from-scope Guild --to-scope Global
|
||||
[p]playlist copy MyGlobalPlaylist --from-scope Global --to-author Draper#6666
|
||||
--to-scope User
|
||||
[p]playlist copy MyPersonalPlaylist --from-scope user --to-author Draper#6666
|
||||
--to-scope Guild --to-guild Red - Discord Bot
|
||||
`[p]playlist copy MyGuildPlaylist --from-scope Guild --to-scope Global`
|
||||
`[p]playlist copy MyGlobalPlaylist --from-scope Global --to-author Draper#6666 --to-scope User`
|
||||
`[p]playlist copy MyPersonalPlaylist --from-scope user --to-author Draper#6666 --to-scope Guild --to-guild Red - Discord Bot`
|
||||
"""
|
||||
|
||||
if scope_data is None:
|
||||
@@ -4284,8 +4293,8 @@ class Audio(commands.Cog):
|
||||
).format(
|
||||
name=from_playlist.name,
|
||||
from_id=from_playlist.id,
|
||||
from_scope=humanize_scope(from_scope, ctx=from_scope_name, the=True),
|
||||
to_scope=humanize_scope(to_scope, ctx=to_scope_name, the=True),
|
||||
from_scope=humanize_scope(from_scope, ctx=from_scope_name),
|
||||
to_scope=humanize_scope(to_scope, ctx=to_scope_name),
|
||||
to_id=to_playlist.id,
|
||||
),
|
||||
)
|
||||
@@ -4297,7 +4306,7 @@ class Audio(commands.Cog):
|
||||
"""Create an empty playlist.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist create playlist_name args
|
||||
`[p]playlist create playlist_name [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -4320,9 +4329,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist create MyGuildPlaylist
|
||||
[p]playlist create MyGlobalPlaylist --scope Global
|
||||
[p]playlist create MyPersonalPlaylist --scope User
|
||||
`[p]playlist create MyGuildPlaylist`
|
||||
`[p]playlist create MyGlobalPlaylist --scope Global`
|
||||
`[p]playlist create MyPersonalPlaylist --scope User`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -4364,7 +4373,7 @@ class Audio(commands.Cog):
|
||||
"""Delete a saved playlist.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist delete playlist_name_OR_id args
|
||||
`[p]playlist delete playlist_name_OR_id [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -4387,9 +4396,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist delete MyGuildPlaylist
|
||||
[p]playlist delete MyGlobalPlaylist --scope Global
|
||||
[p]playlist delete MyPersonalPlaylist --scope User
|
||||
`[p]playlist delete MyGuildPlaylist`
|
||||
`[p]playlist delete MyGlobalPlaylist --scope Global`
|
||||
`[p]playlist delete MyPersonalPlaylist --scope User`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -4438,7 +4447,9 @@ class Audio(commands.Cog):
|
||||
)
|
||||
|
||||
@commands.cooldown(1, 30, commands.BucketType.member)
|
||||
@playlist.command(name="dedupe", usage="<playlist_name_OR_id> [args]")
|
||||
@playlist.command(
|
||||
name="dedupe", usage="<playlist_name_OR_id> [args]", cooldown_after_parsing=True
|
||||
)
|
||||
async def _playlist_remdupe(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
@@ -4449,7 +4460,7 @@ class Audio(commands.Cog):
|
||||
"""Remove duplicate tracks from a saved playlist.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist dedupe playlist_name_OR_id args
|
||||
`[p]playlist dedupe playlist_name_OR_id [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -4472,9 +4483,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist dedupe MyGuildPlaylist
|
||||
[p]playlist dedupe MyGlobalPlaylist --scope Global
|
||||
[p]playlist dedupe MyPersonalPlaylist --scope User
|
||||
`[p]playlist dedupe MyGuildPlaylist`
|
||||
`[p]playlist dedupe MyGlobalPlaylist --scope Global`
|
||||
`[p]playlist dedupe MyPersonalPlaylist --scope User`
|
||||
"""
|
||||
async with ctx.typing():
|
||||
if scope_data is None:
|
||||
@@ -4571,9 +4582,13 @@ class Audio(commands.Cog):
|
||||
)
|
||||
|
||||
@checks.is_owner()
|
||||
@playlist.command(name="download", usage="<playlist_name_OR_id> [v2=False] [args]")
|
||||
@playlist.command(
|
||||
name="download",
|
||||
usage="<playlist_name_OR_id> [v2=False] [args]",
|
||||
cooldown_after_parsing=True,
|
||||
)
|
||||
@commands.bot_has_permissions(attach_files=True)
|
||||
@commands.cooldown(1, 60, commands.BucketType.guild)
|
||||
@commands.cooldown(1, 30, commands.BucketType.guild)
|
||||
async def _playlist_download(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
@@ -4584,12 +4599,12 @@ class Audio(commands.Cog):
|
||||
):
|
||||
"""Download a copy of a playlist.
|
||||
|
||||
These files can be used with the [p]playlist upload command.
|
||||
These files can be used with the `[p]playlist upload` command.
|
||||
Red v2-compatible playlists can be generated by passing True
|
||||
for the v2 variable.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist download playlist_name_OR_id [v2=True_OR_False] args
|
||||
`[p]playlist download playlist_name_OR_id [v2=True_OR_False] [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -4612,9 +4627,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist download MyGuildPlaylist True
|
||||
[p]playlist download MyGlobalPlaylist False --scope Global
|
||||
[p]playlist download MyPersonalPlaylist --scope User
|
||||
`[p]playlist download MyGuildPlaylist True`
|
||||
`[p]playlist download MyGlobalPlaylist False --scope Global`
|
||||
`[p]playlist download MyPersonalPlaylist --scope User`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -4715,8 +4730,10 @@ class Audio(commands.Cog):
|
||||
await ctx.send(file=discord.File(to_write, filename=f"{file_name}.txt"))
|
||||
to_write.close()
|
||||
|
||||
@commands.cooldown(1, 20, commands.BucketType.member)
|
||||
@playlist.command(name="info", usage="<playlist_name_OR_id> [args]")
|
||||
@commands.cooldown(1, 10, commands.BucketType.member)
|
||||
@playlist.command(
|
||||
name="info", usage="<playlist_name_OR_id> [args]", cooldown_after_parsing=True
|
||||
)
|
||||
async def _playlist_info(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
@@ -4727,7 +4744,7 @@ class Audio(commands.Cog):
|
||||
"""Retrieve information from a saved playlist.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist info playlist_name_OR_id args
|
||||
`[p]playlist info playlist_name_OR_id [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -4750,9 +4767,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist info MyGuildPlaylist
|
||||
[p]playlist info MyGlobalPlaylist --scope Global
|
||||
[p]playlist info MyPersonalPlaylist --scope User
|
||||
`[p]playlist info MyGuildPlaylist`
|
||||
`[p]playlist info MyGlobalPlaylist --scope Global`
|
||||
`[p]playlist info MyPersonalPlaylist --scope User`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -4852,14 +4869,14 @@ class Audio(commands.Cog):
|
||||
page_list.append(embed)
|
||||
await menu(ctx, page_list, DEFAULT_CONTROLS)
|
||||
|
||||
@commands.cooldown(1, 30, commands.BucketType.guild)
|
||||
@playlist.command(name="list", usage="[args]")
|
||||
@commands.cooldown(1, 15, commands.BucketType.guild)
|
||||
@playlist.command(name="list", usage="[args]", cooldown_after_parsing=True)
|
||||
@commands.bot_has_permissions(add_reactions=True)
|
||||
async def _playlist_list(self, ctx: commands.Context, *, scope_data: ScopeParser = None):
|
||||
"""List saved playlists.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist list args
|
||||
`[p]playlist list [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -4882,9 +4899,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist list
|
||||
[p]playlist list --scope Global
|
||||
[p]playlist list --scope User
|
||||
`[p]playlist list`
|
||||
`[p]playlist list --scope Global`
|
||||
`[p]playlist list --scope User`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -4976,15 +4993,15 @@ class Audio(commands.Cog):
|
||||
)
|
||||
return embed
|
||||
|
||||
@playlist.command(name="queue", usage="<name> [args]")
|
||||
@commands.cooldown(1, 600, commands.BucketType.member)
|
||||
@playlist.command(name="queue", usage="<name> [args]", cooldown_after_parsing=True)
|
||||
@commands.cooldown(1, 300, commands.BucketType.member)
|
||||
async def _playlist_queue(
|
||||
self, ctx: commands.Context, playlist_name: str, *, scope_data: ScopeParser = None
|
||||
):
|
||||
"""Save the queue to a playlist.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist queue playlist_name
|
||||
`[p]playlist queue playlist_name [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -5007,9 +5024,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist queue MyGuildPlaylist
|
||||
[p]playlist queue MyGlobalPlaylist --scope Global
|
||||
[p]playlist queue MyPersonalPlaylist --scope User
|
||||
`[p]playlist queue MyGuildPlaylist`
|
||||
`[p]playlist queue MyGlobalPlaylist --scope Global`
|
||||
`[p]playlist queue MyPersonalPlaylist --scope User`
|
||||
"""
|
||||
async with ctx.typing():
|
||||
if scope_data is None:
|
||||
@@ -5087,7 +5104,7 @@ class Audio(commands.Cog):
|
||||
"""Remove a track from a playlist by url.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist remove playlist_name_OR_id url args
|
||||
`[p]playlist remove playlist_name_OR_id url [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -5110,11 +5127,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist remove MyGuildPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU
|
||||
[p]playlist remove MyGlobalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU
|
||||
--scope Global
|
||||
[p]playlist remove MyPersonalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU
|
||||
--scope User
|
||||
`[p]playlist remove MyGuildPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU`
|
||||
`[p]playlist remove MyGlobalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU --scope Global`
|
||||
`[p]playlist remove MyPersonalPlaylist https://www.youtube.com/watch?v=MN3x-kAbgFU --scope User`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -5188,8 +5203,8 @@ class Audio(commands.Cog):
|
||||
).format(playlist_name=playlist.name, id=playlist.id, scope=scope_name),
|
||||
)
|
||||
|
||||
@playlist.command(name="save", usage="<name> <url> [args]")
|
||||
@commands.cooldown(1, 120, commands.BucketType.member)
|
||||
@playlist.command(name="save", usage="<name> <url> [args]", cooldown_after_parsing=True)
|
||||
@commands.cooldown(1, 60, commands.BucketType.member)
|
||||
async def _playlist_save(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
@@ -5201,7 +5216,7 @@ class Audio(commands.Cog):
|
||||
"""Save a playlist from a url.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist save name url args
|
||||
`[p]playlist save name url [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -5224,12 +5239,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist save MyGuildPlaylist
|
||||
https://www.youtube.com/playlist?list=PLx0sYbCqOb8Q_CLZC2BdBSKEEB59BOPUM
|
||||
[p]playlist save MyGlobalPlaylist
|
||||
https://www.youtube.com/playlist?list=PLx0sYbCqOb8Q_CLZC2BdBSKEEB59BOPUM --scope Global
|
||||
[p]playlist save MyPersonalPlaylist
|
||||
https://open.spotify.com/playlist/1RyeIbyFeIJVnNzlGr5KkR --scope User
|
||||
`[p]playlist save MyGuildPlaylist https://www.youtube.com/playlist?list=PLx0sYbCqOb8Q_CLZC2BdBSKEEB59BOPUM`
|
||||
`[p]playlist save MyGlobalPlaylist https://www.youtube.com/playlist?list=PLx0sYbCqOb8Q_CLZC2BdBSKEEB59BOPUM --scope Global`
|
||||
`[p]playlist save MyPersonalPlaylist https://open.spotify.com/playlist/1RyeIbyFeIJVnNzlGr5KkR --scope User`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -5282,8 +5294,13 @@ class Audio(commands.Cog):
|
||||
else None,
|
||||
)
|
||||
|
||||
@commands.cooldown(1, 60, commands.BucketType.member)
|
||||
@playlist.command(name="start", aliases=["play"], usage="<playlist_name_OR_id> [args]")
|
||||
@commands.cooldown(1, 30, commands.BucketType.member)
|
||||
@playlist.command(
|
||||
name="start",
|
||||
aliases=["play"],
|
||||
usage="<playlist_name_OR_id> [args]",
|
||||
cooldown_after_parsing=True,
|
||||
)
|
||||
async def _playlist_start(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
@@ -5294,7 +5311,7 @@ class Audio(commands.Cog):
|
||||
"""Load a playlist into the queue.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist start playlist_name_OR_id args
|
||||
` [p]playlist start playlist_name_OR_id [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -5317,9 +5334,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist start MyGuildPlaylist
|
||||
[p]playlist start MyGlobalPlaylist --scope Global
|
||||
[p]playlist start MyPersonalPlaylist --scope User
|
||||
`[p]playlist start MyGuildPlaylist`
|
||||
`[p]playlist start MyGlobalPlaylist --scope Global`
|
||||
`[p]playlist start MyPersonalPlaylist --scope User`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -5451,7 +5468,9 @@ class Audio(commands.Cog):
|
||||
return await ctx.invoke(self.play, query=playlist.url)
|
||||
|
||||
@commands.cooldown(1, 60, commands.BucketType.member)
|
||||
@playlist.command(name="update", usage="<playlist_name_OR_id> [args]")
|
||||
@playlist.command(
|
||||
name="update", usage="<playlist_name_OR_id> [args]", cooldown_after_parsing=True
|
||||
)
|
||||
async def _playlist_update(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
@@ -5462,7 +5481,7 @@ class Audio(commands.Cog):
|
||||
"""Updates all tracks in a playlist.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist update playlist_name_OR_id args
|
||||
`[p]playlist update playlist_name_OR_id [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -5485,9 +5504,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist update MyGuildPlaylist
|
||||
[p]playlist update MyGlobalPlaylist --scope Global
|
||||
[p]playlist update MyPersonalPlaylist --scope User
|
||||
`[p]playlist update MyGuildPlaylist`
|
||||
`[p]playlist update MyGlobalPlaylist --scope Global`
|
||||
`[p]playlist update MyPersonalPlaylist --scope User`
|
||||
"""
|
||||
|
||||
if scope_data is None:
|
||||
@@ -5610,10 +5629,10 @@ class Audio(commands.Cog):
|
||||
"""Uploads a playlist file as a playlist for the bot.
|
||||
|
||||
V2 and old V3 playlist will be slow.
|
||||
V3 Playlist made with [p]playlist download will load a lot faster.
|
||||
V3 Playlist made with `[p]playlist download` will load a lot faster.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist upload args
|
||||
`[p]playlist upload [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -5636,9 +5655,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist upload
|
||||
[p]playlist upload --scope Global
|
||||
[p]playlist upload --scope User
|
||||
`[p]playlist upload`
|
||||
`[p]playlist upload --scope Global`
|
||||
`[p]playlist upload --scope User`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -5728,7 +5747,9 @@ class Audio(commands.Cog):
|
||||
)
|
||||
|
||||
@commands.cooldown(1, 60, commands.BucketType.member)
|
||||
@playlist.command(name="rename", usage="<playlist_name_OR_id> <new_name> [args]")
|
||||
@playlist.command(
|
||||
name="rename", usage="<playlist_name_OR_id> <new_name> [args]", cooldown_after_parsing=True
|
||||
)
|
||||
async def _playlist_rename(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
@@ -5740,7 +5761,7 @@ class Audio(commands.Cog):
|
||||
"""Rename an existing playlist.
|
||||
|
||||
**Usage**:
|
||||
[p]playlist rename playlist_name_OR_id new_name args
|
||||
`[p]playlist rename playlist_name_OR_id new_name [args]`
|
||||
|
||||
**Args**:
|
||||
The following are all optional:
|
||||
@@ -5763,9 +5784,9 @@ class Audio(commands.Cog):
|
||||
Exact guild name
|
||||
|
||||
Example use:
|
||||
[p]playlist rename MyGuildPlaylist RenamedGuildPlaylist
|
||||
[p]playlist rename MyGlobalPlaylist RenamedGlobalPlaylist --scope Global
|
||||
[p]playlist rename MyPersonalPlaylist RenamedPersonalPlaylist --scope User
|
||||
`[p]playlist rename MyGuildPlaylist RenamedGuildPlaylist`
|
||||
`[p]playlist rename MyGlobalPlaylist RenamedGlobalPlaylist --scope Global`
|
||||
`[p]playlist rename MyPersonalPlaylist RenamedPersonalPlaylist --scope User`
|
||||
"""
|
||||
if scope_data is None:
|
||||
scope_data = [PlaylistScope.GUILD.value, ctx.author, ctx.guild, False]
|
||||
@@ -5882,15 +5903,17 @@ class Audio(commands.Cog):
|
||||
for t in track_list:
|
||||
uri = t.get("info", {}).get("uri")
|
||||
if uri:
|
||||
t = {"loadType": "V2_COMPACT", "tracks": [t], "query": uri}
|
||||
database_entries.append(
|
||||
{
|
||||
"query": uri,
|
||||
"data": json.dumps(t),
|
||||
"last_updated": time_now,
|
||||
"last_fetched": time_now,
|
||||
}
|
||||
)
|
||||
t = {"loadType": "V2_COMPAT", "tracks": [t], "query": uri}
|
||||
data = json.dumps(t)
|
||||
if all(k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]):
|
||||
database_entries.append(
|
||||
{
|
||||
"query": uri,
|
||||
"data": data,
|
||||
"last_updated": time_now,
|
||||
"last_fetched": time_now,
|
||||
}
|
||||
)
|
||||
if database_entries:
|
||||
await self.music_cache.database.insert("lavalink", database_entries)
|
||||
|
||||
@@ -6793,8 +6816,8 @@ class Audio(commands.Cog):
|
||||
async def search(self, ctx: commands.Context, *, query: str):
|
||||
"""Pick a track with a search.
|
||||
|
||||
Use `[p]search list <search term>` to queue all tracks found on YouTube. `[p]search sc
|
||||
<search term>` will search SoundCloud instead of YouTube.
|
||||
Use `[p]search list <search term>` to queue all tracks found on YouTube.
|
||||
`[p]search sc<search term>` will search SoundCloud instead of YouTube.
|
||||
"""
|
||||
|
||||
async def _search_menu(
|
||||
@@ -7357,8 +7380,8 @@ class Audio(commands.Cog):
|
||||
async def _shuffle_bumpped(self, ctx: commands.Context):
|
||||
"""Toggle bumped track shuffle.
|
||||
|
||||
Set this to disabled if you wish to avoid bumped songs being shuffled. This takes priority
|
||||
over `[p]shuffle`.
|
||||
Set this to disabled if you wish to avoid bumped songs being shuffled.
|
||||
This takes priority over `[p]shuffle`.
|
||||
"""
|
||||
dj_enabled = self._dj_status_cache.setdefault(
|
||||
ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import glob
|
||||
import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
from pathlib import Path, PosixPath, WindowsPath
|
||||
from typing import List, Optional, Union, MutableMapping
|
||||
from typing import List, Optional, Union, MutableMapping, Iterator, AsyncIterator
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import lavalink
|
||||
@@ -167,29 +170,48 @@ class LocalPath:
|
||||
modified.path = modified.path.joinpath(*args)
|
||||
return modified
|
||||
|
||||
def multiglob(self, *patterns):
|
||||
paths = []
|
||||
def rglob(self, pattern, folder=False) -> Iterator[str]:
|
||||
if folder:
|
||||
return glob.iglob(f"{self.path}{os.sep}**{os.sep}", recursive=True)
|
||||
else:
|
||||
return glob.iglob(f"{self.path}{os.sep}**{os.sep}{pattern}", recursive=True)
|
||||
|
||||
def glob(self, pattern, folder=False) -> Iterator[str]:
|
||||
if folder:
|
||||
return glob.iglob(f"{self.path}{os.sep}*{os.sep}", recursive=False)
|
||||
else:
|
||||
return glob.iglob(f"{self.path}{os.sep}*{pattern}", recursive=False)
|
||||
|
||||
async def multiglob(self, *patterns, folder=False) -> AsyncIterator["LocalPath"]:
|
||||
for p in patterns:
|
||||
paths.extend(list(self.path.glob(p)))
|
||||
for p in self._filtered(paths):
|
||||
yield p
|
||||
for rp in self.glob(p):
|
||||
rp = LocalPath(rp)
|
||||
if folder and rp.is_dir() and rp.exists():
|
||||
yield rp
|
||||
await asyncio.sleep(0)
|
||||
else:
|
||||
if rp.suffix in self._all_music_ext and rp.is_file() and rp.exists():
|
||||
yield rp
|
||||
await asyncio.sleep(0)
|
||||
|
||||
def multirglob(self, *patterns):
|
||||
paths = []
|
||||
async def multirglob(self, *patterns, folder=False) -> AsyncIterator["LocalPath"]:
|
||||
for p in patterns:
|
||||
paths.extend(list(self.path.rglob(p)))
|
||||
|
||||
for p in self._filtered(paths):
|
||||
yield p
|
||||
|
||||
def _filtered(self, paths: List[Path]):
|
||||
for p in paths:
|
||||
if p.suffix in self._all_music_ext:
|
||||
yield p
|
||||
for rp in self.rglob(p):
|
||||
rp = LocalPath(rp)
|
||||
if folder and rp.is_dir() and rp.exists():
|
||||
yield rp
|
||||
await asyncio.sleep(0)
|
||||
else:
|
||||
if rp.suffix in self._all_music_ext and rp.is_file() and rp.exists():
|
||||
yield rp
|
||||
await asyncio.sleep(0)
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def to_string(self):
|
||||
try:
|
||||
return str(self.path.absolute())
|
||||
@@ -209,48 +231,56 @@ class LocalPath:
|
||||
string = f"...{os.sep}{string}"
|
||||
return string
|
||||
|
||||
def tracks_in_tree(self):
|
||||
async def tracks_in_tree(self):
|
||||
tracks = []
|
||||
for track in self.multirglob(*[f"*{ext}" for ext in self._all_music_ext]):
|
||||
if track.exists() and track.is_file() and track.parent != self.localtrack_folder:
|
||||
tracks.append(Query.process_input(LocalPath(str(track.absolute()))))
|
||||
async for track in self.multirglob(*[f"{ext}" for ext in self._all_music_ext]):
|
||||
with contextlib.suppress(ValueError):
|
||||
if track.path.parent != self.localtrack_folder and track.path.relative_to(
|
||||
self.path
|
||||
):
|
||||
tracks.append(Query.process_input(track))
|
||||
return sorted(tracks, key=lambda x: x.to_string_user().lower())
|
||||
|
||||
def subfolders_in_tree(self):
|
||||
files = list(self.multirglob(*[f"*{ext}" for ext in self._all_music_ext]))
|
||||
folders = []
|
||||
for f in files:
|
||||
if f.exists() and f.parent not in folders and f.parent != self.localtrack_folder:
|
||||
folders.append(f.parent)
|
||||
async def subfolders_in_tree(self):
|
||||
return_folders = []
|
||||
for folder in folders:
|
||||
if folder.exists() and folder.is_dir():
|
||||
return_folders.append(LocalPath(str(folder.absolute())))
|
||||
async for f in self.multirglob("", folder=True):
|
||||
with contextlib.suppress(ValueError):
|
||||
if (
|
||||
f not in return_folders
|
||||
and f.path != self.localtrack_folder
|
||||
and f.path.relative_to(self.path)
|
||||
):
|
||||
return_folders.append(f)
|
||||
return sorted(return_folders, key=lambda x: x.to_string_user().lower())
|
||||
|
||||
def tracks_in_folder(self):
|
||||
async def tracks_in_folder(self):
|
||||
tracks = []
|
||||
for track in self.multiglob(*[f"*{ext}" for ext in self._all_music_ext]):
|
||||
if track.exists() and track.is_file() and track.parent != self.localtrack_folder:
|
||||
tracks.append(Query.process_input(LocalPath(str(track.absolute()))))
|
||||
async for track in self.multiglob(*[f"{ext}" for ext in self._all_music_ext]):
|
||||
with contextlib.suppress(ValueError):
|
||||
if track.path.parent != self.localtrack_folder and track.path.relative_to(
|
||||
self.path
|
||||
):
|
||||
tracks.append(Query.process_input(track))
|
||||
return sorted(tracks, key=lambda x: x.to_string_user().lower())
|
||||
|
||||
def subfolders(self):
|
||||
files = list(self.multiglob(*[f"*{ext}" for ext in self._all_music_ext]))
|
||||
folders = []
|
||||
for f in files:
|
||||
if f.exists() and f.parent not in folders and f.parent != self.localtrack_folder:
|
||||
folders.append(f.parent)
|
||||
async def subfolders(self):
|
||||
return_folders = []
|
||||
for folder in folders:
|
||||
if folder.exists() and folder.is_dir():
|
||||
return_folders.append(LocalPath(str(folder.absolute())))
|
||||
async for f in self.multiglob("", folder=True):
|
||||
with contextlib.suppress(ValueError):
|
||||
if (
|
||||
f not in return_folders
|
||||
and f.path != self.localtrack_folder
|
||||
and f.path.relative_to(self.path)
|
||||
):
|
||||
return_folders.append(f)
|
||||
return sorted(return_folders, key=lambda x: x.to_string_user().lower())
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, LocalPath):
|
||||
return NotImplemented
|
||||
return self.path._cparts == other.path._cparts
|
||||
if isinstance(other, LocalPath):
|
||||
return self.path._cparts == other.path._cparts
|
||||
elif isinstance(other, Path):
|
||||
return self.path._cparts == other._cpart
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self):
|
||||
try:
|
||||
@@ -260,24 +290,32 @@ class LocalPath:
|
||||
return self._hash
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, LocalPath):
|
||||
return NotImplemented
|
||||
return self.path._cparts < other.path._cparts
|
||||
if isinstance(other, LocalPath):
|
||||
return self.path._cparts < other.path._cparts
|
||||
elif isinstance(other, Path):
|
||||
return self.path._cparts < other._cpart
|
||||
return NotImplemented
|
||||
|
||||
def __le__(self, other):
|
||||
if not isinstance(other, LocalPath):
|
||||
return NotImplemented
|
||||
return self.path._cparts <= other.path._cparts
|
||||
if isinstance(other, LocalPath):
|
||||
return self.path._cparts <= other.path._cparts
|
||||
elif isinstance(other, Path):
|
||||
return self.path._cparts <= other._cpart
|
||||
return NotImplemented
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, LocalPath):
|
||||
return NotImplemented
|
||||
return self.path._cparts > other.path._cparts
|
||||
if isinstance(other, LocalPath):
|
||||
return self.path._cparts > other.path._cparts
|
||||
elif isinstance(other, Path):
|
||||
return self.path._cparts > other._cpart
|
||||
return NotImplemented
|
||||
|
||||
def __ge__(self, other):
|
||||
if not isinstance(other, LocalPath):
|
||||
return NotImplemented
|
||||
return self.path._cparts >= other.path._cparts
|
||||
if isinstance(other, LocalPath):
|
||||
return self.path._cparts >= other.path._cparts
|
||||
elif isinstance(other, Path):
|
||||
return self.path._cparts >= other._cpart
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class Query:
|
||||
@@ -378,6 +416,10 @@ class Query:
|
||||
|
||||
if isinstance(query, str):
|
||||
query = query.strip("<>")
|
||||
while "ytsearch:" in query:
|
||||
query = query.replace("ytsearch:", "")
|
||||
while "scsearch:" in query:
|
||||
query = query.replace("scsearch:", "")
|
||||
|
||||
elif isinstance(query, Query):
|
||||
for key, val in kwargs.items():
|
||||
|
||||
@@ -43,6 +43,7 @@ __all__ = [
|
||||
"CacheLevel",
|
||||
"format_playlist_picker_data",
|
||||
"get_track_description_unformatted",
|
||||
"track_remaining_duration",
|
||||
"Notifier",
|
||||
"PlaylistScope",
|
||||
]
|
||||
@@ -126,6 +127,20 @@ async def queue_duration(ctx) -> int:
|
||||
return queue_total_duration
|
||||
|
||||
|
||||
async def track_remaining_duration(ctx) -> int:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if not player.current:
|
||||
return 0
|
||||
try:
|
||||
if not player.current.is_stream:
|
||||
remain = player.current.length - player.position
|
||||
else:
|
||||
remain = 0
|
||||
except AttributeError:
|
||||
remain = 0
|
||||
return remain
|
||||
|
||||
|
||||
async def draw_time(ctx) -> str:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
paused = player.paused
|
||||
@@ -213,7 +228,7 @@ async def clear_react(bot: Red, message: discord.Message, emoji: MutableMapping
|
||||
def get_track_description(track) -> Optional[str]:
|
||||
if track and getattr(track, "uri", None):
|
||||
query = Query.process_input(track.uri)
|
||||
if query.is_local:
|
||||
if query.is_local or "localtracks/" in track.uri:
|
||||
if track.title != "Unknown title":
|
||||
return f'**{escape(f"{track.author} - {track.title}")}**' + escape(
|
||||
f"\n{query.to_string_user()} "
|
||||
@@ -229,7 +244,7 @@ def get_track_description(track) -> Optional[str]:
|
||||
def get_track_description_unformatted(track) -> Optional[str]:
|
||||
if track and hasattr(track, "uri"):
|
||||
query = Query.process_input(track.uri)
|
||||
if query.is_local:
|
||||
if query.is_local or "localtracks/" in track.uri:
|
||||
if track.title != "Unknown title":
|
||||
return escape(f"{track.author} - {track.title}")
|
||||
else:
|
||||
@@ -521,8 +536,8 @@ class PlaylistScope(Enum):
|
||||
def humanize_scope(scope, ctx=None, the=None):
|
||||
|
||||
if scope == PlaylistScope.GLOBAL.value:
|
||||
return _("the ") if the else "" + _("Global")
|
||||
return (_("the ") if the else "") + _("Global")
|
||||
elif scope == PlaylistScope.GUILD.value:
|
||||
return ctx.name if ctx else _("the ") if the else "" + _("Server")
|
||||
return ctx.name if ctx else (_("the ") if the else "") + _("Server")
|
||||
elif scope == PlaylistScope.USER.value:
|
||||
return str(ctx) if ctx else _("the ") if the else "" + _("User")
|
||||
return str(ctx) if ctx else (_("the ") if the else "") + _("User")
|
||||
|
||||
@@ -418,6 +418,11 @@ class Downloader(commands.Cog):
|
||||
elif target.is_file():
|
||||
os.remove(str(target))
|
||||
|
||||
@staticmethod
|
||||
async def send_pagified(target: discord.abc.Messageable, content: str) -> None:
|
||||
for page in pagify(content):
|
||||
await target.send(page)
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def pipinstall(self, ctx: commands.Context, *deps: str) -> None:
|
||||
@@ -550,7 +555,7 @@ class Downloader(commands.Cog):
|
||||
if failed:
|
||||
message += "\n" + self.format_failed_repos(failed)
|
||||
|
||||
await ctx.send(message)
|
||||
await self.send_pagified(ctx, message)
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
@@ -596,12 +601,13 @@ class Downloader(commands.Cog):
|
||||
tuple(map(inline, libnames))
|
||||
)
|
||||
if message:
|
||||
await ctx.send(
|
||||
await self.send_pagified(
|
||||
ctx,
|
||||
_(
|
||||
"Cog requirements and shared libraries for all installed cogs"
|
||||
" have been reinstalled but there were some errors:\n"
|
||||
)
|
||||
+ message
|
||||
+ message,
|
||||
)
|
||||
else:
|
||||
await ctx.send(
|
||||
@@ -643,8 +649,7 @@ class Downloader(commands.Cog):
|
||||
f"**{candidate.object_type} {candidate.rev}**"
|
||||
f" - {candidate.description}\n"
|
||||
)
|
||||
for page in pagify(msg):
|
||||
await ctx.send(msg)
|
||||
await self.send_pagified(ctx, msg)
|
||||
return
|
||||
except errors.UnknownRevision:
|
||||
await ctx.send(
|
||||
@@ -658,14 +663,14 @@ class Downloader(commands.Cog):
|
||||
async with repo.checkout(commit, exit_to_rev=repo.branch):
|
||||
cogs, message = await self._filter_incorrect_cogs_by_names(repo, cog_names)
|
||||
if not cogs:
|
||||
await ctx.send(message)
|
||||
await self.send_pagified(ctx, message)
|
||||
return
|
||||
failed_reqs = await self._install_requirements(cogs)
|
||||
if failed_reqs:
|
||||
message += _("\nFailed to install requirements: ") + humanize_list(
|
||||
tuple(map(inline, failed_reqs))
|
||||
)
|
||||
await ctx.send(message)
|
||||
await self.send_pagified(ctx, message)
|
||||
return
|
||||
|
||||
installed_cogs, failed_cogs = await self._install_cogs(cogs)
|
||||
@@ -711,7 +716,7 @@ class Downloader(commands.Cog):
|
||||
+ message
|
||||
)
|
||||
# "---" added to separate cog install messages from Downloader's message
|
||||
await ctx.send(f"{message}{deprecation_notice}\n---")
|
||||
await self.send_pagified(ctx, f"{message}{deprecation_notice}\n---")
|
||||
for cog in installed_cogs:
|
||||
if cog.install_msg:
|
||||
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
||||
@@ -748,14 +753,18 @@ class Downloader(commands.Cog):
|
||||
message += _("Successfully uninstalled cogs: ") + humanize_list(uninstalled_cogs)
|
||||
if failed_cogs:
|
||||
message += (
|
||||
_("\nThese cog were installed but can no longer be located: ")
|
||||
_(
|
||||
"\nDownloader has removed these cogs from the installed cogs list"
|
||||
" but it wasn't able to find their files: "
|
||||
)
|
||||
+ humanize_list(tuple(map(inline, failed_cogs)))
|
||||
+ _(
|
||||
"\nYou may need to remove their files manually if they are still usable."
|
||||
" Also make sure you've unloaded those cogs with `{prefix}unload {cogs}`."
|
||||
"\nThey were most likely removed without using `{prefix}cog uninstall`.\n"
|
||||
"You may need to remove those files manually if the cogs are still usable."
|
||||
" If so, ensure the cogs have been unloaded with `{prefix}unload {cogs}`."
|
||||
).format(prefix=ctx.prefix, cogs=" ".join(failed_cogs))
|
||||
)
|
||||
await ctx.send(message)
|
||||
await self.send_pagified(ctx, message)
|
||||
|
||||
@cog.command(name="pin", usage="<cogs>")
|
||||
async def _cog_pin(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
||||
@@ -778,7 +787,7 @@ class Downloader(commands.Cog):
|
||||
message += _("Pinned cogs: ") + humanize_list(cognames)
|
||||
if already_pinned:
|
||||
message += _("\nThese cogs were already pinned: ") + humanize_list(already_pinned)
|
||||
await ctx.send(message)
|
||||
await self.send_pagified(ctx, message)
|
||||
|
||||
@cog.command(name="unpin", usage="<cogs>")
|
||||
async def _cog_unpin(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
||||
@@ -801,7 +810,7 @@ class Downloader(commands.Cog):
|
||||
message += _("Unpinned cogs: ") + humanize_list(cognames)
|
||||
if not_pinned:
|
||||
message += _("\nThese cogs weren't pinned: ") + humanize_list(not_pinned)
|
||||
await ctx.send(message)
|
||||
await self.send_pagified(ctx, message)
|
||||
|
||||
@cog.command(name="checkforupdates")
|
||||
async def _cog_checkforupdates(self, ctx: commands.Context) -> None:
|
||||
@@ -833,7 +842,7 @@ class Downloader(commands.Cog):
|
||||
if failed:
|
||||
message += "\n" + self.format_failed_repos(failed)
|
||||
|
||||
await ctx.send(message)
|
||||
await self.send_pagified(ctx, message)
|
||||
|
||||
@cog.command(name="update")
|
||||
async def _cog_update(self, ctx: commands.Context, *cogs: InstalledCog) -> None:
|
||||
@@ -869,7 +878,6 @@ class Downloader(commands.Cog):
|
||||
rev: Optional[str] = None,
|
||||
cogs: Optional[List[InstalledModule]] = None,
|
||||
) -> None:
|
||||
message = ""
|
||||
failed_repos = set()
|
||||
updates_available = set()
|
||||
|
||||
@@ -882,7 +890,7 @@ class Downloader(commands.Cog):
|
||||
await repo.update()
|
||||
except errors.UpdateError:
|
||||
message = self.format_failed_repos([repo.name])
|
||||
await ctx.send(message)
|
||||
await self.send_pagified(ctx, message)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -896,11 +904,10 @@ class Downloader(commands.Cog):
|
||||
f"**{candidate.object_type} {candidate.rev}**"
|
||||
f" - {candidate.description}\n"
|
||||
)
|
||||
for page in pagify(msg):
|
||||
await ctx.send(msg)
|
||||
await self.send_pagified(ctx, msg)
|
||||
return
|
||||
except errors.UnknownRevision:
|
||||
message += _(
|
||||
message = _(
|
||||
"Error: there is no revision `{rev}` in repo `{repo.name}`"
|
||||
).format(rev=rev, repo=repo)
|
||||
await ctx.send(message)
|
||||
@@ -917,6 +924,8 @@ class Downloader(commands.Cog):
|
||||
|
||||
pinned_cogs = {cog for cog in cogs_to_check if cog.pinned}
|
||||
cogs_to_check -= pinned_cogs
|
||||
|
||||
message = ""
|
||||
if not cogs_to_check:
|
||||
cogs_to_update = libs_to_update = ()
|
||||
message += _("There were no cogs to check.")
|
||||
@@ -972,7 +981,7 @@ class Downloader(commands.Cog):
|
||||
if repos_with_libs:
|
||||
message += DEPRECATION_NOTICE.format(repo_list=humanize_list(list(repos_with_libs)))
|
||||
|
||||
await ctx.send(message)
|
||||
await self.send_pagified(ctx, message)
|
||||
|
||||
if updates_available and updated_cognames:
|
||||
await self._ask_for_cog_reload(ctx, updated_cognames)
|
||||
|
||||
@@ -38,6 +38,10 @@ class GitException(DownloaderException):
|
||||
Generic class for git exceptions.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, git_command: str) -> None:
|
||||
self.git_command = git_command
|
||||
super().__init__(f"Git command failed: {git_command}\nError message: {message}")
|
||||
|
||||
|
||||
class InvalidRepoName(DownloaderException):
|
||||
"""
|
||||
@@ -138,8 +142,8 @@ class AmbiguousRevision(GitException):
|
||||
Thrown when specified revision is ambiguous.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, candidates: List[Candidate]) -> None:
|
||||
super().__init__(message)
|
||||
def __init__(self, message: str, git_command: str, candidates: List[Candidate]) -> None:
|
||||
super().__init__(message, git_command)
|
||||
self.candidates = candidates
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import distutils.dir_util
|
||||
import functools
|
||||
import shutil
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
@@ -127,15 +127,13 @@ class Installable(RepoJSONMixin):
|
||||
if self._location.is_file():
|
||||
copy_func = shutil.copy2
|
||||
else:
|
||||
# clear copy_tree's cache to make sure missing directories are created (GH-2690)
|
||||
distutils.dir_util._path_created = {}
|
||||
copy_func = distutils.dir_util.copy_tree
|
||||
copy_func = functools.partial(shutil.copytree, dirs_exist_ok=True)
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
|
||||
except: # noqa: E722
|
||||
log.exception("Error occurred when copying path: {}".format(self._location))
|
||||
log.exception("Error occurred when copying path: %s", self._location)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -203,21 +203,20 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
"""
|
||||
valid_exit_codes = (0, 1)
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(
|
||||
self.GIT_IS_ANCESTOR,
|
||||
path=self.folder_path,
|
||||
maybe_ancestor_rev=maybe_ancestor_rev,
|
||||
descendant_rev=descendant_rev,
|
||||
),
|
||||
valid_exit_codes=valid_exit_codes,
|
||||
git_command = ProcessFormatter().format(
|
||||
self.GIT_IS_ANCESTOR,
|
||||
path=self.folder_path,
|
||||
maybe_ancestor_rev=maybe_ancestor_rev,
|
||||
descendant_rev=descendant_rev,
|
||||
)
|
||||
p = await self._run(git_command, valid_exit_codes=valid_exit_codes)
|
||||
|
||||
if p.returncode in valid_exit_codes:
|
||||
return not bool(p.returncode)
|
||||
raise errors.GitException(
|
||||
f"Git failed to determine if commit {maybe_ancestor_rev}"
|
||||
f" is ancestor of {descendant_rev} for repo at path: {self.folder_path}"
|
||||
f" is ancestor of {descendant_rev} for repo at path: {self.folder_path}",
|
||||
git_command,
|
||||
)
|
||||
|
||||
async def is_on_branch(self) -> bool:
|
||||
@@ -253,15 +252,14 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
if new_rev is None:
|
||||
new_rev = self.branch
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(
|
||||
self.GIT_DIFF_FILE_STATUS, path=self.folder_path, old_rev=old_rev, new_rev=new_rev
|
||||
)
|
||||
git_command = ProcessFormatter().format(
|
||||
self.GIT_DIFF_FILE_STATUS, path=self.folder_path, old_rev=old_rev, new_rev=new_rev
|
||||
)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise errors.GitDiffError(
|
||||
"Git diff failed for repo at path: {}".format(self.folder_path)
|
||||
f"Git diff failed for repo at path: {self.folder_path}", git_command
|
||||
)
|
||||
|
||||
stdout = p.stdout.strip(b"\t\n\x00 ").decode().split("\x00\t")
|
||||
@@ -310,18 +308,17 @@ class Repo(RepoJSONMixin):
|
||||
async with self.checkout(descendant_rev):
|
||||
return discord.utils.get(self.available_modules, name=module_name)
|
||||
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(
|
||||
self.GIT_GET_LAST_MODULE_OCCURRENCE_COMMIT,
|
||||
path=self.folder_path,
|
||||
descendant_rev=descendant_rev,
|
||||
module_name=module_name,
|
||||
)
|
||||
git_command = ProcessFormatter().format(
|
||||
self.GIT_GET_LAST_MODULE_OCCURRENCE_COMMIT,
|
||||
path=self.folder_path,
|
||||
descendant_rev=descendant_rev,
|
||||
module_name=module_name,
|
||||
)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise errors.GitException(
|
||||
"Git log failed for repo at path: {}".format(self.folder_path)
|
||||
f"Git log failed for repo at path: {self.folder_path}", git_command
|
||||
)
|
||||
|
||||
commit = p.stdout.decode().strip()
|
||||
@@ -418,19 +415,18 @@ class Repo(RepoJSONMixin):
|
||||
to get messages for.
|
||||
:return: Git commit note log
|
||||
"""
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(
|
||||
self.GIT_LOG,
|
||||
path=self.folder_path,
|
||||
old_rev=old_rev,
|
||||
relative_file_path=relative_file_path,
|
||||
)
|
||||
git_command = ProcessFormatter().format(
|
||||
self.GIT_LOG,
|
||||
path=self.folder_path,
|
||||
old_rev=old_rev,
|
||||
relative_file_path=relative_file_path,
|
||||
)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise errors.GitException(
|
||||
"An exception occurred while executing git log on"
|
||||
" this repo: {}".format(self.folder_path)
|
||||
f"An exception occurred while executing git log on this repo: {self.folder_path}",
|
||||
git_command,
|
||||
)
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
@@ -457,21 +453,24 @@ class Repo(RepoJSONMixin):
|
||||
Full sha1 object name for provided revision.
|
||||
|
||||
"""
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(self.GIT_GET_FULL_SHA1, path=self.folder_path, rev=rev)
|
||||
git_command = ProcessFormatter().format(
|
||||
self.GIT_GET_FULL_SHA1, path=self.folder_path, rev=rev
|
||||
)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
stderr = p.stderr.decode().strip()
|
||||
ambiguous_error = f"error: short SHA1 {rev} is ambiguous\nhint: The candidates are:\n"
|
||||
if not stderr.startswith(ambiguous_error):
|
||||
raise errors.UnknownRevision(f"Revision {rev} cannot be found.")
|
||||
raise errors.UnknownRevision(f"Revision {rev} cannot be found.", git_command)
|
||||
candidates = []
|
||||
for match in self.AMBIGUOUS_ERROR_REGEX.finditer(stderr, len(ambiguous_error)):
|
||||
candidates.append(Candidate(match["rev"], match["type"], match["desc"]))
|
||||
if candidates:
|
||||
raise errors.AmbiguousRevision(f"Short SHA1 {rev} is ambiguous.", candidates)
|
||||
raise errors.UnknownRevision(f"Revision {rev} cannot be found.")
|
||||
raise errors.AmbiguousRevision(
|
||||
f"Short SHA1 {rev} is ambiguous.", git_command, candidates
|
||||
)
|
||||
raise errors.UnknownRevision(f"Revision {rev} cannot be found.", git_command)
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
@@ -554,17 +553,14 @@ class Repo(RepoJSONMixin):
|
||||
return
|
||||
exists, __ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise errors.MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
|
||||
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(self.GIT_CHECKOUT, path=self.folder_path, rev=rev)
|
||||
)
|
||||
git_command = ProcessFormatter().format(self.GIT_CHECKOUT, path=self.folder_path, rev=rev)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise errors.UnknownRevision(
|
||||
"Could not checkout to {}. This revision may not exist".format(rev)
|
||||
f"Could not checkout to {rev}. This revision may not exist", git_command
|
||||
)
|
||||
|
||||
await self._setup_repo()
|
||||
@@ -619,25 +615,22 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
exists, path = self._existing_git_repo()
|
||||
if exists:
|
||||
raise errors.ExistingGitRepo("A git repo already exists at path: {}".format(path))
|
||||
raise errors.ExistingGitRepo(f"A git repo already exists at path: {path}")
|
||||
|
||||
if self.branch is not None:
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(
|
||||
self.GIT_CLONE, branch=self.branch, url=self.url, folder=self.folder_path
|
||||
)
|
||||
git_command = ProcessFormatter().format(
|
||||
self.GIT_CLONE, branch=self.branch, url=self.url, folder=self.folder_path
|
||||
)
|
||||
else:
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(
|
||||
self.GIT_CLONE_NO_BRANCH, url=self.url, folder=self.folder_path
|
||||
)
|
||||
git_command = ProcessFormatter().format(
|
||||
self.GIT_CLONE_NO_BRANCH, url=self.url, folder=self.folder_path
|
||||
)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode:
|
||||
# Try cleaning up folder
|
||||
shutil.rmtree(str(self.folder_path), ignore_errors=True)
|
||||
raise errors.CloningError("Error when running git clone.")
|
||||
raise errors.CloningError("Error when running git clone.", git_command)
|
||||
|
||||
if self.branch is None:
|
||||
self.branch = await self.current_branch()
|
||||
@@ -657,17 +650,14 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
exists, __ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise errors.MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
|
||||
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(self.GIT_CURRENT_BRANCH, path=self.folder_path)
|
||||
)
|
||||
git_command = ProcessFormatter().format(self.GIT_CURRENT_BRANCH, path=self.folder_path)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise errors.GitException(
|
||||
"Could not determine current branch at path: {}".format(self.folder_path)
|
||||
f"Could not determine current branch at path: {self.folder_path}", git_command
|
||||
)
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
@@ -683,16 +673,13 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
exists, __ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise errors.MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
|
||||
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(self.GIT_CURRENT_COMMIT, path=self.folder_path)
|
||||
)
|
||||
git_command = ProcessFormatter().format(self.GIT_CURRENT_COMMIT, path=self.folder_path)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise errors.CurrentHashError("Unable to determine commit hash.")
|
||||
raise errors.CurrentHashError("Unable to determine commit hash.", git_command)
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
@@ -715,16 +702,15 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
exists, __ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise errors.MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
|
||||
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(self.GIT_LATEST_COMMIT, path=self.folder_path, branch=branch)
|
||||
git_command = ProcessFormatter().format(
|
||||
self.GIT_LATEST_COMMIT, path=self.folder_path, branch=branch
|
||||
)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise errors.CurrentHashError("Unable to determine latest commit hash.")
|
||||
raise errors.CurrentHashError("Unable to determine latest commit hash.", git_command)
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
@@ -751,10 +737,11 @@ class Repo(RepoJSONMixin):
|
||||
if folder is None:
|
||||
folder = self.folder_path
|
||||
|
||||
p = await self._run(ProcessFormatter().format(Repo.GIT_DISCOVER_REMOTE_URL, path=folder))
|
||||
git_command = ProcessFormatter().format(Repo.GIT_DISCOVER_REMOTE_URL, path=folder)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise errors.NoRemoteURL("Unable to discover a repo URL.")
|
||||
raise errors.NoRemoteURL("Unable to discover a repo URL.", git_command)
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
@@ -773,19 +760,18 @@ class Repo(RepoJSONMixin):
|
||||
await self.checkout(branch)
|
||||
exists, __ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise errors.MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
raise errors.MissingGitRepo(f"A git repo does not exist at path: {self.folder_path}")
|
||||
|
||||
p = await self._run(
|
||||
ProcessFormatter().format(self.GIT_HARD_RESET, path=self.folder_path, branch=branch)
|
||||
git_command = ProcessFormatter().format(
|
||||
self.GIT_HARD_RESET, path=self.folder_path, branch=branch
|
||||
)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise errors.HardResetError(
|
||||
"Some error occurred when trying to"
|
||||
" execute a hard reset on the repo at"
|
||||
" the following path: {}".format(self.folder_path)
|
||||
"Some error occurred when trying to execute a hard reset on the repo at"
|
||||
f" the following path: {self.folder_path}",
|
||||
git_command,
|
||||
)
|
||||
|
||||
async def update(self) -> Tuple[str, str]:
|
||||
@@ -795,7 +781,7 @@ class Repo(RepoJSONMixin):
|
||||
-------
|
||||
`tuple` of `str`
|
||||
:py:code`(old commit hash, new commit hash)`
|
||||
|
||||
|
||||
Raises
|
||||
-------
|
||||
`UpdateError` - if git pull results with non-zero exit code
|
||||
@@ -804,12 +790,14 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
await self.hard_reset()
|
||||
|
||||
p = await self._run(ProcessFormatter().format(self.GIT_PULL, path=self.folder_path))
|
||||
git_command = ProcessFormatter().format(self.GIT_PULL, path=self.folder_path)
|
||||
p = await self._run(git_command)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise errors.UpdateError(
|
||||
"Git pull returned a non zero exit code"
|
||||
" for the repo located at path: {}".format(self.folder_path)
|
||||
f" for the repo located at path: {self.folder_path}",
|
||||
git_command,
|
||||
)
|
||||
|
||||
await self._setup_repo()
|
||||
@@ -1114,7 +1102,7 @@ class RepoManager:
|
||||
"""
|
||||
repo = self.get_repo(name)
|
||||
if repo is None:
|
||||
raise errors.MissingGitRepo("There is no repo with the name {}".format(name))
|
||||
raise errors.MissingGitRepo(f"There is no repo with the name {name}")
|
||||
|
||||
safe_delete(repo.folder_path)
|
||||
|
||||
|
||||
@@ -26,6 +26,13 @@ class ModLog(commands.Cog):
|
||||
"""Manage modlog settings."""
|
||||
pass
|
||||
|
||||
@checks.is_owner()
|
||||
@modlogset.command(hidden=True, name="fixcasetypes")
|
||||
async def reapply_audittype_migration(self, ctx: commands.Context):
|
||||
"""Command to fix misbehaving casetypes."""
|
||||
await modlog.handle_auditype_key()
|
||||
await ctx.tick()
|
||||
|
||||
@modlogset.command()
|
||||
@commands.guild_only()
|
||||
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
|
||||
|
||||
@@ -12,7 +12,6 @@ from redbot.cogs.warnings.helpers import (
|
||||
from redbot.core import Config, checks, commands, modlog
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.mod import is_admin_or_superior
|
||||
from redbot.core.utils.chat_formatting import warning, pagify
|
||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||
|
||||
@@ -342,30 +341,16 @@ class Warnings(commands.Cog):
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def warnings(
|
||||
self, ctx: commands.Context, user: Optional[Union[discord.Member, int]] = None
|
||||
):
|
||||
"""List the warnings for the specified user.
|
||||
@checks.admin()
|
||||
async def warnings(self, ctx: commands.Context, user: Union[discord.Member, int]):
|
||||
"""List the warnings for the specified user."""
|
||||
|
||||
Omit `<user>` to see your own warnings.
|
||||
|
||||
Note that showing warnings for users other than yourself requires
|
||||
appropriate permissions.
|
||||
"""
|
||||
if user is None:
|
||||
user = ctx.author
|
||||
else:
|
||||
if not await is_admin_or_superior(self.bot, ctx.author):
|
||||
return await ctx.send(
|
||||
warning(_("You are not allowed to check warnings for other users!"))
|
||||
)
|
||||
|
||||
try:
|
||||
userid: int = user.id
|
||||
except AttributeError:
|
||||
userid: int = user
|
||||
user = ctx.guild.get_member(userid)
|
||||
user = user or namedtuple("Member", "id guild")(userid, ctx.guild)
|
||||
try:
|
||||
userid: int = user.id
|
||||
except AttributeError:
|
||||
userid: int = user
|
||||
user = ctx.guild.get_member(userid)
|
||||
user = user or namedtuple("Member", "id guild")(userid, ctx.guild)
|
||||
|
||||
msg = ""
|
||||
member_settings = self.config.member(user)
|
||||
@@ -389,6 +374,35 @@ class Warnings(commands.Cog):
|
||||
pagify(msg, shorten_by=58), box_lang=_("Warnings for {user}").format(user=user)
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def mywarnings(self, ctx: commands.Context):
|
||||
"""List warnings for yourself."""
|
||||
|
||||
user = ctx.author
|
||||
|
||||
msg = ""
|
||||
member_settings = self.config.member(user)
|
||||
async with member_settings.warnings() as user_warnings:
|
||||
if not user_warnings.keys(): # no warnings for the user
|
||||
await ctx.send(_("You have no warnings!"))
|
||||
else:
|
||||
for key in user_warnings.keys():
|
||||
mod_id = user_warnings[key]["mod"]
|
||||
mod = ctx.bot.get_user(mod_id) or _("Unknown Moderator ({})").format(mod_id)
|
||||
msg += _(
|
||||
"{num_points} point warning {reason_name} issued by {user} for "
|
||||
"{description}\n"
|
||||
).format(
|
||||
num_points=user_warnings[key]["points"],
|
||||
reason_name=key,
|
||||
user=mod,
|
||||
description=user_warnings[key]["description"],
|
||||
)
|
||||
await ctx.send_interactive(
|
||||
pagify(msg, shorten_by=58), box_lang=_("Warnings for {user}").format(user=user)
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
|
||||
Reference in New Issue
Block a user