From cc4ccd968f2af9842c529fd9a314f95356e69db6 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sun, 11 Feb 2024 19:56:38 +0800 Subject: [PATCH] Improving the Playlist Function Implementation --- cogs/playlist.py | 87 ++++++++++++++++++++++++------------------ cogs/task.py | 2 +- function.py | 98 ++++++++++++++++++++++++++++++------------------ 3 files changed, 113 insertions(+), 74 deletions(-) diff --git a/cogs/playlist.py b/cogs/playlist.py index 2a093d5..2da34af 100644 --- a/cogs/playlist.py +++ b/cogs/playlist.py @@ -31,7 +31,6 @@ from function import ( get_playlist, check_roles, update_playlist, - update_inbox, get_lang, settings, get_aliases, @@ -46,19 +45,19 @@ def assign_playlistId(existed: list) -> str: if str(i) not in existed: return str(i) -async def check_playlist_perms(userid: int, authorid: int, dId: str) -> dict: - playlist = await get_playlist(authorid, 'playlist', dId) - if not playlist or userid not in playlist['perms']['read']: +async def check_playlist_perms(user_id: int, author_id: int, d_id: str) -> dict: + playlist = await get_playlist(author_id, 'playlist', d_id) + if not playlist or user_id not in playlist['perms']['read']: return {} return playlist async def check_playlist(ctx: commands.Context, name: str = None, full: bool = False, share: bool = True) -> dict: user = await get_playlist(ctx.author.id, 'playlist') - if not user: - return None + await ctx.defer() if full: return user + if not name: return {'playlist': user['200'], 'position': 1, 'id': "200"} @@ -77,11 +76,10 @@ async def search_playlist(url: str, requester: discord.Member, timeNeed=True): tracks = await voicelink.NodePool.get_node().get_tracks(url, requester=requester) tracks = {"name": tracks.name, "tracks": tracks.tracks} if timeNeed: - time = 0 - for track in tracks['tracks']: - time += track.length + time = sum([track for track in tracks["tracks"]]) except: return None + return tracks | ({'time': ctime(time)} if timeNeed else {}) class Playlists(commands.Cog, name="playlist"): @@ -90,11 +88,8 @@ class Playlists(commands.Cog, name="playlist"): self.description = "This is the Vocard playlist system. You can save your favorites and use Vocard to play on any server." async def playlist_autocomplete(self, interaction: discord.Interaction, current: str) -> list: - playlists = PLAYLIST_NAME.get(str(interaction.user.id), None) - if not playlists: - playlists_raw = await get_playlist(interaction.user.id, 'playlist') - playlists = PLAYLIST_NAME[str(interaction.user.id)] = [ - value['name'] for value in playlists_raw.values()] if playlists_raw else [] + playlists_raw: dict[str, dict] = await get_playlist(interaction.user.id, 'playlist') + playlists = [value['name'] for value in playlists_raw.values()] if playlists_raw else [] if current: return [app_commands.Choice(name=p, value=p) for p in playlists if current in p] return [app_commands.Choice(name=p, value=p) for p in playlists] @@ -168,16 +163,19 @@ class Playlists(commands.Cog, name="playlist"): if playlist['type'] == 'link': tracks = await search_playlist(playlist['uri'], requester=ctx.author) results.append({'emoji': ('🔒' if max_p < index else '🔗'), 'id': data, 'time': tracks['time'], 'name': playlist['name'], 'tracks': tracks['tracks'], 'perms': playlist['perms'], 'type': playlist['type']}) + else: if share := playlist['type'] == 'share': playlist = await check_playlist_perms(ctx.author.id, playlist['user'], playlist['referId']) if not playlist: - await update_playlist(ctx.author.id, {f"playlist.{data}": 1}, mode="unset") + await update_playlist(ctx.author.id, {"$unset": {f"playlist.{data}": 1}}) continue + if playlist['type'] == 'link': tracks = await search_playlist(playlist['uri'], requester=ctx.author) results.append({'emoji': ('🔒' if max_p < index else '🤝'), 'id': data, 'time': tracks['time'], 'name': user[data]['name'], 'tracks': tracks['tracks'], 'perms': playlist['perms'], 'owner': user[data]['user'], 'type': 'share'}) continue + init = [] for track in playlist['tracks']: dt = voicelink.decode(track) @@ -186,12 +184,14 @@ class Playlists(commands.Cog, name="playlist"): playlist['tracks'] = init results.append({'emoji': ('🔒' if max_p < index else ('🤝' if share else '❤️')), 'id': data, 'time': ctime(time), 'name': user[data]['name'], 'tracks': playlist['tracks'], 'perms': playlist['perms'], 'owner': user[data].get('user', None), 'type': user[data]['type']}) - except Exception as e: + except: results.append({'emoji': '⛔', 'id': data, 'time': '00:00', 'name': 'Error', 'tracks': [], 'type': 'error'}) - embed = discord.Embed(title=get_lang(ctx.guild.id, 'playlistViewTitle').format(ctx.author.name), - description='```%0s %4s %10s %10s %10s\n' % tuple(get_lang(ctx.guild.id, 'playlistViewHeaders')) + '\n'.join('%0s %3s. %10s %10s %10s' % (info['emoji'], info['id'], f"[{info['time']}]", info['name'], len(info['tracks'])) for info in results) + '```', - color=settings.embed_color) + embed = discord.Embed( + title=get_lang(ctx.guild.id, 'playlistViewTitle').format(ctx.author.display_name), + description='```%0s %4s %10s %10s %10s\n' % tuple(get_lang(ctx.guild.id, 'playlistViewHeaders')) + '\n'.join('%0s %3s. %10s %10s %10s' % (info['emoji'], info['id'], f"[{info['time']}]", info['name'], len(info['tracks'])) for info in results) + '```', + color=settings.embed_color + ) embed.add_field(name=get_lang(ctx.guild.id, 'playlistMaxP'), value=f"➥ {len(user)}/{max_p}", inline=True) embed.add_field(name=get_lang(ctx.guild.id, 'playlistMaxT'), value=f"➥ {max_t}", inline=True) @@ -226,7 +226,7 @@ class Playlists(commands.Cog, name="playlist"): return await ctx.send(get_lang(ctx.guild.id, 'playlistNotInvaildUrl'), ephemeral=True) data = {'uri': link, 'perms': {'read': []}, 'name': name, 'type': 'link'} if link else {'tracks': [], 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'} - await update_playlist(ctx.author.id, {f"playlist.{assign_playlistId([data for data in user])}": data}, update_cache=True) + await update_playlist(ctx.author.id, {"$set": {f"playlist.{assign_playlistId([data for data in user])}": data}}) await ctx.send(get_lang(ctx.guild.id, 'playlistCreated').format(name)) @playlist.command(name="delete", aliases=get_aliases("delete")) @@ -242,9 +242,9 @@ class Playlists(commands.Cog, name="playlist"): return await ctx.send(get_lang(ctx.guild.id, 'playlistDeleteError'), ephemeral=True) if result['playlist']['type'] == 'share': - await update_playlist(result['playlist']['user'], {f"playlist.{result['playlist']['referId']}.perms.read": ctx.author.id}, mode="pull") + await update_playlist(result['playlist']['user'], {"$pull": {f"playlist.{result['playlist']['referId']}.perms.read": ctx.author.id}}) - await update_playlist(ctx.author.id, {f"playlist.{result['id']}": 1}, mode="unset", update_cache=True) + await update_playlist(ctx.author.id, {"$unset": {f"playlist.{result['id']}": 1}}) return await ctx.send(get_lang(ctx.guild.id, 'playlistRemove').format(result['playlist']['name'])) @playlist.command(name="share", aliases=get_aliases("share")) @@ -278,7 +278,17 @@ class Playlists(commands.Cog, name="playlist"): if len(receiver['inbox']) >= 10: return await ctx.send(get_lang(ctx.guild.id, 'inboxFull').format(member), ephemeral=True) - await update_inbox(member.id, {'sender': ctx.author.id, 'referId': result['id'], 'time': datetime.now(), 'title': f'Playlist invitation from {ctx.author}', 'description': f"You are invited to use this playlist.\nPlaylist Name: {result['playlist']['name']}\nPlaylist type: {result['playlist']['type']}", 'type': 'invite'}) + await update_playlist( + member.id, + {"$push": {"inbox": { + 'sender': ctx.author.id, + 'referId': result['id'], + 'time': datetime.now(), + 'title': f'Playlist invitation from {ctx.author}', + 'description': f"You are invited to use this playlist.\nPlaylist Name: {result['playlist']['name']}\nPlaylist type: {result['playlist']['type']}", + 'type': 'invite' + }}} + ) return await ctx.send(get_lang(ctx.guild.id, 'invitationSent').format(member)) @playlist.command(name="rename", aliases=get_aliases("rename")) @@ -305,7 +315,7 @@ class Playlists(commands.Cog, name="playlist"): if not found: return await ctx.send(get_lang(ctx.guild.id, 'playlistNotFound').format(name), ephemeral=True) - await update_playlist(ctx.author.id, {f'playlist.{id}.name': newname}, update_cache=True) + await update_playlist(ctx.author.id, {"$set": {f'playlist.{id}.name': newname}}) await ctx.send(get_lang(ctx.guild.id, 'playlistRenamed').format(name, newname)) @playlist.command(name="inbox", aliases=get_aliases("inbox")) @@ -313,6 +323,8 @@ class Playlists(commands.Cog, name="playlist"): async def inbox(self, ctx: commands.Context) -> None: "Show your playlist invitation." user = await get_playlist(ctx.author.id) + rank, max_p, max_t = check_roles() + if not user['inbox']: return await ctx.send(get_lang(ctx.guild.id, 'inboxNoMsg'), ephemeral=True) @@ -323,15 +335,20 @@ class Playlists(commands.Cog, name="playlist"): if inbox == user['inbox']: return - updateData, dId = {}, {dId for dId in user["playlist"]} - for data in view.newplaylist[:(5 - len(user['playlist']))]: + + update_data, dId = {}, {dId for dId in user["playlist"]} + for data in view.newplaylist[:(max_p - len(user['playlist']))]: addId = assign_playlistId(dId) - await update_playlist(data['sender'], {f"playlist.{data['referId']}.perms.read": ctx.author.id}, mode="push") - updateData[f'playlist.{addId}'] = {'user': data['sender'], 'referId': data['referId'], - 'name': f"Share{data['time'].strftime('%M%S')}", 'type': 'share'} + await update_playlist(data['sender'], {"$push": {f"playlist.{data['referId']}.perms.read": ctx.author.id}}) + update_data[f'playlist.{addId}'] = { + 'user': data['sender'], 'referId': data['referId'], + 'name': f"Share{data['time'].strftime('%M%S')}", 'type': 'share' + } + update_data["inbox"] = view.inbox dId.add(addId) - await update_playlist(ctx.author.id, updateData | {'inbox': view.inbox}, update_cache=True) + if update_data: + await update_playlist(ctx.author.id, {"$set": update_data}) @playlist.command(name="add", aliases=get_aliases("add")) @commands.dynamic_cooldown(cooldown_check, commands.BucketType.guild) @@ -362,7 +379,7 @@ class Playlists(commands.Cog, name="playlist"): if results[0].is_stream: return await ctx.send(get_lang(ctx.guild.id, 'playlistStream'), ephemeral=True) - await update_playlist(ctx.author.id, {f'playlist.{result["id"]}.tracks': results[0].track_id}, mode="push") + await update_playlist(ctx.author.id, {"$push": {f'playlist.{result["id"]}.tracks': results[0].track_id}}) await ctx.send(get_lang(ctx.guild.id, 'playlistAdded').format(results[0].title, ctx.author, result['playlist']['name'])) @playlist.command(name="remove", aliases=get_aliases("remove")) @@ -382,7 +399,7 @@ class Playlists(commands.Cog, name="playlist"): if not 0 < position <= len(result['playlist']['tracks']): return await ctx.send(get_lang(ctx.guild.id, 'playlistPositionNotFound').format(position, name)) - await update_playlist(ctx.author.id, {f'playlist.{result["id"]}.tracks': result['playlist']['tracks'][position - 1]}, mode="pull") + await update_playlist(ctx.author.id, {"$pull": {f'playlist.{result["id"]}.tracks': result['playlist']['tracks'][position - 1]}}) track = voicelink.decode(result['playlist']['tracks'][position - 1]) await ctx.send(get_lang(ctx.guild.id, 'playlistRemoved').format(track.get("title"), ctx.author, name)) @@ -399,7 +416,7 @@ class Playlists(commands.Cog, name="playlist"): if result['playlist']['type'] in ['link', 'share']: return await ctx.send(get_lang(ctx.guild.id, 'playlistNotAllow'), ephemeral=True) - await update_playlist(ctx.author.id, {f'playlist.{result["id"]}.tracks': []}) + await update_playlist(ctx.author.id, {"$set": {f'playlist.{result["id"]}.tracks': []}}) await ctx.send(get_lang(ctx.guild.id, 'playlistClear').format(name)) @playlist.command(name="export", aliases=get_aliases("export")) @@ -408,8 +425,6 @@ class Playlists(commands.Cog, name="playlist"): async def export(self, ctx: commands.Context, name: str) -> None: "Exports the entire playlist to a text file" result = await check_playlist(ctx, name.lower()) - if not result: - return await create_account(ctx) if not result['playlist']: return await ctx.send(get_lang(ctx.guild.id, 'playlistNotFound').format(name), ephemeral=True) @@ -472,7 +487,7 @@ class Playlists(commands.Cog, name="playlist"): track_ids = track_ids.decode().split(",") data = {'tracks': track_ids, 'perms': {'read': [], 'write': [], 'remove': []}, 'name': name, 'type': 'playlist'} - await update_playlist(ctx.author.id, {f"playlist.{assign_playlistId([data for data in user])}": data}, update_cache=True) + await update_playlist(ctx.author.id, {"$set": {f"playlist.{assign_playlistId([data for data in user])}": data}}) await ctx.send(get_lang(ctx.guild.id, 'playlistCreated').format(name)) except: diff --git a/cogs/task.py b/cogs/task.py index bd445fb..97324f0 100644 --- a/cogs/task.py +++ b/cogs/task.py @@ -111,7 +111,7 @@ class Task(commands.Cog): @tasks.loop(hours=12.0) async def cache_cleaner(self): func.SETTINGS_BUFFER.clear() - func.PLAYLIST_NAME.clear() + func.PLAYLISTS_BUFFER.clear() errorFile = func.gen_report() if errorFile: diff --git a/function.py b/function.py index d68ea52..ee6fb02 100644 --- a/function.py +++ b/function.py @@ -1,10 +1,10 @@ -import discord, json, os +import discord, json, os, copy from discord.ext import commands from datetime import datetime from time import strptime from io import BytesIO -from typing import Optional, Union, Any +from typing import Optional, Dict, Any from addons import Settings, TOKENS from motor.motor_asyncio import ( @@ -29,7 +29,19 @@ ERROR_LOGS: dict[int, dict[int, str]] = {} #Stores error that not a Voicelink Ex LANGS: dict[str, dict[str, str]] = {} #Stores all the languages in ./langs SETTINGS_BUFFER: dict[int, dict[str, Any]] = {} #Cache guild language LOCAL_LANGS: dict[str, dict[str, str]] = {} #Stores all the localization languages in ./local_langs -PLAYLIST_NAME: dict[str, list[str]] = {} #Cache the user's playlist name +PLAYLISTS_BUFFER: dict[str, dict] = {} + +PLAYLIST_BASE: dict[str, Any] = { + 'playlist': { + '200': { + 'tracks':[], + 'perms': {'read': [], 'write':[], 'remove': []}, + 'name':'Favourite', + 'type':'playlist' + } + }, + 'inbox':[] +} #-------------- Vocard Functions -------------- def open_json(path: str) -> dict: @@ -50,7 +62,7 @@ def update_json(path: str, new_data: dict) -> None: json.dump(data, json_file, indent=4) def get_lang(guild_id:int, key:str) -> str: - lang = SETTINGS_BUFFER.get(guild_id).get("lang", "EN") + lang = SETTINGS_BUFFER.get(guild_id, {}).get("lang", "EN") if lang in LANGS and not LANGS[lang]: LANGS[lang] = open_json(os.path.join("langs", f"{lang}.json")) @@ -122,23 +134,12 @@ def get_aliases(name: str) -> list: def check_roles() -> tuple[str, int, int]: return 'Normal', 5, 500 -async def get_settings(guild_id:int) -> dict[str, Any]: - settings = SETTINGS_BUFFER.get(guild_id, None) - if not settings: - settings = await SETTINGS_DB.find_one({"_id": guild_id}) - if not settings: - await SETTINGS_DB.insert_one({"_id": guild_id}) - - settings = SETTINGS_BUFFER[guild_id] = settings or {} - return settings - -async def update_settings(guild_id: int, data: dict[str, dict[str, Any]]) -> bool: - settings = await get_settings(guild_id) +async def update_db(db: AsyncIOMotorCollection, tempStore: dict, filter: dict, data: dict) -> bool: for mode, action in data.items(): for key, value in action.items(): cursors = key.split(".") - nested_data = settings + nested_data = tempStore for c in cursors[:-1]: nested_data = nested_data.setdefault(c, {}) @@ -151,32 +152,55 @@ async def update_settings(guild_id: int, data: dict[str, dict[str, Any]]) -> boo elif mode == "$unset": nested_data.pop(cursors[-1], None) + elif mode == "$inc": + nested_data[cursors[-1]] = nested_data.get(cursors[-1], 0) + value + + elif mode == "$push": + nested_data.setdefault(cursors[-1], []).extend([value]) + + elif mode == "$pull": + if cursors[-1] in nested_data: + value = value.get("$in", []) if isinstance(value, dict) else [value] + nested_data[cursors[-1]] = [item for item in nested_data[cursors[-1]] if item not in value] + else: return False - result = await SETTINGS_DB.update_one({"_id": guild_id}, data) + result = await db.update_one(filter, data) return result.modified_count > 0 -async def create_account(ctx: Union[commands.Context, discord.Interaction]) -> None: async def get_settings(guild_id:int) -> dict[str, Any]: - + settings = SETTINGS_BUFFER.get(guild_id, None) + if not settings: + settings = await SETTINGS_DB.find_one({"_id": guild_id}) + if not settings: + await SETTINGS_DB.insert_one({"_id": guild_id}) -async def get_playlist(user_id:int, dType:str=None, dId:str=None) -> bool: - user = await PLAYLISTS_DB.find_one({"_id":user_id}, {"_id": 0}) - if not user: - return None - if dType: - if dId and dType == "playlist": - return user[dType][dId] if dId in user[dType] else None - return user[dType] - return user + settings = SETTINGS_BUFFER[guild_id] = settings or {} + return settings -async def update_playlist(user_id:int, data:dict, *, mode:str="set", update_cache: bool=False) -> None: - if update_cache: - PLAYLIST_NAME.pop(str(user_id), None) - result = await PLAYLISTS_DB.update_one({"_id":user_id}, {f"${mode}": data}) - return result.modified_count > 0 +async def update_settings(guild_id: int, data: dict[str, dict[str, Any]]) -> bool: + settings = await get_settings(guild_id) + return await update_db(SETTINGS_DB, settings, {"_id": guild_id}, data) + +async def get_playlist(user_id: int, d_type: Optional[str] = None, d_id: Optional[str] = None, need_copy: bool = True) -> Dict[str, Any]: + playlist = PLAYLISTS_BUFFER.get(user_id) + if not playlist: + playlist = await PLAYLISTS_DB.find_one({"_id": user_id}) + if not playlist: + playlist = {"_id": user_id, **PLAYLIST_BASE} + await PLAYLISTS_DB.insert_one(playlist) + + PLAYLISTS_BUFFER[user_id] = playlist -async def update_inbox(user_id:int, data:dict) -> bool: - result = await PLAYLISTS_DB.update_one({"_id":user_id}, {"$push":{'inbox':data}}) - return result.modified_count > 0 \ No newline at end of file + if d_type: + if d_id and d_type == "playlist": + playlist = playlist[d_type].get(d_id) + else: + playlist = playlist.get(d_type) + + return copy.deepcopy(playlist) if need_copy else playlist + +async def update_playlist(user_id:int, data:dict) -> None: + playlist = await get_playlist(user_id, need_copy=False) + return await update_db(PLAYLISTS_DB, playlist, {"_id": user_id}, data) \ No newline at end of file